[
  {
    "path": ".dir-locals.el",
    "content": "((nil . ((fill-column . 80)\n         (project-vc-ignores . (\"./_build\"))\n         (require-final-newline . t)\n         (eval . (add-hook 'before-save-hook 'delete-trailing-whitespace nil t))))\n (org-mode . ((org-edit-src-content-indentation 0)))\n (lisp-mode\n  . ((eval . (cl-flet ((enhance-imenu-lisp\n                        (&rest keywords)\n                        (dolist (keyword keywords)\n                          (let ((prefix (when (listp keyword) (cl-second keyword)))\n                                (keyword (if (listp keyword)\n                                             (cl-first keyword)\n                                           keyword)))\n                            (add-to-list\n                             'lisp-imenu-generic-expression\n                             (list (purecopy (concat (capitalize keyword)\n                                                     (if (string= (substring-no-properties keyword -1) \"s\")\n                                                         \"es\"\n                                                       \"s\")))\n                                   (purecopy (concat \"^\\\\s-*(\"\n                                                     (regexp-opt\n                                                      (list (if prefix\n                                                                (concat prefix \"-\" keyword)\n                                                              keyword)\n                                                            (concat prefix \"-\" keyword))\n                                                      t)\n                                                     \"\\\\s-+\\\\(\" lisp-mode-symbol-regexp \"\\\\)\"))\n                                   2))))))\n               ;; This adds the argument to the list of imenu known keywords.\n               (enhance-imenu-lisp\n                '(\"bookmarklet-command\" \"define\")\n                '(\"class\" \"define\")\n                '(\"command\" \"define\")\n                '(\"ffi-method\" \"define\")\n                '(\"ffi-generic\" \"define\")\n                '(\"function\" \"define\")\n                '(\"internal-page-command\" \"define\")\n                '(\"internal-page-command-global\" \"define\")\n                '(\"mode\" \"define\")\n                '(\"parenscript\" \"define\")\n                \"defpsmacro\"))))))\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Bug report\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the bug**\n\n**Steps to reproduce the issue**\n\n**Information**\n- OS Name+Version:\n- Installation method (Flatpak, Guix, package manager, build from source):\n- Output of Nyxt command `show-system-information`:\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: feature\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\n\n\n**Describe the solution you'd like**\n\n\n**Describe alternatives you've considered**\n\n\n**Additional context**\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/ui_request.md",
    "content": "---\nname: UI request\nabout: Suggest a UI change for this project\ntitle: ''\nlabels: ui/ux\nassignees: ''\n\n---\n\n**Please describe the UI issue:**\n\n\n**Describe the solution you'd like**\n\n\n**Describe alternatives you've considered**\n\n\n**Additional context:**\n\n\n**Screenshots/Mock ups:**\n"
  },
  {
    "path": ".github/SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\nOnly the latest stable version is currently supported with security updates.\n\n## Reporting a Vulnerability\n\nhello@atlas.engineer\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "# Description\n\n- Please include a summary of the change.\n\nFixes # (issue)\n\n# Checklist:\n\n- [ ] Git branch state is mergable.\n- [ ] Changelog is up to date (via a separate commit).\n- [ ] New dependencies are accounted for.\n- [ ] Documentation is up to date.\n- [ ] Compilation and tests (`(asdf:test-system :nyxt/<renderer>)`)\n  - No new compilation warnings.\n  - Tests are sufficient.\n"
  },
  {
    "path": ".gitignore",
    "content": "# Ignore build artifacts\nnyxt\nbuild/\nnode_modules/\npackage.json\npackage-lock.json\n\n# Ignore compiled lisp files\n*.FASL\n*.fasl\n*.fas\n*.lisp-temp\n*.dfsl\n*.pfsl\n*.d64fsl\n*.p64fsl\n*.lx64fsl\n*.lx32fsl\n*.dx64fsl\n*.dx32fsl\n*.fx64fsl\n*.fx32fsl\n*.sx64fsl\n*.sx32fsl\n*.wx64fsl\n*.wx32fsl\n\n# Ignore PNG and XCF\n*.xcf\n*.png\n\n# Generated documentation\nmanual.html\n\n# Ignore etags/ctags\nTAGS\n\n# Ignore C object files and libraries\n*.o\n*.so\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"_build/alexandria\"]\n\tpath = _build/alexandria\n\turl = https://gitlab.common-lisp.net/alexandria/alexandria.git\n\tshallow = true\n[submodule \"_build/bordeaux-threads\"]\n\tpath = _build/bordeaux-threads\n\turl = https://github.com/sionescu/bordeaux-threads\n\tshallow = true\n[submodule \"_build/calispel\"]\n\tpath = _build/calispel\n\turl = https://github.com/hawkir/calispel\n\tshallow = true\n[submodule \"_build/cl-jpl-util\"]\n\tpath = _build/cl-jpl-util\n\turl = https://github.com/hawkir/cl-jpl-util\n\tshallow = true\n[submodule \"_build/trivial-garbage\"]\n\tpath = _build/trivial-garbage\n\turl = https://github.com/trivial-garbage/trivial-garbage\n\tshallow = true\n[submodule \"_build/cl-containers\"]\n\tpath = _build/cl-containers\n\turl = https://github.com/gwkkwg/cl-containers\n\tshallow = true\n[submodule \"_build/metatilities-base\"]\n\tpath = _build/metatilities-base\n\turl = https://github.com/gwkkwg/metatilities-base\n\tshallow = true\n[submodule \"_build/cl-custom-hash-table\"]\n\tpath = _build/cl-custom-hash-table\n\turl = https://github.com/metawilm/cl-custom-hash-table\n\tshallow = true\n[submodule \"_build/cl-ppcre\"]\n\tpath = _build/cl-ppcre\n\turl = https://github.com/edicl/cl-ppcre\n\tshallow = true\n[submodule \"_build/flexi-streams\"]\n\tpath = _build/flexi-streams\n\turl = https://github.com/edicl/flexi-streams\n\tshallow = true\n[submodule \"_build/trivial-gray-streams\"]\n\tpath = _build/trivial-gray-streams\n\turl = https://github.com/trivial-gray-streams/trivial-gray-streams\n\tshallow = true\n[submodule \"_build/cl-prevalence\"]\n\tpath = _build/cl-prevalence\n\turl = https://github.com/40ants/cl-prevalence\n\tshallow = true\n[submodule \"_build/s-sysdeps\"]\n\tpath = _build/s-sysdeps\n\turl = https://github.com/svenvc/s-sysdeps\n\tshallow = true\n[submodule \"_build/usocket\"]\n\tpath = _build/usocket\n\turl = https://github.com/usocket/usocket/\n\tshallow = true\n[submodule \"_build/split-sequence\"]\n\tpath = _build/split-sequence\n\turl = https://github.com/sharplispers/split-sequence\n\tshallow = true\n[submodule \"_build/closer-mop\"]\n\tpath = _build/closer-mop\n\turl = https://github.com/pcostanza/closer-mop\n\tshallow = true\n[submodule \"_build/cluffer\"]\n\tpath = _build/cluffer\n\turl = https://github.com/robert-strandh/cluffer\n\tshallow = true\n[submodule \"_build/Acclimation\"]\n\tpath = _build/Acclimation\n\turl = https://github.com/robert-strandh/Acclimation\n\tshallow = true\n[submodule \"_build/Clump\"]\n\tpath = _build/Clump\n\turl = https://github.com/robert-strandh/Clump\n\tshallow = true\n[submodule \"_build/dexador\"]\n\tpath = _build/dexador\n\turl = https://github.com/fukamachi/dexador\n\tshallow = true\n[submodule \"_build/babel\"]\n\tpath = _build/babel\n\turl = https://github.com/cl-babel/babel\n\tshallow = true\n[submodule \"_build/trivial-features\"]\n\tpath = _build/trivial-features\n\turl = https://github.com/trivial-features/trivial-features\n\tshallow = true\n[submodule \"_build/fast-http\"]\n\tpath = _build/fast-http\n\turl = https://github.com/fukamachi/fast-http\n\tshallow = true\n[submodule \"_build/proc-parse\"]\n\tpath = _build/proc-parse\n\turl = https://github.com/fukamachi/proc-parse\n\tshallow = true\n[submodule \"_build/anaphora\"]\n\tpath = _build/anaphora\n\turl = https://github.com/tokenrove/anaphora\n\tshallow = true\n[submodule \"_build/xsubseq\"]\n\tpath = _build/xsubseq\n\turl = https://github.com/fukamachi/xsubseq\n\tshallow = true\n[submodule \"_build/smart-buffer\"]\n\tpath = _build/smart-buffer\n\turl = https://github.com/fukamachi/smart-buffer\n\tshallow = true\n[submodule \"_build/cl-unicode\"]\n\tpath = _build/cl-unicode\n\turl = https://github.com/edicl/cl-unicode\n\tshallow = true\n[submodule \"_build/named-readtables\"]\n\tpath = _build/named-readtables\n\turl = https://github.com/melisgl/named-readtables\n\tshallow = true\n[submodule \"_build/trivial-types\"]\n\tpath = _build/trivial-types\n\turl = https://github.com/m2ym/trivial-types\n\tshallow = true\n[submodule \"_build/quri\"]\n\tpath = _build/quri\n\turl = https://github.com/fukamachi/quri\n\tshallow = true\n[submodule \"_build/fast-io\"]\n\tpath = _build/fast-io\n\turl = https://github.com/rpav/fast-io\n\tshallow = true\n[submodule \"_build/static-vectors\"]\n\tpath = _build/static-vectors\n\turl = https://github.com/sionescu/static-vectors\n\tshallow = true\n[submodule \"_build/cffi\"]\n\tpath = _build/cffi\n\turl = https://github.com/cffi/cffi\n\tshallow = true\n[submodule \"_build/chunga\"]\n\tpath = _build/chunga\n\turl = https://github.com/edicl/chunga\n\tshallow = true\n[submodule \"_build/cl-cookie\"]\n\tpath = _build/cl-cookie\n\turl = https://github.com/fukamachi/cl-cookie\n\tshallow = true\n[submodule \"_build/local-time\"]\n\tpath = _build/local-time\n\turl = https://github.com/dlowe-net/local-time\n\tshallow = true\n[submodule \"_build/trivial-mimes\"]\n\tpath = _build/trivial-mimes\n\turl = https://github.com/Shinmera/trivial-mimes\n\tshallow = true\n[submodule \"_build/cl-base64\"]\n\tpath = _build/cl-base64\n\turl = https://gitlab.common-lisp.net/nyxt/cl-base64.git\n\tshallow = true\n[submodule \"_build/cl-plus-ssl\"]\n\tpath = _build/cl-plus-ssl\n\turl = https://github.com/cl-plus-ssl/cl-plus-ssl\n\tshallow = true\n[submodule \"_build/drakma\"]\n\tpath = _build/drakma\n\turl = https://github.com/edicl/drakma\n\tshallow = true\n[submodule \"_build/puri\"]\n\tpath = _build/puri\n\turl = https://gitlab.common-lisp.net/nyxt/puri.git\n\tshallow = true\n[submodule \"_build/cl-enchant\"]\n\tpath = _build/cl-enchant\n\turl = https://github.com/tlikonen/cl-enchant\n\tshallow = true\n[submodule \"_build/fset\"]\n\tpath = _build/fset\n\turl = https://github.com/slburson/fset\n\tshallow = true\n[submodule \"_build/misc-extensions\"]\n\tpath = _build/misc-extensions\n\turl = https://gitlab.common-lisp.net/misc-extensions/misc-extensions.git\n\tshallow = true\n[submodule \"_build/iolib\"]\n\tpath = _build/iolib\n\turl = https://github.com/sionescu/iolib\n\tshallow = true\n[submodule \"_build/idna\"]\n\tpath = _build/idna\n\turl = https://github.com/antifuchs/idna\n\tshallow = true\n[submodule \"_build/swap-bytes\"]\n\tpath = _build/swap-bytes\n\turl = https://github.com/sionescu/swap-bytes\n\tshallow = true\n[submodule \"_build/log4cl\"]\n\tpath = _build/log4cl\n\turl = https://github.com/sharplispers/log4cl\n\tshallow = true\n[submodule \"_build/moptilities\"]\n\tpath = _build/moptilities\n\turl = https://github.com/gwkkwg/moptilities/\n\tshallow = true\n[submodule \"_build/parenscript\"]\n\tpath = _build/parenscript\n\turl = https://gitlab.common-lisp.net/parenscript/parenscript\n\tshallow = true\n[submodule \"_build/plump\"]\n\tpath = _build/plump\n\turl = https://github.com/Shinmera/plump\n\tshallow = true\n[submodule \"_build/array-utils\"]\n\tpath = _build/array-utils\n\turl = https://github.com/Shinmera/array-utils\n\tshallow = true\n[submodule \"_build/documentation-utils\"]\n\tpath = _build/documentation-utils\n\turl = https://github.com/Shinmera/documentation-utils\n\tshallow = true\n[submodule \"_build/trivial-indent\"]\n\tpath = _build/trivial-indent\n\turl = https://github.com/Shinmera/trivial-indent\n\tshallow = true\n[submodule \"_build/serapeum\"]\n\tpath = _build/serapeum\n\turl = https://github.com/ruricolist/serapeum\n\tshallow = true\n[submodule \"_build/nhooks\"]\n\tpath = _build/nhooks\n\turl = https://github.com/atlas-engineer/nhooks\n\tshallow = true\n[submodule \"_build/trivia\"]\n\tpath = _build/trivia\n\turl = https://github.com/guicho271828/trivia\n\tshallow = true\n[submodule \"_build/optima\"]\n\tpath = _build/optima\n\turl = https://github.com/m2ym/optima\n\tshallow = true\n[submodule \"_build/lisp-namespace\"]\n\tpath = _build/lisp-namespace\n\turl = https://github.com/guicho271828/lisp-namespace\n\tshallow = true\n[submodule \"_build/trivial-cltl2\"]\n\tpath = _build/trivial-cltl2\n\turl = https://github.com/Zulu-Inuoe/trivial-cltl2\n\tshallow = true\n[submodule \"_build/type-i\"]\n\tpath = _build/type-i\n\turl = https://github.com/guicho271828/type-i\n\tshallow = true\n[submodule \"_build/introspect-environment\"]\n\tpath = _build/introspect-environment\n\turl = https://github.com/Bike/introspect-environment\n\tshallow = true\n[submodule \"_build/string-case\"]\n\tpath = _build/string-case\n\turl = https://github.com/pkhuong/string-case\n\tshallow = true\n[submodule \"_build/parse-number\"]\n\tpath = _build/parse-number\n\turl = https://github.com/sharplispers/parse-number/\n\tshallow = true\n[submodule \"_build/parse-declarations\"]\n\tpath = _build/parse-declarations\n\turl = https://gitlab.common-lisp.net/parse-declarations/parse-declarations.git\n\tshallow = true\n[submodule \"_build/global-vars\"]\n\tpath = _build/global-vars\n\turl = https://github.com/lmj/global-vars\n\tshallow = true\n[submodule \"_build/trivial-file-size\"]\n\tpath = _build/trivial-file-size\n\turl = https://github.com/ruricolist/trivial-file-size\n\tshallow = true\n[submodule \"_build/trivial-macroexpand-all\"]\n\tpath = _build/trivial-macroexpand-all\n\turl = https://github.com/cbaggers/trivial-macroexpand-all\n\tshallow = true\n[submodule \"_build/cl-str\"]\n\tpath = _build/cl-str\n\turl = https://github.com/vindarel/cl-str\n\tshallow = true\n[submodule \"_build/cl-change-case\"]\n\tpath = _build/cl-change-case\n\turl = https://github.com/rudolfochrist/cl-change-case\n\tshallow = true\n[submodule \"_build/trivial-clipboard\"]\n\tpath = _build/trivial-clipboard\n\turl = https://github.com/snmsts/trivial-clipboard\n\tshallow = true\n[submodule \"_build/trivial-package-local-nicknames\"]\n\tpath = _build/trivial-package-local-nicknames\n\turl = https://github.com/phoe/trivial-package-local-nicknames\n\tshallow = true\n[submodule \"_build/unix-opts\"]\n\tpath = _build/unix-opts\n\turl = https://github.com/atlas-engineer/unix-opts\n\tshallow = true\n[submodule \"_build/cl-webkit\"]\n\tpath = _build/cl-webkit\n\turl = https://github.com/joachifm/cl-webkit\n\tshallow = true\n[submodule \"_build/cl-gobject-introspection\"]\n\tpath = _build/cl-gobject-introspection\n\turl = https://github.com/andy128k/cl-gobject-introspection\n\tshallow = true\n[submodule \"_build/lparallel\"]\n\tpath = _build/lparallel\n\turl = https://github.com/lmj/lparallel/\n\tshallow = true\n[submodule \"_build/jpl-queues\"]\n\tpath = _build/jpl-queues\n\turl = https://gitlab.common-lisp.net/nyxt/jpl-queues.git\n\tshallow = true\n[submodule \"_build/mt19937\"]\n\tpath = _build/mt19937\n\turl = https://gitlab.common-lisp.net/nyxt/mt19937\n\tshallow = true\n[submodule \"_build/s-xml\"]\n\tpath = _build/s-xml\n\turl = https://gitlab.common-lisp.net/s-xml/s-xml\n\tshallow = true\n[submodule \"_build/cl-utilities\"]\n\tpath = _build/cl-utilities\n\turl = https://gitlab.common-lisp.net/cl-utilities/cl-utilities\n\tshallow = true\n[submodule \"_build/cl-qrencode\"]\n\tpath = _build/cl-qrencode\n\turl = https://github.com/jnjcc/cl-qrencode\n\tshallow = true\n[submodule \"_build/clss\"]\n\tpath = _build/clss\n\turl = https://github.com/Shinmera/clss\n\tshallow = true\n[submodule \"_build/spinneret\"]\n\tpath = _build/spinneret\n\turl = https://github.com/ruricolist/spinneret/\n\tshallow = true\n[submodule \"_build/salza2\"]\n\tpath = _build/salza2\n\turl = https://github.com/xach/salza2\n\tshallow = true\n[submodule \"_build/zpng\"]\n\tpath = _build/zpng\n\turl = https://github.com/xach/zpng\n\tshallow = true\n[submodule \"_build/iterate\"]\n\tpath = _build/iterate\n\turl = https://gitlab.common-lisp.net/iterate/iterate.git\n\tshallow = true\n[submodule \"_build/cl-gopher\"]\n\tpath = _build/cl-gopher\n\turl = https://github.com/knusbaum/cl-gopher\n\tshallow = true\n[submodule \"_build/phos\"]\n\tpath = _build/phos\n\turl = https://github.com/omar-polo/phos\n\tshallow = true\n[submodule \"_build/cl-tld\"]\n\tpath = _build/cl-tld\n\turl = https://github.com/lu4nx/cl-tld\n\tshallow = true\n[submodule \"_build/nfiles\"]\n\tpath = _build/nfiles\n\turl = https://github.com/atlas-engineer/nfiles\n\tshallow = true\n[submodule \"_build/nkeymaps\"]\n\tpath = _build/nkeymaps\n\turl = https://github.com/atlas-engineer/nkeymaps\n\tshallow = true\n[submodule \"_build/py-configparser\"]\n\tpath = _build/py-configparser\n\turl = https://gitlab.common-lisp.net/nyxt/py-configparser\n\tshallow = true\n[submodule \"_build/trivial-custom-debugger\"]\n\tpath = _build/trivial-custom-debugger\n\turl = https://github.com/phoe/trivial-custom-debugger\n\tshallow = true\n[submodule \"_build/lisp-unit2\"]\n\tpath = _build/lisp-unit2\n\turl = https://github.com/AccelerationNet/lisp-unit2\n\tshallow = true\n[submodule \"_build/nsymbols\"]\n\tpath = _build/nsymbols\n\turl = https://github.com/atlas-engineer/nsymbols\n\tshallow = true\n[submodule \"_build/LASS\"]\n\tpath = _build/LASS\n\turl = https://github.com/Shinmera/LASS\n\tshallow = true\n[submodule \"_build/njson\"]\n\tpath = _build/njson\n\turl = https://github.com/atlas-engineer/njson\n\tshallow = true\n[submodule \"_build/nclasses\"]\n\tpath = _build/nclasses\n\turl = https://github.com/atlas-engineer/nclasses/\n\tshallow = true\n[submodule \"_build/prompter\"]\n\tpath = _build/prompter\n\turl = https://github.com/atlas-engineer/prompter\n\tshallow = true\n[submodule \"_build/chipz\"]\n\tpath = _build/chipz\n\turl = https://github.com/sharplispers/chipz\n\tshallow = true\n[submodule \"_build/cl-cffi-gtk\"]\n\tpath = _build/cl-cffi-gtk\n\turl = https://github.com/sharplispers/cl-cffi-gtk\n\tshallow = true\n[submodule \"_build/cl-json\"]\n\tpath = _build/cl-json\n\turl = https://github.com/sharplispers/cl-json\n\tshallow = true\n[submodule \"_build/cl-sqlite\"]\n\tpath = _build/cl-sqlite\n\turl = https://github.com/TeMPOraL/cl-sqlite\n\tshallow = true\n[submodule \"_build/cl-electron\"]\n\tpath = _build/cl-electron\n\turl = https://github.com/atlas-engineer/cl-electron/\n\tshallow = true\n[submodule \"_build/cl-colors-ng\"]\n\tpath = _build/cl-colors-ng\n\turl = https://codeberg.org/cage/cl-colors-ng.git\n\tshallow = true\n[submodule \"_build/cl-interpol\"]\n\tpath = _build/cl-interpol\n\turl = https://github.com/edicl/cl-interpol\n\tshallow = true\n[submodule \"_build/symbol-munger\"]\n\tpath = _build/symbol-munger\n\turl = https://github.com/AccelerationNet/symbol-munger\n\tshallow = true\n[submodule \"_build/in-nomine\"]\n\tpath = _build/in-nomine\n\turl = https://github.com/phoe/in-nomine\n\tshallow = true\n[submodule \"_build/trivial-arguments\"]\n\tpath = _build/trivial-arguments\n\turl = https://github.com/Shinmera/trivial-arguments.git\n\tshallow = true\n"
  },
  {
    "path": "INSTALL",
    "content": "Usage:\n\n    make all                 # Generate Nyxt binary at $PWD.\n    make install             # Install Nyxt.\n    make doc                 # Generate Nyxt static documentation.\n\nDESTDIR and PREFIX set the target destination.  Both must be absolute paths.\nWhen unbound, DESTDIR is set to / and PREFIX is set to $DESTDIR/usr/local/.\n\nNASDF_SOURCE_PATH sets where the source files will be installed.  When unbound,\nit is set to $PREFIX/share/.\n\nWhen NYXT_SUBMODULES is \"true\" (the default), all Lisp dependencies are searched\nat ./_build.  Otherwise, they need to be made visible to ASDF by other means.\nIn case you have received an archive that includes the source of these Lisp\ndependencies, then it all should work out of the box.\n\nNYXT_RENDERER sets the renderer, by default \"electron\".\n\nNYXT_VERSION forces the version number, in the rare eventuality that it can't be\nfetched in another way.\n\nThe static documentation is particularly useful when it can't be consulted from\nNyxt itself (where it is dynamically generated at runtime).\n"
  },
  {
    "path": "README.org",
    "content": "* Nyxt browser\n#+html: <img src=\"https://nyxt-browser.com/static/image/nyxt_256x256.png\" align=\"right\"/>\n\n*Nyxt* [nýkst] is a keyboard-driven web browser designed for hackers.  Inspired by\nEmacs and Vim, it has familiar keybindings ([[https://en.wikipedia.org/wiki/Emacs][Emacs]], [[https://en.wikipedia.org/wiki/Vim_(text_editor)][vi]], [[https://en.wikipedia.org/wiki/IBM_Common_User_Access][CUA]]), and is infinitely\nextensible in Lisp.\n\n*Attention:* Nyxt is under active development.  Please feel free to [[https://github.com/atlas-engineer/nyxt/issues][report]] bugs,\ninstabilities or feature wishes.\n\n-----\n\n* Features\nFor an exhaustive description of all of the features, please refer to the\nmanual.\n\n** Fast tab switching\n\nSwitch easily between your open tabs via fuzzy search.  If you are looking for\n~https://www.example.com~, you could type in ~ele~, ~exa~, ~epl~, or any other\nmatching series of letters.\n\n#+html: <img src=\"https://nyxt-browser.com/static/image/switch-buffer.png\" align=\"center\"/>\n\n** Multiple marking\n\nCommands can accept multiple inputs, allowing you to quickly perform an\noperation against multiple objects.  In the example below we simultaneously open\nseveral bookmarks.\n\n#+html: <img src=\"https://nyxt-browser.com/static/image/multi-select.png\" align=\"center\"/>\n\n** Powerful bookmarks\n\nBookmark a page with tags.  Search bookmarks with compound queries.  Capture\nmore data about your bookmarks, and group and wrangle them in any way you like.\n\n#+html: <img src=\"https://nyxt-browser.com/static/image/bookmark.png\" align=\"center\"/>\n\n** Multi tab search\n\nSearch multiple tabs at the same time, and view all the results in a single\nwindow.  Jump quickly through your open tabs to find what you need.\n\n#+html: <img src=\"https://nyxt-browser.com/static/image/multi-search.png\" align=\"center\"/>\n\n** History as a tree\n\nHistory is represented as a tree that you can traverse.  Smarter than the\n\"forwards-backwards\" abstraction found in other browsers, the tree makes sure\nyou never lose track of where you've been.\n\n#+html: <img src=\"https://nyxt-browser.com/static/image/history.png\" align=\"center\"/>\n\n* Installation\n\nSupported platforms:\n\n- GNU/Linux\n- macOS (in development)\n- Windows (in development)\n- FreeBSD (unofficial)\n\n** GNU/Linux\n\nThe Nyxt team maintains the following distribution means:\n\n- [[https://flathub.org/apps/engineer.atlas.Nyxt][Nyxt on Flathub]]\n\n[[https://repology.org/project/nyxt/versions][Non-official distribution means are supported by the community as well]].  We're\nnot accountable for their quality, so we kindly ask to report issues to the\nmaintainers of those packaging efforts.\n\n* Contributing\n\nPlease refer to the [[file:developer-manual.org][developer's documentation]].\n"
  },
  {
    "path": "_build/README.org",
    "content": "This directory contains all Common Lisp dependencies.  They are fetched via Git\nsubmodules.  This gives us good reproducibility and control, unlike with\nQuicklisp which might not have the right versions.\n\nWe store these in a directory that's excluded from recursion by ASDF.\nas per =asdf/source-registry:*source-registry-exclusions*=.\nThis way we won't conflict with user or system libraries.\n"
  },
  {
    "path": "assets/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n  <dict>\n    <key>NSPrincipalClass</key>\n    <string>Nyxt</string>\n    <key>CFBundleIconFile</key>\n    <string>nyxt.icns</string>\n    <key>CFBundlePackageType</key>\n    <string>APPL</string>\n    <key>CFBundleGetInfoString</key>\n    <string>Nyxt</string>\n    <key>CFBundleSignature</key>\n    <string>????</string>\n    <key>CFBundleExecutable</key>\n    <string>nyxt</string>\n    <key>CFBundleIdentifier</key>\n    <string>engineer.Atlas.Nyxt</string>\n    <key>NSAppTransportSecurity</key>\n    <dict>\n      <key>NSAllowsLocalNetworking</key>\n      <true/>\n    </dict>\n    <key>CFBundleURLTypes</key>\n    <array>\n      <dict>\n\t    <key>CFBundleURLName</key>\n\t    <string>http URL</string>\n\t    <key>CFBundleURLSchemes</key>\n\t    <array>\n\t      <string>http</string>\n\t    </array>\n      </dict>\n      <dict>\n\t    <key>CFBundleURLName</key>\n\t    <string>https URL</string>\n\t    <key>CFBundleURLSchemes</key>\n\t    <array>\n\t      <string>https</string>\n\t    </array>\n      </dict>\n      <dict>\n\t    <key>CFBundleURLName</key>\n\t    <string>gopher URL</string>\n\t    <key>CFBundleURLSchemes</key>\n\t    <array>\n\t      <string>gopher</string>\n\t    </array>\n      </dict>\n      <dict>\n\t    <key>CFBundleURLName</key>\n\t    <string>gemini URL</string>\n\t    <key>CFBundleURLSchemes</key>\n\t    <array>\n\t      <string>gemini</string>\n\t    </array>\n      </dict>\n    </array>\n    <key>CFBundleDocumentTypes</key>\n    <array>\n      <dict>\n\t    <key>CFBundleTypeExtensions</key>\n\t    <array>\n\t      <string>html</string>\n          <string>xhtml</string>\n\t    </array>\n      </dict>\n    </array>\n  </dict>\n</plist>\n"
  },
  {
    "path": "assets/nyxt.appimage.desktop",
    "content": "[Desktop Entry]\nName=Nyxt\nComment=Web Browser for Hackers\nGenericName=Web Browser\nKeywords=Internet;WWW;Browser;Web;Explorer\nExec=\"~a\"\nTerminal=false\nX-MultipleArgs=false\nType=Application\nIcon=nyxt\nCategories=Network;WebBrowser;\nMimeType=text/html;text/xml;application/xhtml+xml;application/xml;application/rss+xml;application/rdf+xml;image/gif;image/jpeg;image/png;x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/ftp;x-scheme-handler/chrome;video/webm;application/x-xpinstall;\nStartupNotify=true\nStartupWMClass=nyxt\n"
  },
  {
    "path": "assets/nyxt.desktop",
    "content": "[Desktop Entry]\nName=Nyxt\nComment=Web Browser for Hackers\nGenericName=Web Browser\nKeywords=Internet;WWW;Browser;Web;Explorer\nExec=nyxt %u\nTerminal=false\nX-MultipleArgs=false\nType=Application\nIcon=nyxt\nCategories=Network;WebBrowser;\nMimeType=text/html;text/xml;application/xhtml+xml;application/xml;application/rss+xml;application/rdf+xml;image/gif;image/jpeg;image/png;x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/ftp;x-scheme-handler/chrome;video/webm;application/x-xpinstall;\nStartupNotify=true\nStartupWMClass=nyxt\n"
  },
  {
    "path": "assets/nyxt.metainfo.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<component type=\"desktop-application\">\n  <!-- Spec: https://www.freedesktop.org/software/appstream/docs/chap-Metadata.html -->\n  <id>engineer.atlas.Nyxt</id>\n  <metadata_license>CC-BY-SA-3.0</metadata_license>\n  <project_license>BSD-3-Clause</project_license>\n  <content_rating type=\"oars-1.1\" />\n  <developer_name>Atlas Engineer</developer_name>\n  <name>Nyxt</name>\n  <summary>The hacker's browser</summary>\n  <description>\n    <p>Quickly analyze, navigate, and extract information from the\n    Internet. Nyxt is fully hackable — all of its source code can be\n    introspected, modified, and tweaked to your exact specification.</p>\n  </description>\n  <categories>\n    <category>Network</category>\n  </categories>\n  <url type=\"homepage\">https://nyxt-browser.com/</url>\n  <url type=\"vcs-browser\">https://github.com/atlas-engineer/nyxt</url>\n  <url type=\"bugtracker\">https://github.com/atlas-engineer/nyxt/issues</url>\n  <url type=\"faq\">https://nyxt-browser.com/faq</url>\n  <url type=\"help\">https://discourse.atlas.engineer/</url>\n  <url type=\"donation\">https://nyxt-browser.com/purchase</url>\n  <!-- <url type=\"translate\"></url> -->\n  <url type=\"contact\">https://atlas.engineer/contact</url>\n  <url type=\"contribute\">https://github.com/atlas-engineer/nyxt/blob/master/developer-manual.org</url>\n  <launchable type=\"desktop-id\">nyxt.desktop</launchable>\n  <provides>\n    <binary>nyxt</binary>\n  </provides>\n  <screenshots>\n    <screenshot type=\"default\">\n      <image type=\"source\" width=\"1108\" height=\"881\">https://nyxt-browser.com/static/image/switch-buffer.png</image>\n      <caption>Fast tab switching</caption>\n    </screenshot>\n    <screenshot>\n      <image type=\"source\" width=\"1108\" height=\"881\">https://nyxt-browser.com/static/image/multi-select.png</image>\n      <caption>Multiple marking</caption>\n    </screenshot>\n    <screenshot>\n      <image type=\"source\" width=\"1108\" height=\"881\">https://nyxt-browser.com/static/image/bookmark.png</image>\n      <caption>Powerful bookmarks</caption>\n    </screenshot>\n    <screenshot>\n      <image type=\"source\" width=\"1108\" height=\"881\">https://nyxt-browser.com/static/image/multi-search.png</image>\n      <caption>Multi tab search</caption>\n    </screenshot>\n    <screenshot>\n      <image type=\"source\" width=\"1108\" height=\"881\">https://nyxt-browser.com/static/image/history.png</image>\n      <caption>History as a tree</caption>\n    </screenshot>\n</screenshots>\n  <!-- Only those published on Flathub are listed -->\n  <releases>\n    <release version=\"3.11.8\" date=\"2024-08-01\">\n      <url>https://nyxt-browser.com/article/release-3.11.8.org</url>\n    </release>\n    <release version=\"3.11.7\" date=\"2024-05-31\">\n      <url>https://nyxt-browser.com/article/release-3.11.7.org</url>\n    </release>\n    <release version=\"3.11.6\" date=\"2024-04-08\">\n      <url>https://nyxt-browser.com/article/release-3.11.6.org</url>\n    </release>\n    <release version=\"3.11.5\" date=\"2024-03-18\">\n      <url>https://nyxt-browser.com/article/release-3.11.5.org</url>\n    </release>\n    <release version=\"3.11.4\" date=\"2024-03-04\">\n      <url>https://nyxt-browser.com/article/release-3.11.4.org</url>\n    </release>\n    <release version=\"3.11.3\" date=\"2024-02-20\">\n      <url>https://nyxt-browser.com/article/release-3.11.3.org</url>\n    </release>\n    <release version=\"3.11.2\" date=\"2024-02-05\">\n      <url>https://nyxt-browser.com/article/release-3.11.2.org</url>\n    </release>\n    <release version=\"3.11.1\" date=\"2024-01-22\">\n      <url>https://nyxt-browser.com/article/release-3.11.1.org</url>\n    </release>\n    <release version=\"3.11.0\" date=\"2024-01-08\">\n      <url>https://nyxt-browser.com/article/release-3.11.0.org</url>\n    </release>\n    <release version=\"3.10.0\" date=\"2023-12-11\">\n      <url>https://nyxt-browser.com/article/release-3.10.0.org</url>\n    </release>\n    <release version=\"3.9.2\" date=\"2023-11-13\">\n      <url>https://nyxt-browser.com/article/release-3.9.2.org</url>\n    </release>\n    <release version=\"3.9.1\" date=\"2023-10-23\">\n      <url>https://nyxt-browser.com/article/release-3.9.1.org</url>\n    </release>\n    <release version=\"3.9.0\" date=\"2023-10-09\">\n      <url>https://nyxt-browser.com/article/release-3.9.0.org</url>\n    </release>\n    <release version=\"3.8.0\" date=\"2023-09-25\">\n      <url>https://nyxt-browser.com/article/release-3.8.0.org</url>\n    </release>\n    <release version=\"3.7.0\" date=\"2023-09-11\">\n      <url>https://nyxt-browser.com/article/release-3.7.0.org</url>\n    </release>\n    <release version=\"3.6.1\" date=\"2023-08-28\">\n      <url>https://nyxt-browser.com/article/release-3.6.1.org</url>\n    </release>\n  </releases>\n</component>\n"
  },
  {
    "path": "developer-manual.org",
    "content": "#+TITLE: Nyxt Developer's Manual\n\n# Install org-make-toc so the TOC below will be automatically generated.\n# https://github.com/alphapapa/org-make-toc\n* Table of contents                                                     :TOC:\n:PROPERTIES:\n:TOC:      :include all :ignore this\n:END:\n:CONTENTS:\n- [[#bill-of-materials][Bill of Materials]]\n  - [[#source][Source]]\n  - [[#common-lisp][Common Lisp]]\n  - [[#web-renderers][Web renderers]]\n    - [[#webkitgtk][WebKitGTK]]\n    - [[#electron][Electron]]\n  - [[#other][Other]]\n- [[#development-environment][Development environment]]\n  - [[#tests][Tests]]\n- [[#installation][Installation]]\n- [[#contributing][Contributing]]\n  - [[#help][Help]]\n  - [[#commit-style][Commit style]]\n  - [[#branch-management][Branch management]]\n  - [[#programming-conventions][Programming conventions]]\n:END:\n\n* Bill of Materials\n** Source\n\nEither get a tarball (=nyxt-<version>-source-with-submodules.tar.xz=) from a\n[[https://github.com/atlas-engineer/nyxt/releases][tagged release]], or clone as a git repository:\n\n#+begin_src sh\nmkdir -p ~/common-lisp\ngit clone --recurse-submodules https://github.com/atlas-engineer/nyxt ~/common-lisp/nyxt\n#+end_src\n\n** Common Lisp\n\nNyxt is written in Common Lisp.  Currently, we only target one of its\nimplementations - [[http://www.sbcl.org/][SBCL]].\n\nNyxt also depends on Common Lisp libraries.  These are bundled in the tarball\nmentioned above or fetched as Git submodules (under =./_build=).\n\nNote for advanced users: the single source of truth for CL libraries is dictated\nby the Git submodules.  Any Nyxt build that deviates from it is considered\nunofficial.  See environment variable =NYXT_SUBMODULES= defined in the makefile\nto override the default behavior.\n\n** Web renderers\n\nNyxt is designed to be web engine agnostic so its dependencies vary.\n\n*** WebKitGTK\n\nUsing the latest [[https://webkitgtk.org][WebKitGTK]] version is advised for security concerns.  The oldest\nversion that supports all features is 2.36.\n\nThe packages that provide the following shared objects are required:\n\n- libwebkit2gtk-4.1.so\n- libgobject-2.0.so\n- libgirepository-1.0.so\n- libglib-2.0.so\n- libgthread-2.0.so\n- libgio-2.0.so\n- libcairo.so\n- libpango-1.0.so\n- libpangocairo-1.0.so\n- libgdk_pixbuf-2.0.so\n- libgdk-3.so\n- libgtk-3.so\n\nTo improve media stream it is recommended to install =gst-libav= and the\nfollowing plugins:\n\n- gst-plugins-bad\n- gst-plugins-base\n- gst-plugins-good\n- gst-plugins-ugly\n\n*** Electron\n\nExperimental support for [[https://www.electronjs.org/][Electron]].  Further documentation soon.\n\n** Other\n\nThe packages that provide the following shared objects are required:\n\n- libssl.so.3\n- libcrypto.so.3\n- libfixposix.so.3\n- libsqlite3.so\n\nAdditionally, the following packages:\n\n- xclip :: when using X system;\n- wl-clipboard :: when using Wayland;\n- enchant :: spellchecking (optional).\n\n* Development environment\n\nLisp favors incremental program development meaning that you make some changes\nand compile them.  In other words, there's no need to compile the whole codebase\nor even restart the program.\n\nThe typical Common Lisp IDE is [[https://github.com/slime/slime][SLIME]] (or its fork [[https://github.com/joaotavora/sly][SLY]]), which requires being\ncomfortable with Emacs.  Add the snippet below to Emacs' init file.\n\n#+begin_src emacs-lisp\n(setq slime-lisp-implementations\n      '((nyxt (\"sbcl\" \"--dynamic-space-size 3072\")\n              :env (\"CL_SOURCE_REGISTRY=~/common-lisp//:~/common-lisp/nyxt/_build//\"))))\n#+end_src\n\nStart the REPL by issuing =M-- M-x sly RET nyxt RET= and evaluate:\n\n#+begin_src lisp\n(asdf:load-system :nyxt/gi-gtk)\n(nyxt:start)\n#+end_src\n\nNote that:\n\n- [[https://asdf.common-lisp.dev/asdf/Configuring-ASDF-to-find-your-systems.html][ASDF must be configured to find the required systems]];\n- =cffi= must be configured to find the required shared objects by setting env\n  var =LD_LIBRARY_PATH= or =cffi:*foreign-library-directories*=.\n\n** Tests\n\nIt is recommended to restart the Lisp image before and after running the tests\nsince some of them are stateful:\n\n#+begin_src lisp\n(asdf:test-system :nyxt/gi-gtk)\n#+end_src\n\n* Installation\n\nNyxt uses the =Make= build system.  Run =make= to display the documentation or\nsee the [[../makefile][Makefile]] for more details.\n\n* Contributing\n\nNyxt is a joint effort and we welcome contributors!  You can find tasks [[https://github.com/atlas-engineer/nyxt/issues?q=is%3Aissue+is%3Aopen+label%3Agood-first-issue][on our\nissue tracker]] to suit your interests and skills.  Please fork the project and\nopen a pull request (PR) on GitHub to undergo the reviewing process.  Refer to\nthe [[*Branch management][branch management section]] for more detailed information.\n\nPlease resist the temptation of discussing changes without drafting its\nimplementation.  Currently, we value pragmatism over creativity.\n\n** Help\n\nFeel free to contact us at any point if you need guidance.\n\n- To learn Common Lisp, see [[https://nyxt-browser.com/learn-lisp]];\n- [[https://github.com/atlas-engineer/nyxt/issues][Open up an issue on GitHub]];\n- Find Nyxt on Libera IRC: =#nyxt=\n- [[https://discord.gg/YXCk7gDKgJ][Nyxt's Discord]];\n\n** Commit style\n\nEnsure to isolate commits containing whitespace changes (including indentation)\nor code movements as to avoid noise in the diffs.\n\nRegarding commit messages, we follow the convention of prefixing the title with\nthe basename when there's a single modified file.  For instance, for changes in\n=source/mode/blocker.lisp= the commit message would look as per below:\n\n#+begin_example\nmode/blocker: Short description of the change\n\nFurther explanation.\n#+end_example\n\n** Branch management\n\nNyxt uses the following branches:\n\n- =master= for development;\n- =<feature-branches>= for working on particular features;\n- =<integer>-series= to backport commits corresponding to specific major\n  versions.\n\nBranch off from the target branch and rebase onto it right before merging as to\navoid merge conflicts.\n\nA commit is said to be atomic when it builds and starts Nyxt successfully.  At\ntimes, for the sake of readability, it is wise to break the changes down to\nsmaller non-atomic commits.  In that case, a merge commit is required (use merge\noption =no-ff=).  This guarantees that running =git bisect= with option\n=--first-parent= only picks atomic commits, which streamlines the process.\n\nThose with commit access may push trivial changes directly to the target branch.\n\n** Programming conventions\n\nThe usual style guides by [[https://www.cs.umd.edu/~nau/cmsc421/norvig-lisp-style.pdf][Norvig & Pitman's Tutorial on Good Lisp Programming\nStyle]] and [[https://google.github.io/styleguide/lispguide.xml][Google Common Lisp Style Guide]] are advised.\n\nFor symbol naming conventions, see https://www.cliki.net/Naming+conventions.\n\nSome of our conventions include:\n\n- Prefer =first= and =rest= over =car= and =cdr=, respectively.\n- Use =define-class= instead of =defclass=.\n- Use =nyxt:define-package= for Nyxt-related pacakges.  Notice that it features\n  default imports (e.g. =export-always=) and package nicknames (e.g. =alex=,\n  =sera=, etc.).  Prefer =uiop:define-package= for general purpose packages.\n- Export using =export-always= next to the symbol definition.  This helps\n  prevent exports to go out-of-sync, or catch typos.  Unlike =export=,\n  =export-always= saves you from surprises upon recompilation.\n- When sensible, declaim the function types using =->=.  Note that there is then\n  no need to mention the type of the arguments and the return value in the\n  docstring.\n- Use the =maybe= and =maybe*= types instead of =(or null ...)= and =(or null\n  (array * (0)) ...)=, respectively.\n- Use the =list-of= type for typed lists.\n- Use =funcall*= to not error when function does not exist.\n- Prefer classes over structs.\n- Classes should be usable with just a =make-instance=.\n- Slots classes should be formatted in the following way:\n#+begin_src lisp\n(slot-name\n slot-value\n ...\n :documentation \"Foo.\")\n#+end_src\n\nWhen =slot-value= is the only parameter specified then:\n#+begin_src lisp\n(slot-name slot-value)\n#+end_src\n- =customize-instance= is reserved for end users.  Use\n  =initialize-instance :after= or =slot-unbound= to initialize the slots.\n  Set up the rest of the class in =customize-instance :after=.  Bear in mind\n  that anything in this last method won't be customizable for the end user.\n- Almost all files should be handled via the =nfiles= library.\n- =(setf SLOT-WRITER) :after= is reserved for \"watchers\",\n  i.e. handlers that are run whenever the slot is set.  The =:around= method is\n  not used by watchers, and thus the watcher may be overridden.\n- We use the =%foo%= naming convention for special local variables.\n- We suffix predicates with =-p=.  Unlike the usual convention, we always use a\n  dash (i.e. =foo-p= over =foop=).\n- Prefer the term =url= over =uri=.\n- URLs should be of type =quri:uri=.  If you need to manipulate a URL string, call\n  it =url-string=. In case the value contains a URL, but is not =quri:url=, use\n  =url-designator= and its =url= method to normalize into =quri:uri=.\n- Paths should be of type =cl:pathname=.\n  Use =uiop:native-namestring= to \"send\" to OS-facing functions,\n  =uiop:ensure-pathname= to \"receive\" from OS-facing functions or to \"trunamize\".\n- Prefer =handler-bind= over =handler-case=: when running from the REPL, this\n  triggers the debugger with a full stacktrace; when running the Nyxt binary,\n  all conditions are caught anyway.\n- Do not handle the =T= condition, this may break everything.  Handle =error=,\n  =serious-condition=, or exceptionally =condition= (for instance if you do not\n  control the called code, and some libraries subclass =condition= instead of\n  =error=).\n- Dummy variables are called =_=.\n- Prefer American spelling.\n- Construct =define-command= requires a short one-line docstring without\n  newlines.\n- Name keyword function parameters as follows =&key (var default-value\n  var-supplied-p)=.\n\n# - Conversion functions =FROM->TO= or =->TO= for generic functions.  The\n#   only one that comes to mind is =url= which does not follow this convention...\n\n# - Blocking function should be prefixed with =wait-on-=.\n\n# Local Variables:\n# eval: (add-hook 'before-save-hook\n#                 (lambda nil (if (fboundp 'org-make-toc)\n#                                 (org-make-toc)\n#                                 (message-box \"Please install org-make-toc.\")))\n#                 nil\n#                 t)\n# End:\n"
  },
  {
    "path": "libraries/analysis/README.org",
    "content": "* Analysis\nAnalysis is a library that provides facilities to help analyze and\nunderstand data. Listed below are the classes:\n\n** Document\nThe document class represents a document. After creating a document,\nyou can perform several operations on it, some examples:\n\n+ term count: how many times does a term appear in a document?\n+ term frequency: how many times does a term appear divided by the\n  total number of words in the document?\n\n** Document Collection\nThe document collection class represents a collection of documents. As\nwith a document, there are several operations available, some examples:\n\n+ dictionary: which words appear in the document collection?\n+ keywords: what are the important keywords in this document\n  collection?\n\n** Document Vertex\nThe document vertex class represents a document that is part of a\ngraph. The edges slot of the document vertex class is used to store\nedges of that particular vertex. The keys in the edges slot hash table\nare the actual vertexes, and the values are the edge weights.\n\n** Document Cluster\nThe document cluster class represents a document that is part of a\ngraph which will be clustered. It extends the document-vertex class\nand adds support for a cluster tag and a list of neighbors. These\nslots are useful for clustering algorithms.\n"
  },
  {
    "path": "libraries/analysis/analysis.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :analysis)\n\n(export-always 'document)\n(defclass document ()\n  ((source :accessor source :initarg :source\n           :documentation \"The source object for the document.\")\n   (string-contents :initarg :string-contents :accessor string-contents)\n   (term-count-table :initform (make-hash-table :test #'equal)\n                     :documentation \"Contains a mapping of term ->\namount of times word appears in the document.\")\n   (vector-data :accessor vector-data\n                :documentation \"Vector representation of the document.\")\n   (rank :accessor rank :documentation \"Rank used for sorting.\")\n   (tokens :accessor tokens)\n   (token-count :accessor token-count))\n  (:documentation \"The document class represents a document. After\ncreating a document, you can perform several operations on it, some\nexamples:\n\n+ term count: how many times does a term appear in a document?\n+ term frequency: how many times does a term appear divided by the\n  total number of words in the document?\"))\n\n(defclass document-collection ()\n  ((documents :initform () :initarg :documents :accessor documents))\n  (:documentation \"The document collection class represents a\ncollection of documents. As with a document, there are several\noperations available, some examples:\n\n+ dictionary: which words appear in the document collection?\n+ keywords: what are the important keywords in this document\n  collection?\"))\n\n(defmethod initialize-instance :after ((document document) &key)\n  (setf (tokens document) (word-tokenize (string-contents document)))\n  (setf (token-count document) (length (tokens document)))\n  (loop for token in (tokens document) do\n    (incf (gethash token (slot-value document 'term-count-table) 0))))\n\n(defmethod term-count ((document document) term)\n  (gethash term (slot-value document 'term-count-table) 0))\n\n(defmethod term-frequency ((document document) term)\n  \"How often does the word exist in the document?\"\n  (/ (term-count document term)\n     ;; prevent division by zero for malformed documents\n     (max 1 (token-count document))))\n\n(defmethod termp ((document document) term)\n  \"Does the term exist in the document?\"\n  (> (term-count document term) 0))\n\n(defmethod add-document ((document-collection document-collection) document)\n  \"Add a document to the document collection.\"\n  (push document (documents document-collection)))\n\n(defun match-term (term)\n  (lambda (document)\n    (termp document term)))\n\n(defmethod document-frequency ((document-collection document-collection) term)\n  (/ (count-if (match-term term) (documents document-collection))\n     (length (documents document-collection))))\n\n(defmethod inverse-document-frequency ((document-collection document-collection) term)\n  (log (/ (length (documents document-collection))\n          (count-if (match-term term) (documents document-collection)))))\n\n(defmethod term-frequency-inverse-document-frequency ((document document)\n                                                      (document-collection document-collection)\n                                                      term)\n  (* (term-frequency document term) (inverse-document-frequency document-collection term)))\n\n(defmethod dictionary ((document document))\n  \"Return a list of all of the words that appear in a document.\"\n  (loop for key being the hash-keys of (slot-value document 'term-count-table)\n        collect key))\n\n(defmethod dictionary ((document-collection document-collection))\n  \"Return a list of all of the words that appear in a document collection.\"\n  (let ((words (list)))\n    (loop for document in (documents document-collection)\n          do (alexandria:appendf words (tokens document)))\n    (remove-duplicates words :test #'equalp)))\n\n(export-always 'keywords)\n(defmethod keywords ((document document) &optional document-collection)\n  (if document-collection\n      (sort (loop for word in (dictionary document)\n                  collect (cons word (term-frequency-inverse-document-frequency\n                                      document document-collection word)))\n            #'>\n            :key #'rest)\n      (sort (loop for word in (dictionary document)\n                  collect (cons word (term-frequency document word)))\n            #'>\n            :key #'rest)))\n\n(export-always 'extract-keywords)\n(defun extract-keywords (text &key (limit 5))\n  \"Extract keywords from a string of text.\"\n  (serapeum:take limit (keywords (make-instance 'analysis:document\n                                                :string-contents text))))\n"
  },
  {
    "path": "libraries/analysis/composite-sequence.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n;; Given the following sequence:\n;; 1 2 3 4 5\n;;\n;; We would record the following sequences/outcomes.\n;;\n;; Chain length of 1:\n;; 1 -> 2\n;; 2 -> 3\n;; 3 -> 4\n;; 4 -> 5\n;;\n;; Chain length of 2:\n;; 1 2 -> 3\n;; 2 3 -> 4\n;; 3 4 -> 5\n;;\n;; Chain length of 3:\n;; 1 2 3 -> 4\n;; 2 3 4 -> 5\n;;\n;; As can be seen above, the amount of subsequences within a given sequence is\n;; equal to (- (length sequence) chain length).\n\n(in-package :analysis)\n\n(export-always 'sequence-model)\n(defclass sequence-model (node)\n  ()\n  (:documentation \"The sequence-model class represents the root of a directed\n  graph. The edges represent possible sequences of events. It may help to\n  envision the graph as a finite state machine.\"))\n\n(defclass node ()\n  ((edges :accessor edges :initform (make-hash-table :test #'equal))))\n\n(export-always 'element)\n(defclass element-node (node)\n  ((element :accessor element :initarg :element)\n   (occurrences\n    :accessor occurrences\n    :initform 0\n    :documentation \"Number of times this element has appeared at the end of a\n    sequence.\")))\n\n(defmethod add-edge ((from-node node) (to-node node))\n  (alexandria:ensure-gethash (element to-node)\n                             (edges from-node) to-node))\n\n(defmethod list-edge-elements ((node node))\n  (mapcar #'element (alexandria:hash-table-values (edges node))))\n\n(defmethod increment ((node node))\n  (incf (occurrences node)))\n\n(export-always 'add-record)\n(defmethod add-record ((model sequence-model) sequence)\n  (multiple-value-bind (list-but-last-element last-element) (serapeum:halves sequence)\n    (let ((leaf (alexandria:ensure-gethash list-but-last-element\n                                           (edges model)\n                                           (make-instance 'node))))\n      (increment (add-edge leaf (make-instance 'element-node :element (first last-element)))))))\n\n(defmethod add-record-subsequence ((model sequence-model) sequence)\n  \"Add a record for all subsequences. E.g. transform '(3 2 1)' into:\n'(3 2 1), '(2 1), '(1)\"\n  (loop while (> (length sequence) 1)\n        collect (add-record model sequence)\n        do (setf sequence (rest sequence))))\n\n(export-always 'predict)\n(defmethod predict ((model sequence-model) sequence)\n  (serapeum:and-let* ((leaf (gethash sequence (edges model)))\n                      (edges (alexandria:hash-table-values (edges leaf))))\n    (first (sort edges #'> :key #'occurrences))))\n\n(defmethod predict-subsequence-simple ((model sequence-model) sequence)\n  \"Predict a sequence's next value based on all subsequence predictions. This is\na naive implementation which simply considers the amount of occurences without\nregard to the weight of different chain lengths.\"\n  (let* ((subsequence-results\n           (loop while (> (length sequence) 1)\n                 collect (let* ((leaf (gethash sequence (edges model)))\n                                (edges (alexandria:hash-table-values (edges leaf))))\n                           (first (sort edges #'> :key #'occurrences)))\n                 do (setf sequence (rest sequence)))))\n    (first (sort subsequence-results #'> :key #'occurrences))))\n"
  },
  {
    "path": "libraries/analysis/data.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :analysis)\n\n(defclass language-data ()\n  ((stop-words\n    :initarg :stop-words\n    :accessor stop-words\n    :initform\n    (list \"a\" \"able\" \"about\" \"above\" \"according\" \"accordingly\" \"across\" \"actually\" \"after\"\n          \"afterwards\" \"again\" \"against\" \"ain't\" \"all\" \"allow\" \"allows\" \"almost\" \"alone\"\n          \"along\" \"already\" \"also\" \"although\" \"always\" \"am\" \"among\" \"amongst\" \"an\" \"and\"\n          \"another\" \"any\" \"anybody\" \"anyhow\" \"anyone\" \"anything\" \"anyway\" \"anyways\" \"anywhere\"\n          \"apart\" \"appear\" \"appreciate\" \"appropriate\" \"are\" \"aren't\" \"around\" \"as\" \"a's\" \"aside\"\n          \"ask\" \"asking\" \"associated\" \"at\" \"available\" \"away\" \"awfully\" \"be\" \"became\" \"because\" \"become\"\n          \"becomes\" \"becoming\" \"been\" \"before\" \"beforehand\" \"behind\" \"being\" \"believe\" \"below\" \"beside\"\n          \"besides\" \"best\" \"better\" \"between\" \"beyond\" \"both\" \"brief\" \"but\" \"by\" \"came\" \"can\" \"cannot\"\n          \"cant\" \"can't\" \"cause\" \"causes\" \"certain\" \"certainly\" \"changes\" \"clearly\" \"c'mon\" \"co\" \"com\"\n          \"come\" \"comes\" \"concerning\" \"consequently\" \"consider\" \"considering\" \"contain\" \"containing\"\n          \"contains\" \"corresponding\" \"could\" \"couldn't\" \"course\" \"c's\" \"currently\" \"definitely\" \"described\"\n          \"despite\" \"did\" \"didn't\" \"different\" \"do\" \"does\" \"doesn't\" \"doing\" \"don\" \"done\" \"don't\" \"down\"\n          \"downwards\" \"during\" \"each\" \"edu\" \"eg\" \"eight\" \"either\" \"else\" \"elsewhere\" \"enough\" \"entirely\"\n          \"especially\" \"et\" \"etc\" \"even\" \"ever\" \"every\" \"everybody\" \"everyone\" \"everything\" \"everywhere\"\n          \"ex\" \"exactly\" \"example\" \"except\" \"far\" \"few\" \"fifth\" \"first\" \"five\" \"followed\" \"following\" \"follows\"\n          \"for\" \"former\" \"formerly\" \"forth\" \"four\" \"from\" \"further\" \"furthermore\" \"get\" \"gets\" \"getting\" \"given\"\n          \"gives\" \"go\" \"goes\" \"going\" \"gone\" \"got\" \"gotten\" \"greetings\" \"had\" \"hadn't\" \"happens\" \"hardly\" \"has\"\n          \"hasn't\" \"have\" \"haven't\" \"having\" \"he\" \"he'd\" \"he'll\" \"hello\" \"help\" \"hence\" \"her\" \"here\" \"hereafter\"\n          \"hereby\" \"herein\" \"here's\" \"hereupon\" \"hers\" \"herself\" \"he's\" \"hi\" \"him\" \"himself\" \"his\" \"hither\"\n          \"hopefully\" \"how\" \"howbeit\" \"however\" \"how's\" \"i\" \"i'd\" \"ie\" \"if\" \"ignored\" \"i'll\" \"i'm\" \"immediate\"\n          \"in\" \"inasmuch\" \"inc\" \"indeed\" \"indicate\" \"indicated\" \"indicates\" \"inner\" \"insofar\" \"instead\" \"into\"\n          \"inward\" \"is\" \"isn't\" \"it\" \"it'd\" \"it'll\" \"its\" \"it's\" \"itself\" \"i've\" \"just\" \"keep\" \"keeps\" \"kept\"\n          \"know\" \"known\" \"knows\" \"last\" \"lately\" \"later\" \"latter\" \"latterly\" \"least\" \"less\" \"lest\" \"let\" \"let's\"\n          \"like\" \"liked\" \"likely\" \"little\" \"look\" \"looking\" \"looks\" \"ltd\" \"mainly\" \"many\" \"may\" \"maybe\" \"me\"\n          \"mean\" \"meanwhile\" \"merely\" \"might\" \"more\" \"moreover\" \"most\" \"mostly\" \"much\" \"must\" \"mustn't\" \"my\"\n          \"myself\" \"name\" \"namely\" \"nd\" \"near\" \"nearly\" \"necessary\" \"need\" \"needs\" \"neither\" \"never\" \"nevertheless\"\n          \"new\" \"next\" \"nine\" \"no\" \"nobody\" \"non\" \"none\" \"noone\" \"nor\" \"normally\" \"not\" \"nothing\" \"novel\" \"now\"\n          \"nowhere\" \"obviously\" \"of\" \"off\" \"often\" \"oh\" \"ok\" \"okay\" \"old\" \"on\" \"once\" \"one\" \"ones\" \"only\" \"onto\"\n          \"or\" \"other\" \"others\" \"otherwise\" \"ought\" \"our\" \"ours\" \"ourselves\" \"out\" \"outside\" \"over\" \"overall\" \"own\"\n          \"particular\" \"particularly\" \"per\" \"perhaps\" \"placed\" \"please\" \"plus\" \"possible\" \"presumably\" \"probably\"\n          \"provides\" \"que\" \"quite\" \"qv\" \"rather\" \"rd\" \"re\" \"really\" \"reasonably\" \"regarding\" \"regardless\" \"regards\"\n          \"relatively\" \"respectively\" \"right\" \"s\" \"said\" \"same\" \"saw\" \"say\" \"saying\" \"says\" \"second\" \"secondly\" \"see\"\n          \"seeing\" \"seem\" \"seemed\" \"seeming\" \"seems\" \"seen\" \"self\" \"selves\" \"sensible\" \"sent\" \"serious\" \"seriously\"\n          \"seven\" \"several\" \"shall\" \"shan't\" \"she\" \"she'd\" \"she'll\" \"she's\" \"should\" \"shouldn't\" \"since\" \"six\" \"so\"\n          \"some\" \"somebody\" \"somehow\" \"someone\" \"something\" \"sometime\" \"sometimes\" \"somewhat\" \"somewhere\" \"soon\" \"sorry\"\n          \"specified\" \"specify\" \"specifying\" \"still\" \"sub\" \"such\" \"sup\" \"sure\" \"t\" \"take\" \"taken\" \"tell\" \"tends\" \"th\"\n          \"than\" \"thank\" \"thanks\" \"thanx\" \"that\" \"thats\" \"that's\" \"the\" \"their\" \"theirs\" \"them\" \"themselves\" \"then\"\n          \"thence\" \"there\" \"thereafter\" \"thereby\" \"therefore\" \"therein\" \"theres\" \"there's\" \"thereupon\" \"these\" \"they\"\n          \"they'd\" \"they'll\" \"they're\" \"they've\" \"think\" \"third\" \"this\" \"thorough\" \"thoroughly\" \"those\" \"though\"\n          \"three\" \"through\" \"throughout\" \"thru\" \"thus\" \"to\" \"together\" \"too\" \"took\" \"toward\" \"towards\" \"tried\"\n          \"tries\" \"truly\" \"try\" \"trying\" \"t's\" \"twice\" \"two\" \"un\" \"under\" \"unfortunately\" \"unless\" \"unlikely\"\n          \"until\" \"unto\" \"up\" \"upon\" \"us\" \"use\" \"used\" \"useful\" \"uses\" \"using\" \"usually\" \"value\" \"various\" \"very\"\n          \"via\" \"viz\" \"vs\" \"want\" \"wants\" \"was\" \"wasn't\" \"way\" \"we\" \"we'd\" \"welcome\" \"well\" \"we'll\" \"went\" \"were\"\n          \"we're\" \"weren't\" \"we've\" \"what\" \"whatever\" \"what's\" \"when\" \"whence\" \"whenever\" \"when's\" \"where\"\n          \"whereafter\" \"whereas\" \"whereby\" \"wherein\" \"where's\" \"whereupon\" \"wherever\" \"whether\" \"which\" \"while\"\n          \"whither\" \"who\" \"whoever\" \"whole\" \"whom\" \"who's\" \"whose\" \"why\" \"why's\" \"will\" \"willing\" \"wish\" \"with\"\n          \"within\" \"without\" \"wonder\" \"won't\" \"would\" \"wouldn't\" \"yes\" \"yet\" \"you\" \"you'd\" \"you'll\" \"your\"\n          \"you're\" \"yours\" \"yourself\" \"yourselves\" \"you've\" \"zero\"))\n   (stop-words-lookup :accessor stop-words-lookup)))\n\n(defmethod initialize-instance :after ((data language-data) &key)\n  (setf (stop-words-lookup data)\n        (loop with ht = (make-hash-table :test #'equal)\n              for stop in (stop-words data)\n              do (setf (gethash stop ht) t)\n              finally (return ht))))\n\n(defparameter *language-data* (make-instance 'language-data))\n"
  },
  {
    "path": "libraries/analysis/dbscan.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :analysis)\n\n;;; dbscan.lisp -- implementation of Density-based spatial clustering\n;;; of applications with noise (DBSCAN) algorithm\n\n(defclass document-cluster (document-vertex)\n  ((cluster :accessor cluster :initform :noise)\n   (neighbors :accessor neighbors))\n  (:documentation \"The document cluster class represents a document\nthat is part of a graph which will be clustered. It extends the\ndocumenet-vertex class and adds support for a cluster tag and a list\nof neighbors. These slots are useful for clustering algorithms.\"))\n\n(defmethod clusters ((collection document-collection))\n  \"Return a list of clusters. Each hash key represents a cluster, and\n   the hash value is the list of elements in that cluster.\n\n   Please note: this function is not responsible for computing the\n   clusters, only for returning the list of pre-tagged documents in\n   cluster lists.\"\n  (let ((result (make-hash-table)))\n    (loop for document in (documents collection)\n          do (push document (gethash (cluster document) result (list))))\n    result))\n\n(defun get-cluster (cluster-label points)\n  \"Return all matching points for a given cluster label.\"\n  (remove-if-not (lambda (i) (eq (cluster i) cluster-label)) points))\n\n(defmethod distance ((vector-1 t) (vector-2 t))\n  \"Return the Euclidean distance between two vectors.\"\n  (sqrt (loop for i across vector-1\n              for j across vector-2\n              sum (expt (- i j) 2))))\n\n(defmethod distance ((document-a document-cluster) (document-b document-cluster))\n  (distance (vector-data document-a) (vector-data document-b)))\n\n(defmethod generate-document-distance-vectors ((collection document-collection))\n  \"Set the edge weights for all document neighbors (graph is fully connected).\"\n  (with-accessors ((documents documents)) collection\n    (loop for document-a in documents\n          do (loop for document-b in documents\n                   do (setf (gethash document-b (edges document-a))\n                            (distance document-a document-b))))))\n\n(defmethod dbscan ((collection document-collection) &key (minimum-points 3)\n                                                         (epsilon 0.5))\n  \"Minimum points refers to the minimum amount of points that must\n   exist in the neighborhood of a point for it to be considered a\n   core-point in a cluster. Epsilon refers to the distance between\n   two points for them to be considered neighbors.\"\n  (labels ((range-query (document)\n             \"Return all points that have a distance less than epsilon.\"\n             (loop for vertex being the hash-keys of (edges document)\n                   when (and (<= (gethash vertex (edges document)) epsilon)\n                             (not (eq vertex document)))\n                   collect vertex))\n           (core-point-p (point)\n             \"Is a point a core-point?\"\n             (<= minimum-points (length (range-query point))))\n           (cluster-match-p (point cluster)\n             \"Check if a core point belongs to a cluster.\"\n             (intersection cluster (range-query point))))\n    ;;; identify core points\n    (let* ((core-points (remove-if-not #'core-point-p (documents collection)))\n           (non-core-points (set-difference (documents collection) core-points)))\n      ;;; assign labels to core points\n      (loop for point in core-points\n            with cluster-count = 0\n            do (loop named cluster-set\n                     for i from 0 to cluster-count\n                     ;; point found cluster match, setf and break\n                     when (cluster-match-p point (get-cluster i core-points))\n                     do (setf (cluster point) i)\n                        (return-from cluster-set)\n                     ;; point found no cluster-match, create new cluster\n                     finally (setf (cluster point) (incf cluster-count))))\n      ;;; assign labels to non-core points\n      (loop for point in non-core-points\n            for intersection = (intersection core-points (range-query point))\n            when intersection\n            do (setf (cluster point) (cluster (first intersection)))))))\n"
  },
  {
    "path": "libraries/analysis/document-vector.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :analysis)\n\n;;; document-vector.lisp: transform a document into a vector\n\n(defmethod word-count-vectorize ((document document) dictionary)\n  \"Transform a document into a vector using word counts.\"\n  (let ((vector-data (make-array (length dictionary) :initial-element 0)))\n    (loop for word in dictionary\n          for index from 0 below (length vector-data)\n          do (setf (aref vector-data index) (term-count document word)))\n    (setf (vector-data document) vector-data)))\n\n(defmethod tf-idf-vectorize ((document document) (collection document-collection) dictionary)\n  \"Transform a document into a vector using tf-idf.\nDefinition: tf-idf: term frequency, inverse document frequency. How\noften does a term a appear in a document as compared to all other\ndocuments?\"\n  (let ((vector-data (make-array (length dictionary) :initial-element 0)))\n    (loop for word in dictionary\n          for index from 0 below (length vector-data)\n          do (setf (aref vector-data index)\n                   (term-frequency-inverse-document-frequency document collection word)))\n    (setf (vector-data document) vector-data)))\n\n(defmethod tf-vectorize ((document document) dictionary)\n  \"Transform a document into a vector using tf.\nDefinition: tf: term frequency. How often does a term appear in a\ndocument?\"\n  (let ((vector-data (make-array (length dictionary) :initial-element 0)))\n    (loop for word in dictionary\n          for index from 0 below (length vector-data)\n          do (setf (aref vector-data index)\n                   (term-frequency document word)))\n    (setf (vector-data document) vector-data)))\n\n(defmethod vectorize-documents ((document-collection document-collection) operation)\n  (let ((dictionary (dictionary document-collection)))\n    (loop for document in (documents document-collection)\n          do (funcall operation document dictionary))))\n\n(defmethod word-count-vectorize-documents ((document-collection document-collection))\n  (vectorize-documents document-collection #'word-count-vectorize))\n\n(defmethod tf-vectorize-documents ((document-collection document-collection))\n  \"Definition: tf: term frequency. How often does a term appear in a\ndocument?\"\n  (vectorize-documents document-collection #'tf-vectorize))\n\n(defmethod tf-idf-vectorize-documents ((document-collection document-collection))\n  \"Definition: tf-idf: term frequency, inverse document frequency. How\noften does a term appear in a document as compared to all other\ndocuments?\"\n  (vectorize-documents document-collection (lambda (document dictionary)\n                                             (tf-idf-vectorize document document-collection dictionary))))\n\n\n"
  },
  {
    "path": "libraries/analysis/package.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(uiop:define-package :analysis\n  (:use :cl)\n  (:import-from :serapeum #:export-always))\n"
  },
  {
    "path": "libraries/analysis/section.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :analysis)\n\n;;; section.lisp -- given a document, automatically generate sections\n\n\n(export-always 'extract-sections)\n(defun extract-sections (text &key (epsilon 0.5))\n  \"Extract the sections from a string of text. Epsilon refers to the\n   distance between two points for them to be considered related.\"\n  (labels ((average-distance (point points)\n             (/ (reduce #'+ points\n                        :key (lambda (i) (distance (vector-data i)\n                                                   (vector-data point))))\n                (length points))))\n    (let ((collection (make-instance 'document-collection)))\n      (loop for sentence in (sentence-tokenize text)\n            do (add-document collection\n                             (make-instance 'document-cluster\n                                            :string-contents sentence)))\n      (tf-vectorize-documents collection)\n      (loop for document in (documents collection)\n            with cluster-index = 0\n            for cluster = (get-cluster cluster-index (documents collection))\n            do (if (and cluster (>= epsilon (average-distance document cluster)))\n                   (setf (cluster document) cluster-index)\n                   (setf (cluster document) (incf cluster-index))))\n      collection)))\n"
  },
  {
    "path": "libraries/analysis/stem.lisp",
    "content": "(in-package :analysis)\n\n;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n;; The software is completely free for any purpose, unless notes at\n;; the head of the program text indicates otherwise (which is\n;; rare). In any case, the notes about licensing are never more\n;; restrictive than the BSD License.\n;\n;; In every case where the software is not written by me (Martin\n;; Porter), this licensing arrangement has been endorsed by the\n;; contributor, and it is therefore unnecessary to ask the contributor\n;; again to confirm it.\n;\n;; The Porter Stemming Algorithm, somewhat mechanically hand translated to Common Lisp by\n;; Steven M. Haflich smh@franz.com Feb 2002.  Most of the inline comments refer to the\n;; original C code.  At the time of this translation the code passes the associated Porter\n;; test files.  See the function test at the end of this file.\n\n;; This port is intended to be portable ANSI Common Lisp.  However, it has only been\n;; compiled and tested with Allegro Common Lisp.  This code is offered in the hope it will\n;; be useful, but with no warranty of correctness, suitability, usability, or anything\n;; else.  The C implementation from which this code was derived was not reentrant, relying\n;; on global variables.  This implementation corrects that.  It is intended that a word to\n;; be stemmed will be in a string with fill-pointer, as this is a natural result when\n;; parsing user input, web scraping, whatever.  If not, a string with fill-pointer is\n;; created, but this is an efficiency hit and is here intended only for lightweight use or\n;; testing.  Using some resource mechanism on these strings would be a useful improvement,\n;; whether here or in the calling code.\n\n;; Postscript: When I contacted Martin Porter about this anachronism, he decided to fix\n;; the C version to implement proper reentrancy.  The CL version is now also served from\n;; his central site.  It should be functionally identical to this one, modulo the current\n;; comment and a couple harmless formatting and comment changes.\n;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n\n;; This is the Porter stemming algorithm, coded up in ANSI C by the\n;; author. It may be regarded as canonical, in that it follows the\n;; algorithm presented in\n\n;; Porter, 1980, An algorithm for suffix stripping, Program, Vol. 14,\n;; no. 3, pp 130-137,\n\n;; only differing from it at the points maked --DEPARTURE-- below.\n\n;; See also http://www.tartarus.org/~martin/PorterStemmer\n\n;; The algorithm as described in the paper could be exactly replicated\n;; by adjusting the points of DEPARTURE, but this is barely necessary,\n;; because (a) the points of DEPARTURE are definitely improvements, and\n;; (b) no encoding of the Porter stemmer I have seen is anything like\n;; as exact as this version, even with the points of DEPARTURE!\n\n;; You can compile it on Unix with 'gcc -O3 -o stem stem.c' after which\n;; 'stem' takes a list of inputs and sends the stemmed equivalent to\n;; stdout.\n\n;; The algorithm as encoded here is particularly fast.\n\n;; Release 1\n\n;; The main part of the stemming algorithm starts here. b is a buffer\n;; holding a word to be stemmed. The letters are in b[k0], b[k0+1] ...\n;; ending at b[k]. In fact k0 = 0 in this demo program. k is readjusted\n;; downwards as the stemming progresses. Zero termination is not in fact\n;; used in the algorithm.\n\n;; Note that only lower case sequences are stemmed. Forcing to lower case\n;; should be done before stem(...) is called.\n\n;; cons(i) is TRUE <=> b[i] is a consonant.\n\n;;; Common Lisp port Version 1.01\n\n;;;\n;;; Common Lisp port Version history\n;;;\n;;; 1.0  -- smh@franz.com Feb 2002\n;;;         initial release\n;;;\n;;; 1.01 -- smh@franz.com 25 Apr 2004\n;;;         step4 signalled error for \"ion\" \"ions\".  Thanks to Jeff Heard\n;;;         for detecting this and suggesting the fix.\n\n(defun consonantp (str i)\n  (let ((char (char str i)))\n    (cond ((member char '(#\\a #\\e #\\i #\\o #\\u)) nil)\n\t  ((eql char #\\y)\n\t   (if (= i 0) t (not (consonantp str (1- i)))))\n\t  (t t))))\n\n;; m() measures the number of consonant sequences between k0 and j. if c is\n;; a consonant sequence and v a vowel sequence, and <..> indicates arbitrary\n;; presence,\n\n;;    <c><v>       gives 0\n;;    <c>vc<v>     gives 1\n;;    <c>vcvc<v>   gives 2\n;;    <c>vcvcvc<v> gives 3\n;;    ....\n\n(defun m (str lim)\n  (let ((n 0)\n\t(i 0))\n    (loop\n      (when (>= i lim) (return-from m n))\n      (if (not (consonantp str i)) (return nil))\n      (incf i))\n    (incf i)\n    (loop\n      (loop\n\t(if (>= i lim) (return-from m n))\n\t(if (consonantp str i) (return nil))\n\t(incf i))\n      (incf i)\n      (incf n)\n      (loop\n\t(if (>= i lim) (return-from m n))\n\t(if (not (consonantp str i)) (return nil))\n\t(incf i))\n      (incf i))))\n\n;; vowelinstem() is TRUE <=> k0,...j contains a vowel\n\n(defun vowelinstem (str)\n  (loop for i from 0 below (fill-pointer str)\n      unless (consonantp str i) return t))\n\n;; doublec(j) is TRUE <=> j,(j-1) contain a double consonant.\n\n(defun doublec (str i)\n  (cond ((< i 1) nil)\n\t((not (eql (char str i) (char str (1- i)))) nil)\n\t(t (consonantp str i))))\n\n;; cvc(i) is TRUE <=> i-2,i-1,i has the form consonant - vowel - consonant\n;; and also if the second c is not w,x or y. this is used when trying to\n;; restore an e at the end of a short word. e.g.\n\n;;    cav(e), lov(e), hop(e), crim(e), but\n;;    snow, box, tray.\n\n(defun cvc (str lim)\n  (decf lim)\n  (if (or (< lim 2)\n\t  (not (consonantp str lim))\n\t  (consonantp str (1- lim))\n\t  (not (consonantp str (- lim 2))))\n      (return-from cvc nil))\n  (if (member (char str lim) '(#\\w #\\x #\\y)) (return-from cvc nil))\n  t)\n\n;; ends(s) is TRUE <=> k0,...k ends with the string s.\n\n(defun ends (str ending)\n  (declare (string str) (simple-string ending))\n  (let ((len1 (length str)) (len2 (length ending)))\n    (loop\n\tfor pa downfrom (1- len1) to 0\n\tand pb downfrom (1- len2) to 0\n\tunless (eql (char str pa) (char ending pb))\n\treturn nil\n\tfinally (return (when (< pb 0)\n\t\t\t  (decf (fill-pointer str) len2)\n\t\t\t  t)))))\n\n;; setto(s) sets (j+1),...k to the characters in the string s, readjusting k.\n\n(defun setto (str suffix)\n  (declare (string str) (simple-string suffix))\n  (loop for char across suffix\n      do (vector-push-extend char str)))\n\n;; r(s) is used further down.\n\n(defun r (str s sfp)\n  (if (> (m str (fill-pointer str)) 0)\n      (setto str s)\n    (setf (fill-pointer str) sfp)))\n\n;; step1ab() gets rid of plurals and -ed or -ing. e.g.\n\n;;     caresses  ->  caress\n;;     ponies    ->  poni\n;;     ties      ->  ti\n;;     caress    ->  caress\n;;     cats      ->  cat\n\n;;     feed      ->  feed\n;;     agreed    ->  agree\n;;     disabled  ->  disable\n\n;;     matting   ->  mat\n;;     mating    ->  mate\n;;     meeting   ->  meet\n;;     milling   ->  mill\n;;     messing   ->  mess\n\n;;     meetings  ->  meet\n\n(defun step1ab (str)\n  (when (eql (char str (1- (fill-pointer str))) #\\s)\n    (cond ((ends str \"sses\") (incf (fill-pointer str) 2))\n\t  ((ends str \"ies\")  (setto str \"i\"))\n\t  ((not (eql (char str (- (fill-pointer str) 2)) #\\s)) (decf (fill-pointer str)))))\n  (cond ((ends str \"eed\") (if (> (m str (fill-pointer str)) 0)\n\t\t\t      (incf (fill-pointer str) 2)\n\t\t\t    (incf (fill-pointer str) 3)))\n\t((let ((sfp (fill-pointer str)))\n\t   (if (or (ends str \"ed\")\n\t\t   (ends str \"ing\"))\n\t       (if (vowelinstem str)\n\t\t   t\n\t\t (progn (setf (fill-pointer str) sfp)\n\t\t\tnil))))\n\t (cond ((ends str \"at\") (setto str \"ate\"))\n\t       ((ends str \"bl\") (setto str \"ble\"))\n\t       ((ends str \"iz\") (setto str \"ize\"))\n\t       ((doublec str (1- (fill-pointer str)))\n\t\t(unless (member (char str (1- (fill-pointer str))) '(#\\l #\\s #\\z))\n\t\t  (decf (fill-pointer str))))\n\t       (t (if (and (= (m str (fill-pointer str)) 1)\n\t\t\t   (cvc str (fill-pointer str)))\n\t\t      (setto str \"e\"))))))\n  str)\n\n;; step1c() turns terminal y to i when there is another vowel in the stem.\n\n(defun step1c (str)\n  (let ((saved-fill-pointer (fill-pointer str)))\n    (when (and (ends str \"y\")\n\t       (vowelinstem str))\n\t(setf (char str (fill-pointer str)) #\\i))\n    (setf (fill-pointer str) saved-fill-pointer))\n  str)\n\n;; step2() maps double suffices to single ones. so -ization ( = -ize plus\n;; -ation) maps to -ize etc. note that the string before the suffix must give\n;; m() > 0.\n\n(defun step2 (str)\n  (let ((sfp (fill-pointer str)))\n    (when (> sfp 2)\n      (block nil\n\t(case (char str (- (length str) 2))\n\t  (#\\a (when (ends str \"ational\") (r str \"ate\"  sfp)  (return))\n\t       (when (ends str \"tional\")  (r str \"tion\" sfp) (return)))\n\t  (#\\c (when (ends str \"enci\")    (r str \"ence\" sfp) (return))\n\t       (when (ends str \"anci\")    (r str \"ance\" sfp) (return)))\n\t  (#\\e (when (ends str \"izer\")    (r str \"ize\"  sfp)  (return)))\n\t  (#\\l (when (ends str \"bli\")     (r str \"ble\"  sfp)  (return))\n\t       ;; -DEPARTURE-\n\t       ;; To match the published algorithm, replace prev line with\n\t       ;; ((when (ends str \"abli\")    (r str \"able\" sfp) (return))\n\t       (when (ends str \"alli\")    (r str \"al\"  sfp)   (return))\n\t       (when (ends str \"entli\")   (r str \"ent\" sfp)  (return))\n\t       (when (ends str \"eli\")     (r str \"e\"   sfp)    (return))\n\t       (when (ends str \"ousli\")   (r str \"ous\" sfp)  (return)))\n\t  (#\\o (when (ends str \"ization\") (r str \"ize\" sfp)  (return))\n\t       (when (ends str \"ation\")   (r str \"ate\" sfp)  (return))\n\t       (when (ends str \"ator\")    (r str \"ate\" sfp)  (return)))\n\t  (#\\s (when (ends str \"alism\")   (r str \"al\"  sfp)   (return))\n\t       (when (ends str \"iveness\") (r str \"ive\" sfp)  (return))\n\t       (when (ends str \"fulness\") (r str \"ful\" sfp)  (return))\n\t       (when (ends str \"ousness\") (r str \"ous\" sfp)  (return)))\n\t  (#\\t (when (ends str \"aliti\")   (r str \"al\"  sfp)   (return))\n\t       (when (ends str \"iviti\")   (r str \"ive\" sfp)  (return))\n\t       (when (ends str \"biliti\")  (r str \"ble\" sfp)  (return)))\n\t  ;; -DEPARTURE-\n\t  ;; To match the published algorithm, delete next line.\n\t  (#\\g (when (ends str \"logi\")    (r str \"log\" sfp)  (return)))))))\n  str)\n\n;; step3() deals with -ic-, -full, -ness etc. similar strategy to step2.\n\n(defun step3 (str)\n  (let ((sfp (fill-pointer str)))\n    (block nil\n      (case (char str (1- (length str)))\n\t(#\\e (when (ends str \"icate\") (r str \"ic\" sfp) (return))\n\t     (when (ends str \"ative\") (r str \"\" sfp)   (return)) ; huh?\n\t     (when (ends str \"alize\") (r str \"al\" sfp) (return)))\n\t(#\\i (when (ends str \"iciti\") (r str \"ic\" sfp) (return)))\n\t(#\\l (when (ends str \"ical\")  (r str \"ic\" sfp) (return))\n\t     (when (ends str \"ful\")   (r str \"\" sfp)   (return))) ; huh?\n\t(#\\s (when (ends str \"ness\")  (r str \"\" sfp)   (return))) ; huh?\n\t)))\n  str)\n\n;; step4() takes off -ant, -ence etc., in context <c>vcvc<v>.\n\n(defun step4 (str)\n  (let ((sfp (fill-pointer str)))\n    (when (> sfp 2)\t\t\t; Unnecessary?\n      (block nil\n\t(case (char str (- sfp 2))\n\t  (#\\a (if (ends str \"al\")    (return)))\n\t  (#\\c (if (ends str \"ance\")  (return))\n\t       (if (ends str \"ence\")  (return)))\n\t  (#\\e (if (ends str \"er\")    (return)))\n\t  (#\\i (if (ends str \"ic\")    (return)))\n\t  (#\\l (if (ends str \"able\")  (return))\n\t       (if (ends str \"ible\")  (return)))\n\t  (#\\n (if (ends str \"ant\")   (return))\n\t       (if (ends str \"ement\") (return))\n\t       (if (ends str \"ment\")  (return))\n\t       (if (ends str \"ent\")   (return)))\n\t  (#\\o (if (ends str \"ion\")\n\t\t   (let ((len (length str)))\n\t\t     (if (and (> len 0)\n\t\t\t      (let ((c (char str (1- len))))\n\t\t\t\t(or (eql c #\\s) (eql c #\\t))))\n\t\t\t (return)\n\t\t       (setf (fill-pointer str) sfp))))\n\t       (if (ends str \"ou\")    (return))) ; takes care of -ous\n\t  (#\\s (if (ends str \"ism\")   (return)))\n\t  (#\\t (if (ends str \"ate\")   (return))\n\t       (if (ends str \"iti\")   (return)))\n\t  (#\\u (if (ends str \"ous\")   (return)))\n\t  (#\\v (if (ends str \"ive\")   (return)))\n\t  (#\\z (if (ends str \"ize\")   (return))))\n\t(return-from step4 str))\n      (unless (> (m str (fill-pointer str)) 1)\n\t(setf (fill-pointer str) sfp)))\n    str))\n\n;; step5() removes a final -e if m() > 1, and changes -ll to -l if m() > 1.\n\n(defun step5 (str)\n  (let ((len (fill-pointer str)))\n    (if (eql (char str (1- len)) #\\e)\n\t(let ((a (m str len)))\n\t  (if (or (> a 1)\n\t\t  (and (= a 1)\n\t\t       (not (cvc str (1- len)))))\n\t      (decf (fill-pointer str))))))\n  (let ((len (fill-pointer str)))\n    (if (and (eql (char str (1- len)) #\\l)\n\t     (doublec str (1- len))\n\t     (> (m str len) 1))\n\t(decf (fill-pointer str))))\n  str)\n\n;; In stem(p,i,j), p is a char pointer, and the string to be stemmed is from p[i] to p[j]\n;; inclusive. Typically i is zero and j is the offset to the last character of a string,\n;; (p[j+1] == '\\0'). The stemmer adjusts the characters p[i] ... p[j] and returns the new\n;; end-point of the string, k.  Stemming never increases word length, so i <= k <= j. To\n;; turn the stemmer into a module, declare 'stem' as extern, and delete the remainder of\n;; this file.\n\n(defun stem (str)\n  (let ((len (length str)))\n    ;; With this line, strings of length 1 or 2 don't go through the\n    ;; stemming process, although no mention is made of this in the\n    ;; published algorithm. Remove the line to match the published\n    ;; algorithm.\n    (if (<= len 2) (return-from stem str)) ; /*-DEPARTURE-*/\n    (if (typep str 'simple-string)\t; Primarily for testing.\n\t(setf str\n\t  (make-array len :element-type 'character\n\t\t      :fill-pointer len :initial-contents str)))\n    (step1ab str) (step1c str) (step2 str) (step3 str) (step4 str) (step5 str)\n    str))\n\n#+never\n(trace step1ab step1c step2 step3 step4 step5)\n\n#+never\n(defun test ()\t\t\t\t; Run against the distributed test files.\n  (with-open-file (f1 \"voc.txt\")\n    (with-open-file (f2 \"output.txt\")\n      (loop as w1 = (read-line f1 nil nil)\n\t  while w1\n\t  as w2 = (read-line f2 nil nil)\n\t  as w3 = (stem w1)\n\t  if (equal w2 w3)\n\t  count t into successes\n\t  else count t into failures\n\t  and do (format t \"(stem ~s) => ~s wanted ~s~%\" w1 w3 w2)\n\t  finally (progn (format t \"sucesses ~d failures ~d~%\" successes failures)\n\t\t\t (return failures))))))\n"
  },
  {
    "path": "libraries/analysis/tests/tests.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(uiop:define-package :analysis/tests\n  (:use :cl :lisp-unit2)\n  (:import-from :analysis))\n(in-package :analysis/tests)\n\n(define-test test-single-length ()\n  (let ((model (make-instance 'analysis::sequence-model)))\n    (analysis::add-record model '(1 2))\n    (assert-equal (analysis::element (analysis::predict model '(1))) 2))\n\n  (let ((model (make-instance 'analysis::sequence-model)))\n    (analysis::add-record model '(1 2))\n    (analysis::add-record model '(2 3))\n    (analysis::add-record model '(2 3))\n    (assert-equal (analysis::element (analysis::predict model '(1))) 2))\n\n  (let ((model (make-instance 'analysis::sequence-model)))\n    (analysis::add-record model '(1 2))\n    (analysis::add-record model '(1 2))\n    (analysis::add-record model '(1 2))\n    (analysis::add-record model '(1 3))\n    (analysis::add-record model '(1 3))\n    (assert-equal (analysis::element (analysis::predict model '(1))) 2))\n\n  (let ((model (make-instance 'analysis::sequence-model)))\n    (analysis::add-record model '(1 2))\n    (analysis::add-record model '(1 2))\n    (analysis::add-record model '(1 3))\n    (analysis::add-record model '(1 3))\n    (analysis::add-record model '(1 3))\n    (assert-equal (analysis::element (analysis::predict model '(1))) 3))\n\n  (let ((model (make-instance 'analysis::sequence-model)))\n    (analysis::add-record model '(1 2))\n    (analysis::add-record model '(1 3))\n    (analysis::add-record model '(1 2))\n    (assert-equal (analysis::element (analysis::predict model '(1))) 2)))\n\n(define-test test-multiple-length ()\n  (let ((model (make-instance 'analysis::sequence-model)))\n    (analysis::add-record model '(1 2 3))\n    (assert-equal (analysis::element (analysis::predict model '(1 2))) 3))\n\n  ;; Make sure the most temporally recent element is used\n  ;; Fails in CCL.\n  (let ((model (make-instance 'analysis::sequence-model)))\n    (analysis::add-record model '(1 2 4))\n    (analysis::add-record model '(1 2 3))\n    (assert-equal (analysis::element (analysis::predict model '(1 2))) 3))\n\n  (let ((model (make-instance 'analysis::sequence-model)))\n    (analysis::add-record model '(1 2 3))\n    (analysis::add-record model '(1 2 4))\n    (analysis::add-record model '(1 2 4))\n    (analysis::add-record model '(1 2 4))\n    (assert-equal (analysis::element (analysis::predict model '(1 2))) 4))\n\n  (let ((model (make-instance 'analysis::sequence-model)))\n    (analysis::add-record model '(1 2 3))\n    (analysis::add-record model '(1 2 4))\n    (analysis::add-record model '(1 2 4))\n    (analysis::add-record model '(1 2 4))\n    (analysis::add-record model '(1 2 3))\n    (analysis::add-record model '(1 2 3))\n    (analysis::add-record model '(1 2 3))\n    (analysis::add-record model '(1 2 3))\n    (assert-equal (analysis::element (analysis::predict model '(1 2))) 3)))\n\n(define-test test-variable-length ()\n  (let ((model (make-instance 'analysis::sequence-model)))\n    (analysis::add-record model '(1 2 3))\n    (analysis::add-record model '(1 2 4))\n    (analysis::add-record model '(1 2 4))\n    (analysis::add-record model '(1 2 4))\n    (analysis::add-record model '(1 2))\n    (analysis::add-record model '(1 2))\n    (analysis::add-record model '(1 2))\n    (analysis::add-record model '(1 2 3))\n    (analysis::add-record model '(1 2 3))\n    (analysis::add-record model '(1 2 3))\n    (analysis::add-record model '(1 2 3))\n    (assert-equal (analysis::element (analysis::predict model '(1 2))) 3))\n\n  (let ((model (make-instance 'analysis::sequence-model)))\n    (analysis::add-record model '(1 2 3))\n    (analysis::add-record model '(1 2 4))\n    (analysis::add-record model '(1 2 4))\n    (analysis::add-record model '(1 2 4))\n    (analysis::add-record model '(1 2))\n    (analysis::add-record model '(1 2))\n    (analysis::add-record model '(1 3))\n    (analysis::add-record model '(1 3))\n    (analysis::add-record model '(1 3))\n    (analysis::add-record model '(1 2 3))\n    (analysis::add-record model '(1 2 3))\n    (analysis::add-record model '(1 2 3))\n    (analysis::add-record model '(1 2 3))\n    (assert-equal (analysis::element (analysis::predict model '(1))) 3)\n    (assert-equal (analysis::element (analysis::predict model '(1 2))) 3)))\n"
  },
  {
    "path": "libraries/analysis/text-rank.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :analysis)\n\n;;; text-rank.lisp -- implementation of textrank algorithm\n\n(defclass document-vertex (document)\n  ((edges :accessor edges :initform (make-hash-table)\n          :documentation \"The keys of the hash table represent the\n          edges, the values of the hash table represent the edge\n          weights.\"))\n  (:documentation \"The document vertex class represents a document\nthat is part of a graph. The edges slot of the document vertex class\nis used to store edges of that particular vertex. The keys in the\nedges slot hash table are the actual vertexes, and the values are the\nedge weights.\"))\n\n(defmethod cosine-similarity ((document-a document) (document-b document))\n  \"Calculate the cosine similarity between two vectors.\"\n  (flet ((vector-product (document-a document-b)\n           (loop for a across (vector-data document-a)\n                 for b across (vector-data document-b)\n                 sum (* a b)))\n         (vector-sum-root (document)\n           (sqrt (loop for i across (vector-data document)\n                       sum (* i i))))\n         (vector-zero-p (document)\n           (every #'zerop (vector-data document))))\n    (if (or (vector-zero-p document-a) (vector-zero-p document-b))\n        0 ; if either vector is completely zero, they are dissimilar\n        (/ (vector-product document-a document-b)\n           (* (vector-sum-root document-a) (vector-sum-root document-b))))))\n\n(defmethod generate-document-similarity-vectors ((collection document-collection))\n  \"Set the edge weights for all document neighbors (graph is fully connected).\"\n  (with-accessors ((documents documents)) collection\n    (loop for document-a in documents\n          do (loop for document-b in documents\n                   do (setf (gethash document-b (edges document-a))\n                            (cosine-similarity document-a document-b))))))\n\n(defmethod text-rank ((collection document-collection) &key (epsilon 0.001)\n                                                            (damping 0.85)\n                                                            (initial-rank)\n                                                            (iteration-limit 100))\n  \"This method is used to calculate the text rankings for a document\n   collection. The `epsilon' is the maximum delta for a given node\n   rank change during an iteration to be considered convergent. The\n   `damping' is a factor utilized to normalize the data. The\n   `initial-rank' is the rank given to nodes before any\n   iterations. The `iteration-limit' is the amount of times the\n   algorithm may traverse the graph before giving up (if the algorithm\n   does not converge).\"\n  (with-accessors ((documents documents)) collection\n    (unless (zerop (length documents))\n      (labels ((set-initial-rank ()\n                 \"Set the initial rank of all documents to a supplied\n                value OR 1/length of the documents.\"\n                 (let ((initial-rank (or initial-rank (/ 1 (length documents)))))\n                   (mapcar (lambda (document) (setf (rank document) initial-rank)) documents)))\n               (graph-neighbors (document)\n                 \"Return a list of neighbors. In a fully connected graph,\n                all nodes are a neighbor except for the node itself.\"\n                 (remove document documents))\n               (graph-neighbor-edge-sum (document)\n                 \"Add up the edges of all neighbors of a given node.\"\n                 (let ((sum (- (reduce #'+ (alexandria:hash-table-values (edges document))) 1)))\n                   (if (> sum 0) sum 1)))\n               (document-similarity (document-a document-b)\n                 (gethash document-b (edges document-a) 0))\n               (convergedp (previous-score current-score)\n                 \"Check if a delta qualifies for convergence.\"\n                 (<=  (abs (- previous-score current-score)) epsilon))\n               (calculate-rank (document)\n                 \"Calculate the rank of a document.\"\n                 (loop for neighbor in (graph-neighbors document)\n                       sum (/ (* damping (rank neighbor) (document-similarity document neighbor))\n                              (graph-neighbor-edge-sum neighbor)))))\n        (set-initial-rank)\n        (loop with converged = nil\n              for iteration from 0 to iteration-limit until converged\n              do (setf converged t)\n                 (loop for document in documents\n                       for old-rank = (rank document)\n                       for new-rank = (calculate-rank document)\n                       do (setf (rank document) new-rank)\n                       unless (convergedp old-rank new-rank)\n                       do (setf converged nil)))))))\n\n(export-always 'summarize-text)\n(defun summarize-text (text &key (summary-length 3) (show-rank-p nil))\n  (let ((collection (make-instance 'document-collection)))\n    (loop for sentence in (sentence-tokenize text)\n          do (add-document collection\n                           (make-instance 'document-vertex\n                                          :string-contents sentence)))\n    (tf-idf-vectorize-documents collection)\n    (generate-document-similarity-vectors collection)\n    (text-rank collection :iteration-limit 100)\n    (serapeum:take summary-length\n                   (mapcar (if show-rank-p\n                               (lambda (i) (cons (rank i) (string-contents i)))\n                               #'string-contents)\n                           (sort (documents collection) #'> :key #'rank)))))\n"
  },
  {
    "path": "libraries/analysis/tokenize.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :analysis)\n\n(defun word-tokenize (string &key (remove-stop-words t) (stem nil) (down-case t) (alphabetic t))\n  \"Split a string into a list of words.\"\n  (let* ((alpha-scanner (cl-ppcre:create-scanner \"^[A-Za-z]*$\"))\n         (tokens (str:split \" \" (str:collapse-whitespaces string)))\n         (tokens (if remove-stop-words\n                     (delete-if (lambda (x) (gethash (string-downcase  x) (stop-words-lookup *language-data*))) tokens)\n                     tokens))\n         (tokens (if stem\n                     (mapcar #'stem tokens)\n                     tokens))\n         (tokens (if down-case\n                     (mapcar #'string-downcase tokens)\n                     tokens))\n         (tokens (if alphabetic\n                     (delete-if-not (lambda (x) (cl-ppcre:scan alpha-scanner x)) tokens)\n                     tokens)))\n    tokens))\n\n(defun sentence-tokenize (string)\n  \"Split a string into a list of sentences.\"\n  ;; TODO: Use \"\\\\p{Terminal_Punctuation}\" regexp instead to catch all terminal\n  ;; punctuation marks, including \",\" and \";\"?\n  (remove \"\" (mapcar #'str:trim (cl-ppcre:split \"[.!?]\" string)) :test #'equal))\n"
  },
  {
    "path": "libraries/download-manager/engine.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :download-manager)\n\n(defvar *default-download-directory* #p\"~/Downloads/\")\n(defun default-download-directory ()\n  (let ((dir (ignore-errors (uiop:run-program '(\"xdg-user-dir\" \"DOWNLOAD\")\n                                              :output '(:string :stripped t)))))\n    (when (or (null dir) (string= dir (uiop:getenv \"HOME\")))\n      (setf dir (uiop:getenv \"XDG_DOWNLOAD_DIR\")))\n    (unless dir\n      (setf dir *default-download-directory*))\n    dir))\n\n(defun download-directory (&optional (directory (default-download-directory)))\n  \"Return path to download directory.\nCreate it if it does not exist.\"\n  (unless directory\n    (setf directory (default-download-directory)))\n  (unless (string= \"\" (file-namestring directory))\n    (setf directory (format nil \"~a/\" (namestring directory))))\n  (truename (ensure-directories-exist directory)))\n\n(defun ensure-unique-file (file)\n  \"Return FILE if unique or suffix it with a number otherwise.\"\n  (loop with original-name = file\n        with suffix = 1\n        while (uiop:file-exists-p file)\n        do (setf file (make-pathname :defaults original-name\n                                     :name (format nil \"~a.~d\" (pathname-name (pathname original-name))\n                                                   suffix)))\n        do (incf suffix))\n  (namestring (pathname file)))\n\n(defvar *notifications* nil\n  \"A channel which can be queried for download notifications.\nThe channel return value is a `download'.\")\n\n(defclass download ()\n  ((requested-url\n    :accessor requested-url\n    :initarg :requested-url\n    :initform (quri:uri \"\")\n    :type quri:uri\n    :documentation \"The URL that the user requested.\nThis may be different from the actual location of the download, e.g. in case of\nautomatic redirection.  See RESOLVED-URL.\")\n   (resolved-url\n    :accessor resolved-url\n    :initarg :resolved-url\n    :initform (quri:uri \"\")\n    :type quri:uri\n    :documentation \"The actual source of the download.\nThis may be different from the URL the user requested, see REQUESTED-URL.\")\n   (file\n    :accessor file\n    :initarg :file\n    :initform \"\"\n    :documentation \"Path pointing to the local storage location of the\ndownloaded file.\")\n   (downstream\n    :accessor downstream\n    :initarg :downstream\n    :initform nil\n    :documentation \"The stream which can be read from to do the actual\ndownload.\")\n   (status\n    :accessor status\n    :initarg :status\n    ;; TODO: String?\n    :initform nil)\n   (header\n    :accessor header\n    :initarg :header\n    :initform \"\")\n   (update-interval\n    :type alexandria:non-negative-real\n    :accessor update-interval\n    :initarg :update-interval\n    :initform 1.0\n    :documentation \"Time in seconds after which a notification is sent to the\n`*notifications*' channel.\")\n   (last-update\n    :type alexandria:non-negative-real\n    :accessor last-update\n    :initarg :last-update\n    :initform 0.0\n    :documentation \"Time in seconds when the last notification was sent.\")\n   (finished-p\n    :accessor finished-p\n    :initform nil\n    :documentation \"Non-nil if it has finished downloading.\")\n   (bytes-fetched\n    :accessor bytes-fetched\n    :initform 0)\n   (bytes-last-update\n    :accessor bytes-last-update\n    :initform 0\n    :documentation \"Bytes fetched when last `update' was called.\")\n   (last-update-speed\n    :accessor last-update-speed\n    :initform 0\n    :documentation \"Download speed in B/s when last `update' was called.\")))\n\n(defmethod filename ((download download))\n  \"Return the full name of this downloaded file, as a string.\"\n  (format nil \"~a\" (file download)))\n\n(defmethod temp-file ((download download))\n  \"Return a file name suitable for unfinished\ndownloads.\"\n  (ensure-unique-file\n   (format nil \"~a.part\" (namestring (file download)))))\n\n(defmethod bytes-total ((download download))\n  (let ((bytes (gethash \"content-length\"\n                        (header download) 0)))\n    (if (stringp bytes) (parse-integer bytes) bytes)))\n\n(defmethod progress ((download download))\n  \"Return progress ratio.\nWhen download is completed, return 1.0.\nWhen progress cannot be computer (because bytes-total is unknown), return\n(values 0 'unknown).\"\n  (cond\n    ((finished-p download) 1)\n    ((if (> (bytes-total download) 0)\n         (/ (float (bytes-fetched download))\n            (float (bytes-total download)))\n         (values 0 'unknown)))))\n\n(defmethod update ((download download))\n  \"Send DOWNLOAD to the `notifications' channel.\nOnly send if last update was more than `update-interval' seconds ago.\"\n  (let* ((new-time (/ (get-internal-real-time) (float internal-time-units-per-second)))\n         (time-diff (- new-time (last-update download))))\n    (when (or (< (update-interval download) time-diff)\n              (finished-p download))\n      (calispel:! *notifications* download)\n      (setf (last-update-speed download)\n            (if (= 0 time-diff)\n                0\n                (round (/ (float (- (bytes-fetched download) (bytes-last-update download)))\n                          time-diff))))\n      (setf (bytes-last-update download) (bytes-fetched download))\n      (setf (last-update download) new-time))))\n\n(declaim (ftype (function (quri:uri &key (:directory (or string pathname))\n                                    (:proxy (or quri:uri null))\n                                    (:cookies (or string null))))\n                resolve))\n(defun resolve (url &key\n                      (directory (default-download-directory))\n                      proxy\n                      cookies)\n  \"Start downloading URL concurrently and return a corresponding `download' object.\nIf DIRECTORY is nil, `default-download-directory' will be used.  COOKIES can\nspecify a cookie jar as a string, which is useful for authenticated downloads.\nPROXY is the full proxy address, e.g. \\\"socks5://127.0.0.1:9050\\\".\"\n  (unless *notifications*\n    (setf *notifications* (make-instance 'calispel:channel)))\n  (let ((download (cache :url url\n                         :directory (download-directory directory)\n                         :cookies cookies\n                         :proxy proxy)))\n    ;; TODO: We just use bt:make-thread, no need for a channel... Unless need to\n    ;; watch for unfinished downloads and warn the user before closing.\n    (bt:make-thread\n     (lambda ()\n       (fetch download))\n     :name \"download-manager\")\n    download))\n"
  },
  {
    "path": "libraries/download-manager/native.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n;;; Native Common Lisp download manager backend.\n(in-package :download-manager)\n\n(defmethod cache ((type (eql :url)) url &rest args)\n  (log:debug url args)\n  (apply #'locally-cache url args))\n\n(defun parse-cookie-jar-string (cookie-jar-string host path)\n  \"Host is for instance \\\"example.org\\\" and path is \\\"/foo/bar\\\".\"\n  (cl-cookie:make-cookie-jar\n   :cookies (mapcar (lambda (c)\n                      (cl-cookie:parse-set-cookie-header c host path))\n                    (cl-ppcre:split \" *; *\" cookie-jar-string))))\n\n(defun locally-cache (requested-url\n                      &key\n                      (directory (download-directory))\n                      cookies\n                      proxy)\n  (let* ((cookies-jar\n           (unless (str:emptyp cookies)\n             (parse-cookie-jar-string cookies (quri:uri-host requested-url)\n                                      (quri:uri-path requested-url)))))\n    (handler-case\n        (multiple-value-bind (stream status response-headers resolved-url)\n            (dex:get (quri:render-uri requested-url)\n                     :want-stream t :force-binary t :keep-alive nil\n                     :proxy (and proxy (quri:render-uri proxy)) :cookie-jar cookies-jar)\n          ;; TODO: Allow caller to set the target filename?\n          (let* ((file (merge-pathnames\n                        directory (extract-filename requested-url\n                                                    response-headers))))\n            ;; TODO: Touch file now to ensure uniqueness when actually downloading?\n            (make-instance 'download\n                           :requested-url requested-url\n                           :resolved-url (quri:uri resolved-url)\n                           :header response-headers\n                           :file file\n                           :status status\n                           :downstream stream)))\n      (error (c)\n        (error c)))))\n\n(defmethod fetch ((download download)\n                  &key (buffer-size 16)) ; Small for testing.\n  \"Return the number of bytes fetched.\"\n  (let* ((buffer (make-array buffer-size :element-type '(unsigned-byte 8)))\n         ;; Without `uiop:parse-native-namestring' `with-open-file' would fail\n         ;; if `temp-file' had a wildcard.\n         (temp-file (uiop:parse-native-namestring (temp-file download))))\n    (with-open-file (output temp-file\n                            :direction :output\n                            :if-exists :supersede\n                            :element-type '(unsigned-byte 8))\n      (log:info \"Downloading ~s~%  to ~s.\"\n                (or (ignore-errors (quri:url-decode (quri:render-uri (resolved-url download))))\n                    (quri:render-uri (resolved-url download)))\n                (namestring (file download)))\n      (loop :for byte-position = (read-sequence buffer (downstream download))\n\n            :do (update download)\n\n            :when (plusp byte-position)\n              :do (incf (bytes-fetched download) byte-position)\n\n            :if (plusp byte-position)\n              :do (write-sequence buffer output :end byte-position)\n            :else :return nil))\n    ;; TODO: Report something if bytes-fetched is not the same as bytes-total.\n    (setf (finished-p download) t)\n    (uiop:rename-file-overwriting-target temp-file\n                                         (ensure-unique-file\n                                          ;; Same as above for `parse-native-namestring'.\n                                          (uiop:parse-native-namestring\n                                           (namestring (file download)))))\n    (update download)\n    (bytes-fetched download)))\n\n(defun parse-http-header (header-entry)\n  \"Return the alist of key-value paris in HEADER-ENTRY.\"\n  (mapcar (lambda (key-value)\n            (cl-ppcre:split \"=\" key-value))\n          ;; TODO: Don't split at escaped or quoted semicolons?\n          (cl-ppcre:split \" *; *\" header-entry)))\n\n(defun normalize-filename (filename)\n  \"Remove surrounding quotes and return the basename as a string.\nReturn NIL if filename is not a string or a pathname.\"\n  (when (pathnamep filename)\n    (setf filename (namestring filename)))\n  (when (stringp filename)\n    (file-namestring (string-trim \"\\\"\" filename))))\n\n(defun extract-filename (url &optional headers)\n  \"Extract a filename to save the contents of a URL under.\"\n  ;; See https://en.wikipedia.org/wiki/List_of_HTTP_header_fields.\n  (or (normalize-filename\n       (second (assoc \"filename\"\n                      (parse-http-header\n                       (gethash \"content-disposition\" headers))\n                      :test #'string=)))\n      (let ((basename\n              (ignore-errors (file-namestring (quri:uri-path url)))))\n        (if (or (null basename) (string= \"\" basename))\n            \"index.html\"\n            basename))))\n"
  },
  {
    "path": "libraries/download-manager/package.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(uiop:define-package :download-manager\n  (:use :cl)\n  (:export #:init\n           #:*notifications*\n           #:default-download-directory\n           #:download\n           #:resolved-url\n           #:requested-url\n           #:header\n           #:file\n           #:filename\n           #:bytes-fetched\n           #:bytes-total\n           #:progress\n           #:finished-p\n           #:last-update-speed\n           #:cache\n           #:resolve))\n"
  },
  {
    "path": "libraries/nasdf/install.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nasdf)\n\n(export-always 'nasdf-file)\n(defclass nasdf-file (static-file)\n  ((if-does-not-exist\n    :initform :error\n    :initarg :if-does-not-exist\n    :type (member :error nil)\n    :documentation \"What to do when input file is missing:\n- `:error': Signal an error.\n- `nil': Skip it.\"))\n  (:documentation \"Component type for files to install.\"))\n(import 'nasdf-file :asdf-user)\n\n(export-always 'nasdf-binary-file)\n(defclass nasdf-binary-file (nasdf-file) ()\n  (:documentation \"Component type for executables to install.\"))\n(import 'nasdf-binary-file :asdf-user)\n\n(export-always 'nasdf-library-file)\n(defclass nasdf-library-file (nasdf-binary-file) ()\n  (:documentation \"Component type for libraries (shared objects) to install.\"))\n(import 'nasdf-library-file :asdf-user)\n\n(export-always 'nasdf-desktop-file)\n(defclass nasdf-desktop-file (nasdf-file) ()\n  (:documentation \"Component type for XDG .desktop files to install.\"))\n(import 'nasdf-desktop-file :asdf-user)\n\n(export-always 'nasdf-appdata-file)\n(defclass nasdf-appdata-file (nasdf-file) ()\n  (:documentation \"Component type for Appdata files to install.\"))\n(import 'nasdf-appdata-file :asdf-user)\n\n(export-always 'nasdf-icon-scalable-file)\n(defclass nasdf-icon-scalable-file (nasdf-file) ()\n  (:documentation \"Component type for the SVG icon.\"))\n(import 'nasdf-icon-scalable-file :asdf-user)\n\n(export-always 'nasdf-icon-directory)\n(defclass nasdf-icon-directory (nasdf-file)\n  ((asdf/interface::type :initform \"png\")) ; TODO: Is there a standard way to access the type?\n  (:documentation \"Component type for directory containing icon files to install.\nFile of type `type' are looked for.\nThe last number found in the file name is used to install the icon in the right directory.\"))\n(import 'nasdf-icon-directory :asdf-user)\n\n;; TODO: Is it possible to list all files targetted by an ASDF system?\n(export-always 'nasdf-source-directory)\n(defclass nasdf-source-directory (nasdf-file)\n  ((exclude-subpath\n    :initform '()\n    :type (or null (cons string *))\n    :accessor exclude-subpath\n    :initarg :exclude-subpath\n    :documentation \"Subpath to exclude from installation.\nSubpaths are relative to the component, so\n\n  (:nasdf-source-directory \\\"foo\\\" :exclude-subpath (\\\"bar\\\"))\n\nmeans that foo/bar is excluded, but foo/baz is not.\n\nIf subpath is a directory, then all its subpaths are excluded as well.\")\n   (exclude-types\n    :initform '(\"fasl\")\n    :type (or null (cons string *))\n    :accessor exclude-types\n    :initarg :exclude-types\n    :documentation \"Pattern of files to exclude when not using Git.\"))\n  (:documentation \"Directory of Common Lisp source files.\nSubdirectory is included.\nGit is used to list the tracked files -- untracked files will be ignored.\nIf Git is not found, fall back to copying everything except files of type in `exclude-types'.\n\nDestination directory is given by the `dest-source-dir' generic function.\"))\n(import 'nasdf-source-directory :asdf-user)\n\n(defun nil-pathname-p (pathname)\n  \"Return non-nil if PATHNAME is `*nil-pathname*' or nil.\"\n  (the (values boolean &optional)\n       (or (null pathname)\n           (pathname-equal pathname *nil-pathname*))))\n\n(defun basename (pathname)              ; From nfiles.\n  \"Return the basename, that is:\n- if it's a directory, the name of the directory,\n- if it's a file, the name of the file including its type (extension),\n- nil if it's a nil-pathname (#p\\\"\\\").\"\n  (if (nil-pathname-p pathname)\n      nil                               ; TODO: Shouldn't we return #p\"\" instead?\n      (first (last (pathname-directory\n                    ;; Ensure directory _after_ truenamizing, otherwise if\n                    ;; non-directory file exists it may not yield a directory.\n                    (ensure-directory-pathname\n                     (ensure-pathname pathname :truenamize t)))))))\n\n(defun path-from-env (environment-variable default)\n  (let ((env (getenv environment-variable)))\n    (if env\n        (ensure-directory-pathname env)\n        default)))\n\n(defun relative-path-from-env (environment-variable default)\n  (let ((env (getenv environment-variable)))\n    (if env\n        (relativize-pathname-directory (ensure-directory-pathname env))\n        default)))\n\n;; We use `defparameter' so that paths are re-computed on system reload.\n(export-always '*destdir*)\n(defparameter *destdir* (if (getenv \"DESTDIR\")\n                            (ensure-directory-pathname (getenv \"DESTDIR\"))\n                            #p\"/\"))\n\n(export-always '*prefix*)\n(defparameter *prefix* (merge-pathnames* (relative-path-from-env \"PREFIX\" #p\"usr/local/\")\n                                         *destdir*))\n\n(export-always '*datadir*)\n(defparameter *datadir* (path-from-env \"DATADIR\" (merge-pathnames* \"share/\" *prefix*)))\n(export-always '*bindir*)\n(defparameter *bindir* (path-from-env \"BINDIR\" (merge-pathnames* \"bin/\" *prefix*)))\n(export-always '*libdir*)\n(defparameter *libdir* (path-from-env \"LIBDIR\" (merge-pathnames* \"lib/\" *prefix*)))\n\n(export-always 'libdir)\n(defmethod libdir ((component nasdf-library-file))\n  (let ((name (primary-system-name (component-system component))))\n    (ensure-directory-pathname (merge-pathnames* name *libdir*))))\n\n(export-always '*dest-source-dir*)\n(defvar *dest-source-dir* (path-from-env \"NASDF_SOURCE_PATH\" *datadir*)\n  \"Root of where the source will be installed.\nFinal path is resolved in `dest-source-dir'.\")\n\n(export-always 'dest-source-dir)\n(defmethod dest-source-dir ((component nasdf-source-directory))\n  \"The directory into which the source is installed.\"\n  (let ((name (primary-system-name (component-system component))))\n    (ensure-directory-pathname\n     (merge-pathnames* name *dest-source-dir*))))\n\n(export-always '*chmod-program*)\n(defvar *chmod-program* \"chmod\")\n(export-always '*chmod-executable-arg*)\n(defvar *chmod-executable-arg* \"+x\")\n\n(export-always 'make-executable)\n(defun make-executable (file)\n  \"Does nothing if files does not exist.\"\n  ;; TODO: Use iolib/os:file-permissions instead of chmod?  Too verbose?\n  (when (file-exists-p file)\n    (run-program (list *chmod-program* *chmod-executable-arg* (native-namestring file)))))\n\n(export-always 'install-file)\n(defun install-file (file dest)\n  \"Like `copy-file' but ensures all parent directories are created if necessary.\"\n  (ensure-all-directories-exist\n   (list (directory-namestring dest)))\n  (copy-file file dest))\n\n(defmethod perform ((op compile-op) (c nasdf-file)) ; REVIEW: load-op?\n  (loop for input in (input-files op c)\n        for output in (output-files op c)\n        do (if (or (file-exists-p input)\n                   (slot-value c 'if-does-not-exist))\n               (progn\n                 (install-file input output)\n                 ;; (format *error-output* \"~&; installing file~%;  ~s~%; to~%;  ~s~%\" source dest) ; Too verbose?\n                 (logger \"installed ~s\" output))\n               (logger \"skipped ~s\" output)))\n  nil)\n\n(defmethod output-files ((op compile-op) (c nasdf-file))\n  (values (list (merge-pathnames* (pathname-name (component-name c))\n                                  *prefix*))\n          t))\n\n(defmethod output-files ((op compile-op) (c nasdf-binary-file))\n  (values (list (merge-pathnames* (basename (component-name c)) *bindir*))\n          t))\n\n(defmethod perform ((op compile-op) (c nasdf-binary-file))\n  (call-next-method)\n  (mapc #'make-executable (output-files op c))\n  nil)\n\n(defmethod output-files ((op compile-op) (c nasdf-library-file))\n  (values (list (merge-pathnames* (basename (component-name c)) (libdir c)))\n          t))\n\n(defmethod output-files ((op compile-op) (c nasdf-desktop-file))\n  (values (list (merge-pathnames* (merge-pathnames*\n                                   (basename (component-name c))\n                                   \"applications/\")\n                                  *datadir*))\n          t))\n\n(defmethod output-files ((op compile-op) (c nasdf-appdata-file))\n  (values (list (merge-pathnames* (merge-pathnames*\n                                   (basename (component-name c))\n                                   \"metainfo/\")\n                                  *datadir*))\n          t))\n\n(defmethod output-files ((op compile-op) (c nasdf-icon-scalable-file))\n  (values (list (merge-pathnames* (merge-pathnames*\n                                   (basename (component-name c))\n                                   \"icons/hicolor/scalable/apps/\")\n                                  *datadir*))\n          t))\n\n;; TODO Moving png icons to assets/icons would simplify their handling.\n(defun scan-last-number (path)\n  \"Return the last number found in PATH.\nReturn NIL is there is none.\"\n  (let ((result (block red\n                  (reduce (lambda (&optional next-char result)\n                            (if (parse-integer (string next-char) :junk-allowed t)\n                                (cons next-char result)\n                                (if result\n                                    (return-from red result)\n                                    result)))\n                          (native-namestring path)\n                          :initial-value '()\n                          :from-end t))))\n    (when result\n      (coerce result 'string))))\n\n(defmethod input-files ((op compile-op) (c nasdf-icon-directory))\n  \"Return all files of NASDF-ICON-DIRECTORY `type' in its directory.\nFile must contain a number in their path.\"\n  (let ((result (remove-if (complement #'scan-last-number)\n                           (directory-files (component-pathname c)\n                                            (strcat \"*.\" (file-type c))))))\n    (let* ((dimensions (mapcar #'scan-last-number result))\n           (dups (set-difference dimensions\n                                 (remove-duplicates dimensions)\n                                 :test 'string=)))\n      (if (= 0 (length dups))\n          result\n          (error \"Directory contains icons with duplicate dimensions: ~a\" dups)))))\n\n(defmethod output-files ((op compile-op) (c nasdf-icon-directory))\n  (let ((name (primary-system-name (component-system c))))\n    (values\n     (mapcar (lambda (path)\n               (let ((icon-size (scan-last-number path)) )\n                 (format nil \"~a/icons/hicolor/~ax~a/apps/~a.png\"\n                         *datadir*\n                         icon-size icon-size\n                         name)))\n             (input-files op c))\n     t)))\n\n(defun file-excluded-type (file exclude-types)\n  (member (pathname-type file) exclude-types :test 'equalp))\n\n(defun list-directory (directory &key exclude-subpath (exclude-types '(\"fasl\")))\n  (let ((result '()))\n    (collect-sub*directories\n     (ensure-directory-pathname directory)\n     (constantly t)\n     (lambda (dir)\n       (notany (lambda (exclusion)\n                 (string-suffix-p (basename dir) exclusion))\n               (mapcar #'basename exclude-subpath)))\n     (lambda (subdirectory)\n       (setf result (append result\n                            (remove-if\n                             (lambda (file) (file-excluded-type file exclude-types))\n                             (directory-files subdirectory))))))\n    result))\n\n(export-always 'copy-directory)\n(defun copy-directory (source destination &key exclude-subpath (exclude-types '(\"fasl\")) verbose-p) ; REVIEW: Unused, but seem quite useful.\n  \"Copy the content (the file tree) of SOURCE to DESTINATION.\"\n  (when verbose-p\n    (logger \"copy ~s/* inside ~s.\" source destination))\n  (mapc (lambda (file)\n          (unless (member (pathname-type file) exclude-types :test 'equalp)\n            (let ((destination-file\n                    (merge-pathnames*\n                     (subpathp file (ensure-directory-pathname source))\n                     (ensure-pathname destination :truenamize t :ensure-directory t))))\n              (install-file file destination-file))))\n        (list-directory source :exclude-subpath exclude-subpath\n                               :exclude-types exclude-types)))\n\n\n(defmethod input-files ((op compile-op) (component nasdf-source-directory))\n  \"Return all files of NASDF-SOURCE-DIRECTORY.\"\n  (with-current-directory ((system-source-directory (component-system component)))\n    (list-directory (component-pathname component)\n                    :exclude-subpath (exclude-subpath component)\n                    :exclude-types (exclude-types component))))\n\n(defmethod output-files ((op compile-op) (component nasdf-source-directory))\n  (let ((root (system-source-directory (component-system component))))\n    (values\n     (mapcar (lambda (path)\n               (merge-pathnames* (subpathp path root) (dest-source-dir component)))\n             (input-files op component))\n     t)))\n\n(export-always 'nasdf-source-file)\n(defclass nasdf-source-file (nasdf-file) ()\n  (:documentation \"Common Lisp source files.\n\nDestination directory is given by the `dest-source-dir' generic function.\"))\n(import 'nasdf-source-file :asdf-user)\n\n(defmethod dest-source-dir ((component nasdf-source-file)) ; TODO: Factor with other method?\n  \"The directory into which the source is installed.\"\n  (let ((name (primary-system-name (component-system component))))\n    (ensure-directory-pathname\n     (merge-pathnames* name *dest-source-dir*))))\n\n(defmethod output-files ((op compile-op) (c nasdf-source-file))\n  (values (list (merge-pathnames* (basename (component-name c)) (dest-source-dir c)))\n          t))\n"
  },
  {
    "path": "libraries/nasdf/log.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nasdf)\n\n;; TODO: Use full-fledged logging facility?\n;; Maybe we want to keep this dependency-free though...\n\n(defvar *log-prefix* \"; \")\n\n(defun logger (control-string &rest format-arguments)\n  \"Like `format' but assumes `*error-output*' as a stream and ensures fresh lines.\"\n  (let ((*standard-output* *error-output*))\n    (fresh-line)\n    (princ *log-prefix*)\n    (apply #'format t control-string format-arguments)\n    (fresh-line)))\n"
  },
  {
    "path": "libraries/nasdf/nasdf.asd",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(defsystem \"nasdf\"\n  :version \"0.1.8\"\n  :author \"Atlas Engineer LLC\"\n  :description \"ASDF helpers for system setup, testing and installation.\"\n  :license \"BSD 3-Clause\"\n  :components ((:file \"package\")\n               (:file \"log\")\n               (:file \"nasdf\")\n               (:file \"install\")\n               (:file \"systems\")\n               (:file \"tests\")))\n"
  },
  {
    "path": "libraries/nasdf/nasdf.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nasdf)\n\n(defmacro export-always (symbols &optional (package nil package-supplied?))\n  \"Like `export', but also evaluated at compile time.\"\n  `(eval-when (:compile-toplevel :load-toplevel :execute)\n     (export ,symbols ,@(and package-supplied? (list package)))))\n\n(defun env-true-p (env-variable)\n  (let ((value (getenv env-variable)))\n    (or (string-equal \"true\" value)\n        (string-equal \"yes\" value)\n        (string-equal \"on\" value)\n        (string-equal \"1\" value))))\n"
  },
  {
    "path": "libraries/nasdf/package.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n#+sb-package-locks\n(eval-when (:compile-toplevel :load-toplevel :execute)\n  (when (find-package :nasdf)\n    (sb-ext:unlock-package :nasdf)))\n\n(uiop:define-package :nasdf\n  (:use #:cl #:uiop #:asdf)\n  (:documentation \"ASDF helpers for system setup, testing and installation.\n\nA system that installs files:\n\n(defsystem \\\"my-project/install\\\"\n  :defsystem-depends-on (\\\"nasdf\\\")\n  :depends-on (alexandria)\n  :components ((:nasdf-desktop-file \\\"assets/my-project.desktop\\\")\n               (:nasdf-icon-directory \\\"assets/\\\")\n               (:nasdf-binary-file \\\"my-project\\\")\n               (:nasdf-library-file \\\"libraries/web-extensions/libmy.so\\\"\n                                   :if-does-not-exist nil)\n               (:nasdf-source-directory \\\"source\\\")\n               (:nasdf-source-directory \\\"nasdf\\\")\n               (:nasdf-source-directory \\\"libraries\\\"\n                :exclude-subpath (\\\"web-extensions\\\") ; Do not install this non-Lisp source.\n                :exclude-types (\\\"o\\\" \\\"c\\\" \\\"h\\\" ; C code and artifacts.\n                                    \\\"fasl\\\"))))\n\nA test system:\n\n(defsystem \\\"my-project/tests\\\"\n  :defsystem-depends-on (\\\"nasdf\\\")\n  :class :nasdf-test-system\n  :depends-on (alexandria lisp-unit2)\n  :components ((:file \\\"tests\\\"))\n  :test-suite-args (:package :my-project/tests))\"))\n\n#+sb-package-locks\n(sb-ext:lock-package :nasdf)\n"
  },
  {
    "path": "libraries/nasdf/readme.org",
    "content": "#+TITLE: NASDF\n\nNASDF is an ASDF extension providing utilities to ease system setup, testing and\ninstallation.\n\n* Features\n\n- Test suite helpers.\n- Installation helpers such as handling of icons or desktop files.\n\nSee [[file:package.lisp]] for more details.\n\n* Environment variables\n\nNASDF exposes the following environment variables for convenience:\n\n- =NASDF_SOURCE_PATH= :: See =nasdf:*dest-source-dir*=.\n- =NASDF_USE_LOGICAL_PATHS= :: Allow non-expanded logical pathnames in system\n  pathnames.\n  This is particularly useful when shipping the source.\n  Disable it if your tooling (e.g. SLIME) encounters issues to find the\n  definition of symbols.\n  See =asdf:nasdf-file=.\n\nAll boolean environment variables try to be smart enough to understand what you\nmean; for instance both =on= and =true= are valid values to enable the feature.\n"
  },
  {
    "path": "libraries/nasdf/systems.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nasdf)\n\n(export-always 'nasdf-system)\n(defclass nasdf-system (system) ()\n  (:documentation \"Extended ASDF system.\nIt enables features such as:\n- Togglable logical-pathnames depending on NASDF_USE_LOGICAL_PATHS.\n- Executable dependencies are made immutable for ASDF to prevent accidental reloads.\"))\n(import 'nasdf-system :asdf-user)\n\n(defmethod perform :before ((o image-op) (c nasdf-system))\n  \"Perform some last minute tweaks to the final image.\n\n- Register immutable systems to prevent compiled images from\ntrying to recompile the application and its dependencies.\nSee `:*immutable-systems*'.\n\n- If on SBCL, include `sb-sprof', the statistical profiler, since it's one of\nthe few modules that's not automatically included in the image.\"\n  #+sbcl\n  (require :sb-sprof)\n  (map () 'register-immutable-system (already-loaded-systems)))\n\n(defun set-new-translation (host logical-directory\n                            root-directory\n                            &optional (translated-directory (string-downcase (substitute #\\/ #\\; logical-directory))))\n  \"Add default translations for LOGICAL-DIRECTORY (e.g. \\\"foo;bar;\\\") in HOST.\nDefault translations:\n- FASL files are expanded as usual with `apply-output-translations' (should default to the ASDF cache).\n- Other files are expanded to their absolute location.\n\nThis effectively makes the logical pathname behave as if it had been a physical\npathname.\"\n  (let* ((logical-directory (if (string-suffix-p logical-directory \";\")\n                                logical-directory\n                                (strcat logical-directory \";\")))\n         (logical-path (strcat host \":\" logical-directory \"**;*.*.*\"))\n         (logical-fasl-path (strcat host \":\" logical-directory \"**;*.fasl.*\"))\n         (path-translation (ensure-pathname\n                            (subpathname* root-directory\n                                          translated-directory)\n                            :ensure-directory t\n                            :wilden t))\n         (fasl-translation (ensure-pathname\n                            (apply-output-translations\n                             (subpathname* root-directory\n                                           translated-directory))\n                            :wilden t)))\n    (if (ignore-errors (logical-pathname-translations host))\n        (flet ((set-alist (key value)\n                 (let ((pair (assoc key (logical-pathname-translations host)\n                                    :key #'namestring\n                                    :test #'string-equal)))\n                   (if pair\n                       (setf (rest pair) (list value))\n                       (push (list key value)\n                             (logical-pathname-translations host))))))\n          (set-alist logical-path path-translation)\n          (set-alist logical-fasl-path fasl-translation)\n          ;; Return this for consistency:\n          (list (list logical-fasl-path fasl-translation)\n                (list logical-path path-translation)))\n        (setf (logical-pathname-translations host)\n              ;; WARNING: fasl path must come first as it's more specific.\n              (list (list logical-fasl-path fasl-translation)\n                    (list logical-path path-translation))))))\n\n(defun logical-word-or-lose (word)      ; From  `sb-impl::logical-word-or-lose'.\n  (declare (string word))\n  (when (string= word \"\")\n    (error 'namestring-parse-error\n           :complaint \"Attempted to treat invalid logical hostname ~\n                       as a logical host:~%  ~S\"\n           :args (list word)\n           :namestring word :offset 0))\n  (let ((word (string-upcase word)))\n    (dotimes (i (length word))\n      (let ((ch (schar word i)))\n        (unless (and (typep ch 'standard-char)\n                     (or (alpha-char-p ch) (digit-char-p ch) (char= ch #\\-)))\n          (error 'namestring-parse-error\n                 :complaint \"logical namestring character which ~\n                             is not alphanumeric or hyphen:~%  ~S\"\n                 :args (list ch)\n                 :namestring word :offset i))))\n    (coerce word 'string)))\n\n(defun parse-logical-pathname (pathname)\n  \"Return two values:\n- the host;\n- the directory.\"\n  (let* ((name (namestring pathname))\n         (pos (position #\\: name)))\n    (when pos\n      (let ((host (subseq name 0 (position #\\: name))))\n        (when (ignore-errors (logical-word-or-lose host))\n          (values host\n                  (subseq name (1+ (position #\\: name)))))))))\n\n(defmethod component-pathname ((system nasdf-system))\n  \"If NASDF_USE_LOGICAL_PATHS environment variable is set, use logical path source\nlocation, otherwise use the translated path.\n\nTools such as Emacs (SLIME and SLY) may fail to make use of logical paths, say,\nto go to the compilation error location.\"\n  (let ((path (call-next-method)))\n    (when path\n      (let ((final-path (let ((host (parse-logical-pathname path)))\n                          (if host\n                              (progn\n                                (set-new-translation host\n                                                     (subseq (namestring path) (1+ (length host)))\n                                                     (system-source-directory system))\n                                ;; The #p reader macro expands to logical\n                                ;; pathnames only if the host is already\n                                ;; defined, which may not be the case at this\n                                ;; point, so we remake the pathname.\n                                (make-pathname :defaults path))\n                              path))))\n        (if (env-true-p \"NASDF_USE_LOGICAL_PATHS\")\n            final-path\n            (translate-logical-pathname final-path))))))\n\n(defclass nyxt-renderer-system (system) ()\n  (:documentation \"Specialized systems for Nyxt with renderer dependency.\nThe renderer is configured from NYXT_RENDERER or `*nyxt-renderer*'.\"))\n(import 'nyxt-renderer-system :asdf-user)\n\n(export '*nyxt-renderer*)\n(defvar *nyxt-renderer* (or (getenv \"NYXT_RENDERER\")\n                            \"gi-gtk\"))\n\n(defmethod component-depends-on ((o prepare-op) (c nyxt-renderer-system))\n  `((load-op ,(format nil \"nyxt/~a-application\" *nyxt-renderer*))\n    ,@(call-next-method)))\n"
  },
  {
    "path": "libraries/nasdf/tests.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nasdf)\n\n(export-always 'nasdf-test-system)\n(defclass nasdf-test-system (nasdf-system)\n  ((test-suite-args\n    :initform nil\n    :initarg :test-suite-args\n    :reader test-suite-args\n    :documentation \"Arguments passed to `lisp-unit2:run-tests'.\"))\n  (:documentation \"Specialized system that runs `lisp-unit2' test suites, whose parameters are\nspecified by the `test-suite-args' slot.\"))\n(import 'nasdf-test-system  :asdf-user)\n\n(defmethod perform ((op test-op) (c nasdf-test-system))\n  (destructuring-bind (&key package tags exclude-tags &allow-other-keys) (test-suite-args c)\n    (let ((output (symbol-call\n                   :lisp-unit2 :run-tests\n                   :package package\n                   :tags tags\n                   :run-contexts (find-symbol* :with-summary-context :lisp-unit2)))))))\n\n(export-always 'print-benchmark)\n(defun print-benchmark (benchmark-results)\n  (labels ((rat->float (num)\n             (if (integerp num) num (float num)))\n           (print-times (entry)\n             (let ((title (first entry))\n                   (attr (rest entry)))\n               (unless (or (member (symbol-name title)\n                                   '(\"RUN-TIME\" \"SYSTEM-RUN-TIME\"))\n                           (and (member (symbol-name title)\n                                        '(\"PAGE-FAULTS\" \"EVAL-CALLS\")\n                                        :test #'string=)\n                                (zerop (getf attr :average))))\n                 (format t \" ~a: ~,9t~a\"\n                         (string-downcase title)\n                         (rat->float (getf attr :average)))\n                 (format t \"~32,8t[~a, ~a]\"\n                         (rat->float (getf attr :minimum))\n                         (rat->float (getf attr :maximum)))\n                 (format t \"~56,8t(median ~a, deviation ~a, total ~a)\"\n                         (rat->float (getf attr :median))\n                         (rat->float (getf attr :deviation))\n                         (rat->float (getf attr :total)))\n                 (format t \"~%\")))))\n    (dolist (mark benchmark-results)\n      (format t \"~a (~a sample~:p):~%\" (first mark)\n              (getf (rest (second mark)) :samples))\n      (mapc #'print-times (rest mark)))))\n"
  },
  {
    "path": "libraries/password-manager/package.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(uiop:define-package :password\n  (:use :cl)\n  (:import-from :nclasses #:define-class)\n  (:import-from :serapeum #:export-always))\n\n(eval-when (:compile-toplevel :load-toplevel :execute)\n  (trivial-package-local-nicknames:add-package-local-nickname :sera :serapeum :password))\n"
  },
  {
    "path": "libraries/password-manager/password-keepassxc.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :password)\n\n(define-class keepassxc-interface (password-interface)\n  ((executable (pathname->string (sera:resolve-executable \"keepassxc-cli\")))\n   (password-file\n    :documentation \"The path to the KeePass password database.\")\n   (key-file\n    nil\n    :type (or null string pathname)\n    :documentation \"The key file for `password-file'.\")\n   (master-password\n    \"\"\n    :type string\n    :documentation \"The password to the `password-file'.\")\n   (yubikey-slot\n    nil\n    :documentation \"Yubikey slot to unlock the `password-file'.\")\n   (entries-cache\n    nil\n    :type list\n    :export nil\n    :documentation \"The cache to speed the entry listing up.\"))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t))\n\n(push 'keepassxc-interface *interfaces*)\n\n(defmethod list-passwords ((password-interface keepassxc-interface))\n  (or (entries-cache password-interface)\n      (let* ((st (make-string-input-stream (master-password password-interface)))\n             (output (execute password-interface\n                              (append (list \"ls\" \"-Rf\") ; Recursive flattened.\n                                      (when (key-file password-interface)\n                                        (list \"-k\" (uiop:native-namestring (key-file password-interface))))\n                                      (when (yubikey-slot password-interface)\n                                        (list \"-y\" (yubikey-slot password-interface)))\n                                      (list (password-file password-interface)))\n                              :input st :output '(:string :stripped t))))\n        (setf (entries-cache password-interface)\n              (remove-if (alexandria:curry #'str:ends-with-p \"/\") (sera:lines output))))))\n\n(defmethod clip-password ((password-interface keepassxc-interface) &key password-name service)\n  (declare (ignore service))\n  (with-input-from-string (st (master-password password-interface))\n    (execute password-interface\n             (append\n              (list \"clip\")\n              (when (key-file password-interface)\n                (list \"-k\" (uiop:native-namestring (key-file password-interface))))\n              (when (yubikey-slot password-interface)\n                (list \"-y\" (yubikey-slot password-interface)))\n              (list (password-file password-interface) password-name))\n             :input st\n             :wait-p nil)))\n\n(defmethod clip-username ((password-interface keepassxc-interface) &key password-name service)\n  (declare (ignore service))\n  (with-input-from-string (st (master-password password-interface))\n    (execute password-interface\n             (append (list \"clip\" \"--attribute\" \"username\")\n                     (when (key-file password-interface)\n                       (list \"-k\" (uiop:native-namestring (key-file password-interface))))\n                     (when (yubikey-slot password-interface)\n                       (list \"-y\" (yubikey-slot password-interface)))\n                     (list (password-file password-interface) password-name))\n             :input st\n             :wait-p nil)))\n\n(defmethod save-password ((password-interface keepassxc-interface)\n                          &key password-name username password service)\n  (declare (ignore service))\n  ;; This is to force entries re-fetching the next time we need passwords.\n  (setf (entries-cache password-interface) nil)\n  (with-input-from-string (st (format nil \"~a~C~a\"\n                                      (master-password password-interface)\n                                      #\\newline password))\n    (execute password-interface\n             (append (list \"add\" \"--username\" username\n                           \"--password-prompt\" (password-file password-interface))\n                     (when (key-file password-interface)\n                       (list \"-k\" (uiop:native-namestring (key-file password-interface))))\n                     (when (yubikey-slot password-interface)\n                       (list \"-y\" (yubikey-slot password-interface)))\n                     (list (if (str:emptyp password-name)\n                               \"--generate\"\n                               password-name)))\n             :input st)))\n\n(defmethod password-correct-p ((password-interface keepassxc-interface))\n  (handler-case\n      (list-passwords password-interface)\n    (uiop/run-program:subprocess-error ()\n      nil)))\n"
  },
  {
    "path": "libraries/password-manager/password-pass.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :password)\n\n(define-class password-store-interface (password-interface)\n  ((executable (pathname->string (sera:resolve-executable \"pass\")))\n   (sleep-timer (or (uiop:getenv \"PASSWORD_STORE_CLIP_TIME\") 45))\n   (password-directory (or (uiop:getenv \"PASSWORD_STORE_DIR\")\n                           (format nil \"~a/.password-store\" (uiop:getenv \"HOME\")))\n                       :type string\n                       :reader password-directory))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t))\n\n(push 'password-store-interface *interfaces*)\n\n(defmethod list-passwords ((password-interface password-store-interface))\n  (let ((directory (uiop:truename* (uiop:parse-native-namestring\n                                    (password-directory password-interface)))))\n    (when directory\n      ;; Special care must be taken for symlinks. Say `~/.password-store/work`\n      ;; points to `~/work/pass`, would we follow symlinks, we would not be able to\n      ;; truncate `~/.password-store/` in `~/work/pass/some/password.gpg`.  Because\n      ;; of this, we don't follow symlinks.\n      (let* ((raw-list (uiop:directory*\n                        ;; We truncate the root directory so that the password list\n                        ;; resembles the output from `pass list`. To do so, we\n                        ;; truncate `~/.password-store/` in the pathname strings of\n                        ;; the passwords.\n                        (format nil \"~a/**/*.gpg\" directory)))\n             (dir-length (length (namestring directory))))\n        (mapcar (lambda (x)\n                  (subseq (namestring x) dir-length (- (length (namestring x)) (length \".gpg\"))))\n                raw-list)))))\n\n(defmethod clip-password ((password-interface password-store-interface) &key password-name service)\n  (declare (ignore service))\n  (execute password-interface (list \"show\" \"--clip\" password-name)\n    ;; Outputting to string blocks `pass'.\n    :output 'nil))\n\n(defvar *multiline-separator* \": *\"\n  \"A regular expression to separate keys from values in the `pass' multiline format.\")\n\n(defun parse-multiline (content)\n  \"Return an alist of the multiple entries.\nAn entry is a sequence of\n- a key string,\n- a colon,\n- optional spaces,\n- a value string.\n\nThis is meant to handle the organization suggestion from\nhttp://www.passwordstore.org/#organization.\n\nLines that don't match the format are ignored.\nThe first line (the password) is skipped.\"\n  (unless (uiop:emptyp content)\n    (let ((lines (str:split (string #\\newline)\n                            content)))\n      (delete nil (mapcar (lambda (line)\n                            (let ((entry (ppcre:split *multiline-separator* line :limit 2)))\n                              (when (= 2 (length entry))\n                                entry)))\n                          ;; Skip first line to ignore password:\n                          (rest lines))))))\n\n(defvar *username-keys* '(\"login\" \"user\" \"username\")\n  \"A list of string keys used to find the `pass' username in `clip-username'.\")\n\n(defmethod clip-username ((password-interface password-store-interface) &key password-name service)\n  \"Save the multiline entry that's prefixed with on of the `*username-keys*' to clipboard.\nCase is ignored.\nThe prefix is discarded from the result and returned.\"\n  (declare (ignore service))\n  (when password-name\n    (let* ((content (execute password-interface (list \"show\" password-name)\n                      :output '(:string :stripped t)))\n           (entries (parse-multiline content))\n           (username-entry (when entries\n                             (some (lambda (key)\n                                     (find key entries :test #'string-equal :key #'first))\n                                   *username-keys*))))\n      (when username-entry\n        (trivial-clipboard:text (second username-entry))\n        (second username-entry)))))\n\n(defmethod save-password ((password-interface password-store-interface)\n                          &key password-name username password service)\n  (declare (ignore service))\n  (with-open-stream (st (make-string-input-stream (format nil \"~a~%username:~a\"\n                                                          password\n                                                          username)))\n    (execute password-interface (list \"insert\" \"--multiline\" password-name)\n      :input st))\n  (when (str:emptyp password)\n    (execute password-interface (list \"generate\" password-name))))\n\n(defmethod password-correct-p ((password-interface password-store-interface))\n  t)\n"
  },
  {
    "path": "libraries/password-manager/password-security.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :password)\n\n;;; Provide an interface to the command line \"security\" program used\n;;; on BSD and Darwin systems to interface with the system keychain\n\n(define-class security-interface (password-interface)\n  ((executable (pathname->string (sera:resolve-executable \"security\"))))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t))\n\n(push 'security-interface *interfaces*)\n\n(defmethod list-passwords ((password-interface security-interface))\n  (error \"Listing passwords not supported by the 'security' interface.\"))\n\n(defmethod clip-password ((password-interface security-interface) &key password-name service)\n  (clip-password-string password-interface\n   (str:replace-all\n    \"\\\"\" \"\"\n    (str:replace-first\n     \"password: \" \"\"\n     (nth-value 1\n                (execute password-interface\n                  (list \"find-internet-password\"\n                        \"-a\" password-name \"-s\" service \"-g\")\n                  :error-output '(:string :stripped t)))))))\n\n(defmethod clip-username ((password-interface security-interface) &key password-name service)\n  (declare (ignore password-name service))\n  (error \"Username clipping is not supported by security interface.\"))\n\n(defmethod password-correct-p ((password-interface security-interface)) t)\n"
  },
  {
    "path": "libraries/password-manager/password.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :password)\n\n(define-class password-interface ()\n  ((executable\n    nil\n    :type (or null string)\n    :documentation \"The program to query for password information.\")\n   (sleep-timer\n    15\n    :type alexandria:non-negative-real\n    :documentation \"The amount of time to sleep, in seconds.\"))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t))\n\n(export-always 'list-passwords)\n(defgeneric list-passwords (password-interface)\n  (:documentation \"Retrieve all available passwords.\"))\n\n(export-always 'clip-password)\n(defgeneric clip-password (password-interface &key password-name service)\n  (:documentation \"Retrieve specific password by name.\"))\n\n(export-always 'clip-username)\n(defgeneric clip-username (password-interface &key password-name service)\n  (:documentation \"Retrieve specific login by name of the password entry.\"))\n\n(export-always 'save-password)\n(defgeneric save-password (password-interface\n                           &key password-name username password service)\n  (:documentation \"Save password to database.\nIf PASSWORD-NAME is empty, then generate a new password.\"))\n\n(export-always 'password-correct-p)\n(defgeneric password-correct-p (password-interface)\n  (:documentation \"Return T if set password is correct, NIL otherwise.\"))\n\n(export-always 'complete-interface)\n(defgeneric complete-interface (password-interface)\n  (:method ((password-interface password-interface))\n    password-interface)\n  (:documentation \"Return the PASSWORD-INTERFACE with all the misfilled fields corrected.\"))\n\n(defgeneric execute (interface arguments &rest run-program-args &key wait-p &allow-other-keys)\n  (:method ((interface password-interface) (arguments list) &rest run-program-args &key (wait-p t) &allow-other-keys)\n    (apply (if wait-p #'uiop:run-program #'uiop:launch-program)\n           (append (uiop:ensure-list (executable interface)) arguments)\n           (alexandria:remove-from-plist run-program-args :wait-p)))\n  (:documentation \"Execute the command matching the INTERFACE, with ARGS.\n\n`uiop:run-program' is used underneath, with RUN-PROGRAM-ARGS being its\narguments.\n\nWhen the WAIT-P is NIL, `uiop:launch-program' is used instead of\n`uiop:run-program'.\"))\n\n(defun safe-clipboard-text ()\n  \"Return clipboard content, or \\\"\\\" if the content is not textual.\"\n  ;; xclip errors out when the clipboard contains non-text:\n  ;; https://github.com/astrand/xclip/issues/38#issuecomment-466625564.\n  (ignore-errors (trivial-clipboard:text)))\n\n;;; Prerequisite Functions\n(defmethod clip-password-string ((password-interface password-interface) pass)\n  (trivial-clipboard:text pass)\n  (bt:make-thread\n   (lambda ()\n     (sleep (sleep-timer password-interface))\n     (when (string= (safe-clipboard-text) pass)\n       ;; Reset the clipboard so that the user does not accidentally paste\n       ;; something else.\n       (trivial-clipboard:text \"\")))))\n\n\n;;; Commands to wrap together.\n(defun pathname->string (pathname)\n  \"Like `namestring' but return NIL if PATHNAME is NIL.\"\n  (when pathname\n    (namestring pathname)))\n\n(export-always '*interfaces*)\n(defvar *interfaces* '())\n"
  },
  {
    "path": "libraries/text-buffer/package.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(uiop:define-package :text-buffer\n  (:use :cl)\n  (:export #:text-buffer #:cursor))\n"
  },
  {
    "path": "libraries/text-buffer/text-buffer.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :text-buffer)\n\n(defclass text-buffer (cluffer-simple-line:line) ())\n\n(defclass cursor (cluffer-simple-line::right-sticky-cursor)\n  ((word-separation-characters\n    :accessor word-separation-characters\n    :initform '(\":\" \"/\" \".\" \" \" \" \"))))\n\n(defmethod string-representation ((buffer text-buffer))\n  (with-output-to-string (out)\n    (map nil (lambda (string)\n               (write-string string out))\n         (cluffer:items buffer))))\n\n(defmethod invisible-string-representation ((buffer text-buffer))\n  (make-string (cluffer:item-count buffer) :initial-element #\\*))\n\n(defmethod safe-forward ((cursor cursor))\n  (unless (cluffer:end-of-line-p cursor)\n    (cluffer:forward-item cursor)\n    (cluffer:item-before-cursor cursor)))\n\n(defmethod safe-backward ((cursor cursor))\n  (unless (cluffer:beginning-of-line-p cursor)\n    (cluffer:backward-item cursor)\n    (cluffer:item-after-cursor cursor)))\n\n(defmethod delete-item-forward ((cursor cursor))\n  (unless (cluffer:end-of-line-p cursor)\n    (cluffer:delete-item cursor)\n    t))\n\n(defmethod delete-item-backward ((cursor cursor))\n  (unless (cluffer:beginning-of-line-p cursor)\n    (cluffer:erase-item cursor)\n    t))\n\n(defmethod word-separation-chars-at-cursor-p ((cursor cursor) &key direction)\n  \"Return non-nil when `word-separation-characters' are found\nbefore/after the cursor position. You can specify to look before or\nafter the cursor by supplying :backward or :forward for the direction\nvalue.\"\n  (find (cond ((and (not (cluffer:beginning-of-line-p cursor))\n                    (eq direction :backward))\n               (cluffer:item-before-cursor cursor))\n              ((and (not (cluffer:end-of-line-p cursor))\n                    (eq direction :forward))\n               (cluffer:item-after-cursor cursor)))\n        (word-separation-characters cursor)\n        :test #'equal))\n\n(defmethod move-to-word ((cursor cursor) &key direction conservative-word-move)\n  \"Move the cursor to the boundary of a word and return its\nposition. A word is a string bounded by\n`word-separation-characters'. Specify a `direction' of :forward or\n:backward to change the movement.\"\n  (labels ((move-to-boundary (&key over-non-word-chars)\n             \"Move the cursor while it finds\n              `word-separation-characters' adjacent to it. When\n              `over-non-word-chars' is `t' move the cursor otherwise.\"\n             (loop named movement\n                   while (if over-non-word-chars\n                             (word-separation-chars-at-cursor-p cursor :direction direction)\n                             (not (word-separation-chars-at-cursor-p cursor :direction direction)))\n                   unless (if (eq direction :backward)\n                              (safe-backward cursor)\n                              (safe-forward cursor))\n                   do (return-from movement))))\n    (if (word-separation-chars-at-cursor-p cursor :direction direction)\n        (progn (move-to-boundary :over-non-word-chars t)\n               (when conservative-word-move (move-to-boundary)))\n        (move-to-boundary)))\n  (cluffer:cursor-position cursor))\n\n(defmethod move-forward-word ((cursor cursor) &key conservative-word-move)\n  (move-to-word cursor :direction :forward\n                       :conservative-word-move conservative-word-move))\n\n(defmethod move-backward-word ((cursor cursor) &key conservative-word-move)\n  (move-to-word cursor :direction :backward\n                       :conservative-word-move conservative-word-move))\n\n(defmethod delete-word ((cursor cursor) &key direction)\n  \"Delete characters until encountering the boundary of a\nword. Specify a `direction' as :forward or :backward.\"\n  (let ((start-cursor-position (cluffer:cursor-position cursor))\n        (end-cursor-position\n          (if (eq direction :backward)\n              (move-backward-word cursor :conservative-word-move t)\n              (move-forward-word cursor :conservative-word-move t))))\n    (dotimes (i (abs (- start-cursor-position end-cursor-position)))\n      (if (eq direction :backward)\n          (cluffer:delete-item cursor)\n          (cluffer:erase-item cursor)))))\n\n(defmethod delete-forward-word ((cursor cursor))\n  \"Delete characters forward until encountering the end of a word.\"\n  (delete-word cursor :direction :forward))\n\n(defmethod delete-backward-word ((cursor cursor))\n  \"Delete characters backward until encountering the end of a word.\"\n  (delete-word cursor :direction :backward))\n\n(defmethod kill-forward-line ((cursor cursor))\n  (loop while (delete-item-forward cursor)))\n\n(defmethod insert-string ((cursor cursor) string)\n  (loop for char across string do\n        (cluffer:insert-item cursor (string char))))\n\n(defmethod word-at-cursor ((cursor cursor))\n    \"Return word at cursor. If cursor is between two words, return the\nfirst one.\"\n  (let ((original-cursor-position (cluffer:cursor-position cursor)))\n    (move-backward-word cursor)\n    (let* ((delta (abs (- (cluffer:cursor-position cursor)\n                          (move-forward-word cursor))))\n           (word-at-cursor (reverse (apply #'concatenate 'string\n                                           (loop repeat delta\n                                                 collect (safe-backward cursor))))))\n      (setf (cluffer:cursor-position cursor) original-cursor-position)\n      word-at-cursor)))\n\n(defmethod replace-word-at-cursor ((cursor cursor) string)\n  (unless (uiop:emptyp (word-at-cursor cursor))\n    (move-backward-word cursor)\n    (delete-forward-word cursor))\n  (insert-string cursor string))\n\n(defmethod kill-line ((cursor cursor))\n  \"Kill the complete line.\"\n  (cluffer:beginning-of-line cursor)\n  (kill-forward-line cursor))\n\n(defun word-start (s position &optional (white-spaces '(#\\space #\\no-break_space)))\n  \"Return the index of the beginning word at POSITION in string S.\"\n  (apply #'max\n         (mapcar (lambda (char)\n                   (let ((pos (position char s\n                                        :end position\n                                        :from-end t)))\n                     (if pos\n                         (1+ pos)\n                         0)))\n                 white-spaces)))\n\n(defun word-end (s position &optional (white-spaces '(#\\space #\\no-break_space)))\n  \"Return the index of the end of the word at POSITION in string S.\"\n  (apply #'min\n         (mapcar (lambda (char)\n                   (or (position char s :start position)\n                       (length s)))\n                 white-spaces)))\n"
  },
  {
    "path": "libraries/theme/README.org",
    "content": "#+TITLE: Theme library for Nyxt\n#+PROPERTY: :results silent\n\n* Overview\n\nThis general purpose theme library provides the means to customize the colors\nand fonts of Nyxt's UI.  Besides exposing the set of tweakable options,\nopinionated defaults are provided.\n\nOwing to its flexibility, it can be used to theme other projects.\n\n** Palette's rationale\n\nThe following semantic color groups are defined:\n\n- ~background~ :: large surfaces.\n- ~primary~ :: primary interface elements.\n- ~secondary~ :: secondary or decorative interface elements.\n- ~action~ :: focus or call to action.\n- ~success~ :: successful completion, download, or evaluation.\n- ~warning~ :: errors, invalid operations, or consequential actions.\n- ~highlight~ :: eye-catching text highlighting.\n\nFor each group, 2 variation colors with more and less contrast are defined.\nThese are intended for cases of complex and overlapping\ninterfaces. E.g. ~background+~ and ~background-~.\n\nAdditionally, a foreground color is defined. E.g. ~on-background~.\n\nThis rationale is loosely based on [[https://m2.material.io/design/material-theming/implementing-your-theme.html][Google Material Design Guidelines]].\n\n** Example\n\n#+begin_src lisp\n(defvar my-theme\n  (make-instance 'theme:theme\n                 :background-color \"#F0F0F0\"\n                 :primary-color \"#595959\"\n                 :secondary-color \"#E6E6E6\"\n                 :action-color \"#5FCFFF\"\n                 :highlight-color \"#FAC090\"\n                 :success-color \"#AEE5BE\"\n                 :warning-color \"#F3B5AF\"\n                 :font-family \"Iosevka\"\n                 :monospace-font-family \"Iosevka\")\n  \"Example theme.\n\nWhen the values for on-colors are omitted, they're automatically set to either\nblack or white, according to what achieves a better contrast.\n\nWhen the values for color+ and color- are omitted, they fallback on regular\ncolor values.\n\nNote that not all semantic color groups need to be defined.\")\n\n;; Set the theme in Nyxt's config file\n(define-configuration browser ((theme my-theme)))\n#+end_src\n\n* Defaults\n\nWe suggest following the WCAG (Web Content Accessibility Guidelines) with\nrespect to contrast ratios.  The lowest standard (Level AA) requires a ratio of\n4.5:1, while a higher standard (Level AAA) requires 7:1.\n\nThe target contrast ratios for the default palette are summarized below.\n\n- Minus colors (e.g. ~background-~) :: >= 4.5:1\n- Regular colors (e.g. ~background~) :: >= 6.5:1\n- Plus colors (e.g. ~background+~) :: >= 8.5:1\n\n** Light theme\n\n| Color Name    | Value   | ~on-*~ Value | Contrast |\n|---------------+---------+--------------+----------|\n| ~background+~ | #FFFFFF | #000000      |    21.00 |\n| ~background~  | #F8F8F8 | #000000      |    19.77 |\n| ~background-~ | #ECECEC | #000000      |    17.78 |\n|---------------+---------+--------------+----------|\n| ~primary+~    | #474747 | #FFFFFF      |     9.29 |\n| ~primary~     | #555555 | #FFFFFF      |     7.46 |\n| ~primary-~    | #686868 | #FFFFFF      |     5.57 |\n|---------------+---------+--------------+----------|\n| ~secondary+~  | #BFBFBF | #000000      |    11.42 |\n| ~secondary~   | #A6A6A6 | #000000      |     8.63 |\n| ~secondary-~  | #909090 | #000000      |     6.58 |\n|---------------+---------+--------------+----------|\n| ~action+~     | #72CDFE | #000000      |    11.88 |\n| ~action~      | #37A8E4 | #000000      |     7.88 |\n| ~action-~     | #178DCC | #000000      |     5.72 |\n|---------------+---------+--------------+----------|\n| ~highlight+~  | #FFFA66 | #000000      |    19.12 |\n| ~highlight~   | #FCE304 | #000000      |    16.13 |\n| ~highlight-~  | #FCBA04 | #000000      |    12.16 |\n|---------------+---------+--------------+----------|\n| ~success+~    | #71FE7D | #000000      |    16.18 |\n| ~success~     | #8AEA92 | #000000      |    14.26 |\n| ~success-~    | #86D58E | #000000      |    11.92 |\n|---------------+---------+--------------+----------|\n| ~warning+~    | #88040D | #FFFFFF      |    10.14 |\n| ~warning~     | #AF1923 | #FFFFFF      |     7.03 |\n| ~warning-~    | #D2232E | #FFFFFF      |     5.22 |\n|---------------+---------+--------------+----------|\n#+TBLFM: $4='(contrast $2 $3);%.2f\n\n** Dark theme\n\n| Color Name    | Value   | ~on-*~ Value | Contrast |\n|---------------+---------+--------------+----------|\n| ~background+~ | #000000 | #FFFFFF      |    21.00 |\n| ~background~  | #121212 | #FFFFFF      |    18.73 |\n| ~background-~ | #333333 | #FFFFFF      |    12.63 |\n|---------------+---------+--------------+----------|\n| ~primary+~    | #EFA671 | #000000      |    10.36 |\n| ~primary~     | #E48D4E | #000000      |     8.22 |\n| ~primary-~    | #D7752F | #000000      |     6.47 |\n|---------------+---------+--------------+----------|\n| ~secondary+~  | #683008 | #FFFFFF      |    10.42 |\n| ~secondary~   | #844115 | #FFFFFF      |     7.64 |\n| ~secondary-~  | #9F592D | #FFFFFF      |     5.33 |\n|---------------+---------+--------------+----------|\n| ~action+~     | #481FA2 | #FFFFFF      |    10.54 |\n| ~action~      | #571FD2 | #FFFFFF      |     8.29 |\n| ~action-~     | #763DF2 | #FFFFFF      |     5.65 |\n|---------------+---------+--------------+----------|\n| ~highlight+~  | #FC83F2 | #000000      |     9.67 |\n| ~highlight~   | #F46DE8 | #000000      |     8.20 |\n| ~highlight-~  | #EA43DD | #000000      |     6.35 |\n|---------------+---------+--------------+----------|\n| ~success+~    | #87FCDF | #000000      |    17.02 |\n| ~success~     | #4CFBCF | #000000      |    16.01 |\n| ~success-~    | #05F4CD | #000000      |    14.83 |\n|---------------+---------+--------------+----------|\n| ~warning+~    | #FFD152 | #000000      |    14.49 |\n| ~warning~     | #FCBA04 | #000000      |    12.16 |\n| ~warning-~    | #FCA904 | #000000      |    10.82 |\n|---------------+---------+--------------+----------|\n#+TBLFM: $4='(contrast $2 $3);%.2f\n\n** Remarks\n\nThe minus and plus colors, when omitted, are set to the corresponding regular\ncolor.\n\n~on-colors~, when omitted, are set to either black or white, depending on which\nresults in a higher contrast ratio with its corresponding ~color~.\n\nOne might be tempted to think that ~on-colors~ are meant to be used solely for\ntext, but the principle holds more generality, when placing tiny elements over\nhuge surfaces.\n\nTake blue and yellow, colors that have a poor contrast ratio.  Consider that,\n(1) you inscribe a blue circle that covers most of the yellow square's surface,\nand (2) you were to draw a tiny blue cross on the same yellow background.  In\nsituation (1), you still properly discern the circle, whereas in (2) you'd\nstruggle to see it.\n\n* COMMENT TBLFM Code\nAuxiliary code to update contrast ratios on the tables shown in this document.\n\nInstructions:\n- Evaluate the cell below;\n- Run command =org-table-recalculate-buffer-tables=.\n\n#+begin_src emacs-lisp\n(defun contrast (c1 c2)\n  \"Measure WCAG contrast ratio between C1 and C2.\nC1 and C2 are color values written in hexadecimal RGB.\"\n  (cl-flet ((wcag-formula (hex)\n                          (cl-loop for k in '(0.2126 0.7152 0.0722)\n                                   for x in (color-name-to-rgb hex)\n                                   sum (* k (if (<= x 0.03928)\n                                                (/ x 12.92)\n                                              (expt (/ (+ x 0.055) 1.055) 2.4))))))\n    (let ((ct (/ (+ (wcag-formula c1) 0.05)\n                 (+ (wcag-formula c2) 0.05))))\n      (max ct (/ ct)))))\n#+end_src\n"
  },
  {
    "path": "libraries/theme/package.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(uiop:define-package :theme\n  (:use :cl)\n  (:shadow #:warning)\n  (:import-from :serapeum #:export-always)\n  (:import-from :nclasses #:define-class))\n"
  },
  {
    "path": "libraries/theme/tests/tests.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package cl-user)\n(uiop:define-package :theme/tests\n  (:use :cl :lisp-unit2)\n  (:import-from :theme))\n(in-package :theme/tests)\n\n(defvar *theme* (make-instance 'theme:theme\n                               :background-color \"white\"\n                               :action-color \"#37A8E4\")\n  \"Dummy theme for testing.\")\n\n(define-test fallback-colors ()\n  (assert-string= \"white\" (theme:background-color- *theme*))\n  (assert-string= \"white\" (theme:background-color+ *theme*))\n  (assert-string= \"black\" (theme:on-background-color *theme*))\n  (assert-false (theme:primary-color+ *theme*))\n  (assert-false (theme:primary-color *theme*))\n  (assert-false (theme:primary-color- *theme*))\n  (assert-false (theme:on-primary-color *theme*)))\n\n(define-test css-substitution ()\n  (assert-string= \"a{background-color:white;color:black;}h1{color:#37A8E4 !important;}\"\n                  (let ((lass:*pretty* nil))\n                    (theme:themed-css *theme*\n                      `(a\n                        :background-color ,theme:background-color\n                        :color ,theme:on-background-color)\n                      `(h1\n                        :color ,theme:action-color \"!important\")))))\n\n(defmethod assert-contrast ((theme theme:theme)\n                            &key (min-color+-contrast 8.5)\n                              (min-color-contrast 6.5)\n                              (min-color--contrast 4.5))\n  (macrolet ((assert-contrast-ratio (color1 color2 min-contrast)\n               `(assert-true (>= (theme:contrast-ratio ,color1 ,color2)\n                                 ,min-contrast))))\n    (multiple-value-bind (on-colors regular-colors minus-colors plus-colors)\n        (values-list\n         (theme:filter-palette (list (alexandria:curry #'uiop:string-prefix-p \"ON-\")\n                                     (alexandria:rcurry #'uiop:string-suffix-p \"COLOR\")\n                                     (alexandria:rcurry #'uiop:string-suffix-p \"COLOR-\")\n                                     (alexandria:rcurry #'uiop:string-suffix-p \"COLOR+\"))\n                               (theme:palette theme)))\n      (loop for on-color in on-colors\n            for regular-color in regular-colors\n            for minus-color in minus-colors\n            for plus-color in plus-colors\n            do (assert-contrast-ratio (funcall regular-color theme)\n                                      (funcall on-color theme)\n                                      min-color-contrast)\n            do (assert-contrast-ratio (funcall plus-color theme)\n                                      (funcall on-color theme)\n                                      min-color+-contrast)\n            do (assert-contrast-ratio (funcall minus-color theme)\n                                      (funcall on-color theme)\n                                      min-color--contrast)))))\n\n(define-test default-light-theme-contrast ()\n  (assert-contrast theme:+light-theme+))\n\n(define-test default-dark-theme-contrast ()\n  (assert-contrast theme:+dark-theme+))\n"
  },
  {
    "path": "libraries/theme/theme.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :theme)\n\n(define-class theme ()\n  ((background-color+\n    :documentation \"More contrasting variation of `background-color'.\")\n   (background-color\n    :documentation \"The background color of the theme.\")\n   (background-color-\n    :documentation \"Less contrasting variation of `background-color'.\")\n   (on-background-color\n    :documentation \"The color for elements/text in front of `background-color'.\")\n   (primary-color+\n    :documentation \"More contrasting variation of `primary-color'.\")\n   (primary-color\n    :documentation \"Primary UI element color.\")\n   (primary-color-\n    :documentation \"Less contrasting variation of `primary-color'.\")\n   (on-primary-color\n    :documentation \"The color for elements/text in front of `primary-color'.\")\n   (secondary-color+\n    :documentation \"More contrasting variation of `secondary-color'.\")\n   (secondary-color\n    :documentation \"Secondary UI element color.\")\n   (secondary-color-\n    :documentation \"Less contrasting variation of `secondary-color'.\")\n   (on-secondary-color\n    :documentation \"The color for elements/text in front of `secondary-color'.\")\n   (action-color+\n    :documentation \"More contrasting variation of `action-color'.\")\n   (action-color\n    :documentation \"Color for focused and important elements.\")\n   (action-color-\n    :documentation \"Less contrasting variation of `action-color'.\")\n   (on-action-color\n    :documentation \"The color for elements/text in front of `action-color'.\")\n   (highlight-color+\n    :documentation \"More contrasting variation of `highlight-color'.\")\n   (highlight-color\n    :documentation \"The color for elements requiring attention.\")\n   (highlight-color-\n    :documentation \"Less contrasting variation of `highlight-color'.\")\n   (on-highlight-color\n    :documentation \"The color for elements/text in front of `highlight-color'.\")\n   (success-color+\n    :documentation \"More contrasting variation of `success-color'.\")\n   (success-color\n    :documentation \"The color to express success.\")\n   (success-color-\n    :documentation \"Less contrasting variation of `success-color'.\")\n   (on-success-color\n    :documentation \"The color for elements/text in front of `success-color'.\")\n   (warning-color+\n    :documentation \"More contrasting variation of `warning-color'.\")\n   (warning-color\n    :documentation \"The color to express errors.\")\n   (warning-color-\n    :documentation \"Less contrasting variation of `warning-color'.\")\n   (on-warning-color\n    :documentation \"The color for elements/text in front of `warning-color'.\")\n   (font-family\n    \"Public Sans\"\n    :documentation \"The font family to use by default.\")\n   (monospace-font-family\n    \"DejaVu Sans Mono\"\n    :documentation \"The monospace font family to use by default.\"))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:export-predicate-name-p t))\n\n(defmethod initialize-instance :after ((theme theme) &key)\n  (multiple-value-bind (on-colors regular-colors minus-colors plus-colors)\n      (values-list\n       (filter-palette (list (alexandria:curry #'uiop:string-prefix-p \"ON-\")\n                             (alexandria:rcurry #'uiop:string-suffix-p \"COLOR\")\n                             (alexandria:rcurry #'uiop:string-suffix-p \"COLOR-\")\n                             (alexandria:rcurry #'uiop:string-suffix-p \"COLOR+\"))\n                       (palette theme)))\n    (loop for on-color in on-colors\n          for regular-color in regular-colors\n          for minus-color in minus-colors\n          for plus-color in plus-colors\n          do (when (and (not (slot-value theme on-color))\n                        (slot-value theme regular-color))\n               (setf (slot-value theme on-color)\n                     (contrasting-color (slot-value theme regular-color))))\n          do (when (and (not (slot-value theme minus-color))\n                        (slot-value theme regular-color))\n               (setf (slot-value theme minus-color)\n                     (slot-value theme regular-color)))\n          do (when (and (not (slot-value theme plus-color))\n                        (slot-value theme regular-color))\n               (setf (slot-value theme plus-color)\n                     (slot-value theme regular-color))))))\n\n(export-always 'dark-p)\n(defmethod dark-p ((theme theme))\n  \"Whether the theme is dark.\"\n  (when (string= \"white\" (contrasting-color (background-color theme))) t))\n\n(export-always 'palette)\n(defmethod palette ((theme theme))\n  \"Return color slots of THEME.\n\nExample that returns the palette's color values:\n(mapcar (alexandria:rcurry #'funcall +light-theme+)\n        (palette +light-theme+))\"\n  (serapeum:filter (alexandria:curry #'serapeum:string-contains-p \"COLOR\")\n                   (mopu:direct-slot-names theme)\n                   :key #'string))\n\n(export-always 'filter-palette)\n(defun filter-palette (preds palette)\n  \"Partition PALETTE according to PREDS.\"\n  (serapeum:partitions preds palette :key #'string))\n\n(export-always 'with-theme)\n(defmacro with-theme (theme-instance &body body)\n  \"Evaluate BODY with THEME and THEME's slots let-bound.\"\n  `(let ((theme ,theme-instance))\n     (with-slots ,(mopu:direct-slot-names 'theme) theme\n       ,@body)))\n\n(export-always 'themed-css)\n(defmacro themed-css (theme &body forms)\n  \"Generate CSS via lass FORMS styled according to THEME.\n\nExample:\n\n(themed-css (make-instance 'theme :background-color \\\"white\\\")\n           `(|h1,h2,h3,h4,h5,h6|\n             :border-style \\\"solid\\\"\n             :border-color ,theme:on-background-color)\n           `(p\n             :color ,(if (theme:dark-p theme:theme) \\\"yellow\\\" \\\"green\\\")))\"\n  `(with-theme ,theme (lass:compile-and-write ,@forms)))\n\n(export-always '+light-theme+)\n(defvar +light-theme+\n  (make-instance 'theme\n                 :background-color+ \"#FFFFFF\"\n                 :background-color  \"#F8F8F8\"\n                 :background-color- \"#ECECEC\"\n                 :primary-color+    \"#999999\"\n                 :primary-color     \"#686868\"\n                 :primary-color-    \"#555555\"\n                 :secondary-color+  \"#BFBFBF\"\n                 :secondary-color   \"#A6A6A6\"\n                 :secondary-color-  \"#909090\"\n                 :action-color+     \"#72CDFE\"\n                 :action-color      \"#37A8E4\"\n                 :action-color-     \"#178DCC\"\n                 :highlight-color+  \"#FFFA66\"\n                 :highlight-color   \"#FCE304\"\n                 :highlight-color-  \"#FCBA04\"\n                 :success-color+    \"#71FE7D\"\n                 :success-color     \"#8AEA92\"\n                 :success-color-    \"#86D58E\"\n                 :warning-color+    \"#88040D\"\n                 :warning-color     \"#AF1923\"\n                 :warning-color-    \"#D2232E\"))\n\n(export-always '+dark-theme+)\n(defvar +dark-theme+\n  (make-instance 'theme:theme\n                 :background-color- \"#3B4252\"\n                 :background-color \"#2E3440\"\n                 :background-color+ \"#434C5E\"\n                 :on-background-color \"#E5E9F0\"\n                 :primary-color- \"#5E81AC\"\n                 :primary-color \"#5E81AC\"\n                 :primary-color+ \"#81A1C1\"\n                 :on-primary-color \"#ECEFF4\"\n                 :secondary-color- \"#4C566A\"\n                 :secondary-color \"#4C566A\"\n                 :secondary-color+ \"#5E81AC\"\n                 :on-secondary-color \"#E5E9F0\"\n                 :action-color- \"#88C0D0\"\n                 :action-color \"#88C0D0\"\n                 :action-color+ \"#81A1C1\"\n                 :on-action-color \"#2E3440\"\n                 :success-color- \"#8FBCBB\"\n                 :success-color \"#8FBCBB\"\n                 :success-color+ \"#81A1C1\"\n                 :on-success-color \"#2E3440\"\n                 :highlight-color- \"#B48EAD\"\n                 :highlight-color \"#B48EAD\"\n                 :highlight-color+ \"#D8DEE9\"\n                 :on-highlight-color \"#2E3440\"\n                 :warning-color- \"#EBCB8B\"\n                 :warning-color \"#EBCB8B\"\n                 :warning-color+ \"#D08770\"\n                 :on-warning-color \"#2E3440\"))\n"
  },
  {
    "path": "libraries/theme/utilities.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :theme)\n\n(serapeum:-> relative-luminance ((or string integer cl-colors-ng:rgb cl-colors-ng:hsv))\n             real)\n(defun relative-luminance (color)\n  \"Compute relative luminance of COLOR.\"\n  ;; See https://www.w3.org/WAI/GL/wiki/Relative_luminance\n  (loop for const in '(0.2126 0.7152 0.0722)\n        for rgb-component in (list (cl-colors-ng:rgb-red (cl-colors-ng:as-rgb color))\n                                   (cl-colors-ng:rgb-green (cl-colors-ng:as-rgb color))\n                                   (cl-colors-ng:rgb-blue (cl-colors-ng:as-rgb color)))\n        sum (* const (if (<= rgb-component 0.04045)\n                         (/ rgb-component 12.92)\n                         (expt (/ (+ rgb-component 0.055) 1.055) 2.4)))))\n\n(serapeum:-> contrast-ratio ((or string integer cl-colors-ng:rgb cl-colors-ng:hsv)\n                             (or string integer cl-colors-ng:rgb cl-colors-ng:hsv))\n             (real 0 21)) ; Ratio between black and white.\n(export-always 'contrast-ratio)\n(defun contrast-ratio (color1 color2)\n  \"Compute contrast ratio between COLOR1 and COLOR2.\"\n  ;; See https://www.w3.org/WAI/GL/wiki/Contrast_ratio\n  (let ((ratio (/ (+ (relative-luminance color1) 0.05)\n                  (+ (relative-luminance color2) 0.05))))\n    (max ratio (/ ratio))))\n\n(serapeum:-> contrasting-color ((or string integer cl-colors-ng:rgb cl-colors-ng:hsv)) string)\n(export-always 'contrasting-color)\n(defun contrasting-color (color)\n  \"Determine whether black or white best contrasts with COLOR.\"\n  (if (>= (contrast-ratio color \"white\")\n          (contrast-ratio color \"black\"))\n      \"white\"\n      \"black\"))\n"
  },
  {
    "path": "libraries/user-interface/package.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(uiop:define-package :user-interface\n  (:use :cl))\n"
  },
  {
    "path": "libraries/user-interface/user-interface.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :user-interface)\n\n;; Taken from serapeum\n(defmacro export-always (symbols &optional (package nil package-supplied?))\n  \"Like `export', but also evaluated at compile time.\"\n  `(eval-when (:compile-toplevel :load-toplevel :execute)\n     (export ,symbols ,@(and package-supplied? (list package)))))\n\n(export-always 'id)\n(defvar *id* 0 \"Counter used to generate a unique ID.\")\n\n(defun unique-id ()\n  (format nil \"ui-element-~d\" (incf *id*)))\n\n(defgeneric to-html (object)\n  (:documentation \"The HTML representation of OBJECT.\nA form suitable to be compiled by Spinneret.\"))\n\n(export-always 'buffer)\n(defclass ui-element ()\n  ((id :accessor id)\n   (buffer :accessor buffer :initarg :buffer\n           :documentation \"Buffer where element is drawn.\")))\n\n(defmethod initialize-instance :after ((element ui-element) &key)\n  (setf (id element) (unique-id)))\n\n(export-always 'connect)\n(defmethod connect ((element ui-element) buffer)\n  (setf (buffer element) buffer))\n\n(export-always 'update)\n(defgeneric update (ui-element)\n  (:documentation \"Propagate changes to the buffer.\"))\n\n(export-always 'button)\n(export-always 'text)\n(export-always 'action)\n(defclass button (ui-element)\n  ((text :initform \"\" :initarg :text :accessor text)\n   (alt-text :initform \"\" :initarg :alt-text :accessor alt-text)\n   (action :initform \"\"  :initarg :action :accessor action)))\n\n(defmethod (setf text) :after (text (button button))\n  (declare (ignorable text))\n  (when (slot-boundp button 'buffer)\n    (update button)))\n\n(defmethod (setf action) :after (action (button button))\n  (declare (ignorable action))\n  (when (slot-boundp button 'buffer)\n    (update button)))\n\n(defmethod (setf alt-text) :after (text (button button))\n  (declare (ignorable text))\n  (when (slot-boundp button 'buffer)\n    (update button)))\n\n(export-always 'to-html)\n(defmethod to-html ((button button))\n  (spinneret:with-html\n      (:button :id (id button)\n               :class \"button\"\n               :title (alt-text button)\n               :onclick (action button)\n               (text button))))\n\n(export-always 'paragraph)\n(defclass paragraph (ui-element)\n  ((text :initform \"\" :initarg :text :accessor text)))\n\n(defmethod (setf text) :after (text (paragraph paragraph))\n  (declare (ignorable text))\n  (when (slot-boundp paragraph 'buffer)\n    (update paragraph)))\n\n(defmethod to-html ((paragraph paragraph))\n  (spinneret:with-html\n      (:p :id (id paragraph) (text paragraph))))\n\n(export-always 'progress-bar)\n(export-always 'percentage)\n(defclass progress-bar (ui-element)\n  ((percentage :initform 0\n               :initarg :percentage\n               :accessor percentage\n               :documentation \"The percentage the progress bar is\nfilled up, use a number between 0 and 100.\")))\n\n(defmethod to-html ((progress-bar progress-bar))\n  (spinneret:with-html\n      (:div :class \"progress-bar-base\"\n            (:div :class \"progress-bar-fill\"\n                  :id (id progress-bar)\n                  ;; empty string to force markup to make closing :div tag\n                  \"\"))))\n\n(defmethod (setf percentage) :after (percentage (progress-bar progress-bar))\n  (declare (ignorable percentage))\n  (when (slot-boundp progress-bar 'buffer)\n    (update progress-bar)))\n"
  },
  {
    "path": "licenses/ASSET-LICENSE",
    "content": "All non-source code assets are licensed as CC BY-SA.\n\nCreative Commons Deed\nThis is a human-readable summary of the full license below.\nYou are free:\n\nto Share— to copy, distribute and transmit the work, and\nto Remix— to adapt the work for any purpose, even commercially.\n\nUnder the following conditions:\n\nAttribution— You must attribute the work in the manner specified by the\nauthor or licensor (but not in any way that suggests that they endorse\nyou or your use of the work.)  Share Alike—If you alter, transform, or\nbuild upon this work, you may distribute the resulting work only under\nthe same, similar or a compatible license.  With the understanding\nthat:\n\nWaiver— Any of the above conditions can be waived if you get\npermission from the copyright holder.  Other Rights—In no way are any\nof the following rights affected by the license: your fair dealing or\nfair use rights; the author's moral rights; and rights other persons\nmay have either in the work itself or in how the work is used, such as\npublicity or privacy rights.\n\nNotice—For any reuse or distribution, you must make clear to others\nthe license terms of this work. The best way to do that is with a link\nto https://creativecommons.org/licenses/by-sa/3.0/\n"
  },
  {
    "path": "licenses/DejaVu Fonts License.txt",
    "content": "Fonts are (c) Bitstream (see below). DejaVu changes are in public domain.\nGlyphs imported from Arev fonts are (c) Tavmjong Bah (see below)\n\nBitstream Vera Fonts Copyright\n------------------------------\n\nCopyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is\na trademark of Bitstream, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof the fonts accompanying this license (\"Fonts\") and associated\ndocumentation files (the \"Font Software\"), to reproduce and distribute the\nFont Software, including without limitation the rights to use, copy, merge,\npublish, distribute, and/or sell copies of the Font Software, and to permit\npersons to whom the Font Software is furnished to do so, subject to the\nfollowing conditions:\n\nThe above copyright and trademark notices and this permission notice shall\nbe included in all copies of one or more of the Font Software typefaces.\n\nThe Font Software may be modified, altered, or added to, and in particular\nthe designs of glyphs or characters in the Fonts may be modified and\nadditional glyphs or characters may be added to the Fonts, only if the fonts\nare renamed to names not containing either the words \"Bitstream\" or the word\n\"Vera\".\n\nThis License becomes null and void to the extent applicable to Fonts or Font\nSoftware that has been modified and is distributed under the \"Bitstream\nVera\" names.\n\nThe Font Software may be sold as part of a larger software package but no\ncopy of one or more of the Font Software typefaces may be sold by itself.\n\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS\nOR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT,\nTRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME\nFOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING\nANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,\nWHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF\nTHE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE\nFONT SOFTWARE.\n\nExcept as contained in this notice, the names of Gnome, the Gnome\nFoundation, and Bitstream Inc., shall not be used in advertising or\notherwise to promote the sale, use or other dealings in this Font Software\nwithout prior written authorization from the Gnome Foundation or Bitstream\nInc., respectively. For further information, contact: fonts at gnome dot\norg. \n\nArev Fonts Copyright\n------------------------------\n\nCopyright (c) 2006 by Tavmjong Bah. All Rights Reserved.\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the fonts accompanying this license (\"Fonts\") and\nassociated documentation files (the \"Font Software\"), to reproduce\nand distribute the modifications to the Bitstream Vera Font Software,\nincluding without limitation the rights to use, copy, merge, publish,\ndistribute, and/or sell copies of the Font Software, and to permit\npersons to whom the Font Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright and trademark notices and this permission notice\nshall be included in all copies of one or more of the Font Software\ntypefaces.\n\nThe Font Software may be modified, altered, or added to, and in\nparticular the designs of glyphs or characters in the Fonts may be\nmodified and additional glyphs or characters may be added to the\nFonts, only if the fonts are renamed to names not containing either\nthe words \"Tavmjong Bah\" or the word \"Arev\".\n\nThis License becomes null and void to the extent applicable to Fonts\nor Font Software that has been modified and is distributed under the \n\"Tavmjong Bah Arev\" names.\n\nThe Font Software may be sold as part of a larger software package but\nno copy of one or more of the Font Software typefaces may be sold by\nitself.\n\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL\nTAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n\nExcept as contained in this notice, the name of Tavmjong Bah shall not\nbe used in advertising or otherwise to promote the sale, use or other\ndealings in this Font Software without prior written authorization\nfrom Tavmjong Bah. For further information, contact: tavmjong @ free\n. fr."
  },
  {
    "path": "licenses/SOURCE-LICENSE",
    "content": "BSD 3-Clause License\n\nCopyright (c) 2017-2025, Atlas Engineer LLC.\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n  list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n  this list of conditions and the following disclaimer in the documentation\n  and/or other materials provided with the distribution.\n\n* Neither the name of the copyright holder nor the names of its\n  contributors may be used to endorse or promote products derived from\n  this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "makefile",
    "content": "# SPDX-FileCopyrightText: Atlas Engineer LLC\n# SPDX-License-Identifier: BSD-3-Clause\n\n## Use Bourne shell syntax.\nSHELL = /bin/sh\nUNAME := $(shell uname)\n\nLISP ?= sbcl\nSBCL_FLAGS =\nifeq ($(LISP), sbcl)\n\tSBCL_FLAGS=--dynamic-space-size $(shell sbcl --noinform --no-userinit --non-interactive --eval '(prin1 (max 3072 (/ (sb-ext:dynamic-space-size) 1024 1024)))' --quit | tail -1)\nendif\n\nLISP_FLAGS ?= $(SBCL_FLAGS) --no-userinit --non-interactive\n\nNYXT_SUBMODULES ?= true\nNYXT_RENDERER ?= electron\nNASDF_USE_LOGICAL_PATHS ?= true\nNODE_SETUP ?= true\n\nexport NYXT_SUBMODULES\nexport NYXT_RENDERER\nexport NASDF_USE_LOGICAL_PATHS\nexport NODE_SETUP\n\n.PHONY: help\nhelp:\n\t@cat INSTALL\n\nmakefile_dir := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))\n\nifeq ($(NYXT_SUBMODULES),true)\n\tCL_SOURCE_REGISTRY = $(makefile_dir)_build//\n\texport CL_SOURCE_REGISTRY\nendif\n\nlisp_eval:=$(LISP) $(LISP_FLAGS) \\\n\t--eval '(require \"asdf\")' \\\n\t--eval '(asdf:load-asd \"$(makefile_dir)/libraries/nasdf/nasdf.asd\")' \\\n\t--eval '(asdf:load-asd \"$(makefile_dir)/nyxt.asd\")' \\\n\t--eval\n\nlisp_quit:=--eval '(uiop:quit 0 \\#+bsd nil)'\n\n## asdf:load-system is a bit slow on :nyxt/$(NYXT_RENDERER)-application, so we\n## keep a Make dependency on the Lisp files.\nlisp_files := nyxt.asd $(shell find . -type f -name '*.lisp')\nnyxt: $(lisp_files)\n\tif [ \"$(NYXT_RENDERER)\" = \"electron\" ] && \\\n\t   [ \"$(NODE_SETUP)\" = \"true\" ] && \\\n\t   [ \"$(NYXT_SUBMODULES)\" = \"true\" ]; then \\\n\t\t$(MAKE) -C $(makefile_dir)_build/cl-electron install; \\\n\tfi\n\t$(lisp_eval) '(asdf:load-system :nyxt/$(NYXT_RENDERER)-application)' \\\n\t\t--eval '(asdf:make :nyxt/$(NYXT_RENDERER)-application)' \\\n\t\t$(lisp_quit) || (printf \"\\n%s\\n%s\\n\" \"Compilation failed, see the above stacktrace.\" && exit 1)\n\n.PHONY: all\nall: nyxt\n\n.PHONY: doc\ndoc:\n\t$(lisp_eval) '(asdf:load-system :nyxt)' \\\n\t\t--eval '(asdf:load-system :nyxt/documentation)' $(lisp_quit)\n\n.PHONY: check\ncheck:\n\t$(lisp_eval) '(asdf:test-system :nyxt)'\n\n.PHONY: clean\nclean:\n\trm nyxt\n"
  },
  {
    "path": "nyxt.asd",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n#-asdf3.1 (error \"Nyxt requires ASDF 3.1.2\")\n\n;; WARNING: We _must_ declare the translation host or else ASDF won't recognize\n;; the pathnames as logical-pathnames, thus returning the system directory\n;; instead.\n(setf (logical-pathname-translations \"NYXT\") nil)\n\n(defsystem \"nyxt\"\n  :defsystem-depends-on (\"nasdf\")\n  :class :nasdf-system\n  :version \"4\"                          ; 4.0.0-pre-release-3\n  :author \"Atlas Engineer LLC\"\n  :homepage \"https://nyxt-browser.com\"\n  :description \"Extensible web browser in Common Lisp\"\n  :license \"BSD 3-Clause\"\n  :depends-on (alexandria\n               bordeaux-threads\n               calispel\n               cl-base64\n               cl-colors-ng\n               cl-gopher\n               cl-json\n               cl-ppcre\n               cl-ppcre-unicode\n               cl-prevalence\n               cl-qrencode\n               cl-tld\n               closer-mop\n               clss\n               dexador\n               enchant\n               flexi-streams\n               iolib\n               iolib/os\n               lass\n               local-time\n               log4cl\n               lparallel\n               nclasses\n               nfiles\n               nhooks\n               njson/cl-json\n               nkeymaps\n               nsymbols/star\n               parenscript\n               phos\n               plump\n               prompter\n               py-configparser\n               quri\n               serapeum\n               spinneret\n               sqlite\n               str\n               trivia\n               trivial-arguments\n               trivial-clipboard\n               trivial-package-local-nicknames\n               trivial-types\n               unix-opts\n               ;; Local systems:\n               nyxt/analysis\n               nyxt/download-manager\n               nyxt/password-manager\n               nyxt/text-buffer\n               nyxt/theme\n               nyxt/user-interface)\n  :pathname #p\"NYXT:source;\"\n  :components ((:file \"utilities\")\n               (:file \"types\")\n               (:file \"package\" :depends-on (\"utilities\" \"types\"))\n               (:module \"Utilities\"\n                :pathname \"\"\n                :depends-on (\"package\")\n                :components\n                ((:file \"time\")\n                 (:file \"keyscheme\")\n                 (:file \"conditions\")\n                 (:file \"user-interface\")))\n               (:module \"Core\"\n                :pathname \"\"\n                :depends-on (\"Utilities\")\n                :serial t\n                :components\n                ((:file \"renderer\")\n                 (:file \"global\")\n                 (:file \"concurrency\")\n                 (:file \"user-files\")\n                 (:file \"user-classes\")\n                 (:file \"configuration\")\n                 (:file \"parenscript-macro\")\n                 (:file \"message\")\n                 (:file \"command\")\n                 (:file \"renderer-script\")\n                 (:file \"urls\")\n                 (:file \"inspector\")\n                 (:file \"dom\")\n                 (:file \"search-engine\")\n                 (:file \"buffer\")\n                 (:file \"window\")\n                 (:file \"mode\")\n                 (:file \"history\")\n                 (:file \"spinneret-tags\")\n                 (:file \"browser\")\n                 (:file \"foreign-interface\")\n                 (:file \"clipboard\")\n                 (:file \"color\")\n                 (:file \"input\")\n                 (:file \"prompt-buffer\")\n                 (:file \"command-commands\")\n                 (:file \"recent-buffers\")\n                 (:file \"external-editor\")))\n               (:module \"Core modes\"\n                :pathname \"mode\"\n                :depends-on (\"Core\")\n                :components\n                ((:file \"input-edit\")\n                 (:file \"buffer-listing\")\n                 (:file \"message\")\n                 (:file \"passthrough\")\n                 (:file \"document\" :depends-on (\"passthrough\"))\n                 (:file \"hint\" :depends-on (\"document\"))\n                 (:file \"search-buffer\")\n                 (:file \"spell-check\" :depends-on (\"document\"))\n                 (:file \"help\" :depends-on (\"document\" \"search-buffer\"))\n                 (:file \"history\")\n                 (:file \"keyscheme\")\n                 (:file \"process\")))\n               (:file \"describe\" :depends-on (\"Core modes\"))\n               (:module \"Prompter modes\"\n                :pathname \"mode\"\n                :depends-on (\"describe\" \"Core modes\")\n                :components\n                ((:file \"prompt-buffer\")\n                 (:file \"hint-prompt-buffer\" :depends-on (\"prompt-buffer\"))\n                 (:file \"file-manager\" :depends-on (\"prompt-buffer\"))\n                 (:file \"download\" :depends-on (\"file-manager\"))))\n               (:file \"mode/base\" :depends-on (\"Core modes\"))\n               (:file \"status\" :depends-on (\"Core\"))\n               (:module \"Help\"\n                :pathname \"\"\n                :depends-on (\"Core modes\" \"Modes\")\n                :components\n                ((:file \"help\")\n                 (:file \"about\")\n                 (:file \"tutorial\")))\n               (:file \"configuration-commands\" :depends-on (\"Help\"))\n               (:file \"start\" :depends-on (\"configuration-commands\"))\n               (:file \"manual\" :depends-on (\"configuration-commands\"))\n               (:module \"Modes\"\n                :pathname \"mode\"\n                :depends-on (\"Core modes\")\n                :components\n                ((:file \"annotate\")\n                 (:file \"autofill\")\n                 (:file \"bookmark\")\n                 (:file \"bookmarklets\")\n                 (:file \"cruise-control\" :depends-on (\"repeat\"))\n                 (:file \"emacs\")\n                 (:file \"expedition\")\n                 (:file \"history-migration\")\n                 (:file \"macro-edit\")\n                 (:file \"no-sound\")\n                 (:file \"password\")\n                 (:file \"reading-line\")\n                 (:file \"repeat\")\n                 (:file \"small-web\")\n                 (:file \"style\" :depends-on (\"bookmarklets\"))\n                 (:file \"visual\")\n                 (:file \"vi\")\n                 (:file \"watch\"))))\n  :in-order-to ((test-op (test-op \"nyxt/tests\")\n                         ;; Dumping the manual may catch errors.\n                         (compile-op \"nyxt/documentation\")\n                         ;; Subsystems:\n                         (test-op \"nyxt/analysis\")\n                         (test-op \"nyxt/theme\"))))\n\n(defsystem \"nyxt/tests\"\n  :defsystem-depends-on (\"nasdf\")\n  :class :nasdf-test-system\n  :depends-on (nyxt lisp-unit2)\n  :pathname #p\"NYXT:tests;\"\n  :components ((:file \"package\")\n               (:file \"define-configuration\")\n               (:file \"prompt-buffer\")\n               (:file \"urls\")\n               (:file \"user-script-parsing\")\n               (:file \"mode\")\n               (:module \"Modes\"\n                :pathname \"mode\"\n                :components\n                ((:file \"autofill\")\n                 (:file \"annotate\")\n                 (:file \"base\")\n                 (:file \"blocker\")\n                 (:file \"bookmark\")\n                 (:file \"bookmarklets\")\n                 (:file \"buffer-listing\")\n                 (:file \"certificate-exception\")\n                 (:file \"cruise-control\")\n                 (:file \"document\")\n                 (:file \"download\")\n                 (:file \"emacs\")\n                 (:file \"expedition\")\n                 (:file \"file-manager\")\n                 (:file \"force-https\")\n                 (:file \"help\")\n                 (:file \"hint-prompt-buffer\")\n                 (:file \"hint\")\n                 (:file \"history\")\n                 (:file \"input-edit\")\n                 (:file \"keyscheme\")\n                 (:file \"macro-edit\")\n                 (:file \"message\")\n                 (:file \"no-image\")\n                 (:file \"no-script\")\n                 (:file \"no-sound\")\n                 (:file \"no-webgl\")\n                 (:file \"passthrough\")\n                 (:file \"password\")\n                 (:file \"process\")\n                 (:file \"prompt-buffer\")\n                 (:file \"proxy\")\n                 (:file \"reading-line\")\n                 ;; TODO Fix repeat-mode architecture.  Visit the file below for\n                 ;; more information.\n                 ;; (:file \"repeat\")\n                 (:file \"search-buffer\")\n                 (:file \"small-web\")\n                 (:file \"spell-check\")\n                 (:file \"style\")\n                 (:file \"vi\")\n                 ;; TODO Fix visual-mode architecture.  Visit the file below for\n                 ;; more information.\n                 ;; (:file \"visual\")\n                 (:file \"user-script\")\n                 (:file \"watch\"))))\n    :test-suite-args (:package :nyxt/tests))\n\n(defsystem \"nyxt/benchmarks\"\n  :defsystem-depends-on (\"nasdf\")\n  :class :nasdf-system\n  :depends-on (nyxt alexandria trivial-benchmark)\n  :pathname #p\"NYXT:tests;benchmarks;\"\n  :components ((:file \"package\")\n               (:file \"prompter\"))\n  :perform (test-op (op c)\n                    (eval-input\n                     \"(nasdf:print-benchmark\n                       (alexandria:hash-table-alist\n                        (benchmark:run-package-benchmarks :package :nyxt/benchmarks\n                                                          :verbose t)))\")))\n\n(defsystem \"nyxt/documentation\"\n  :depends-on (nyxt)\n  :perform (compile-op (o c)\n                       (with-open-file (out \"manual.html\" :direction :output :if-exists :supersede)\n                         (write-string (symbol-call :nyxt :manual-html) out)\n                         (format *error-output* \"Manual dumped to ~s.~&\" \"manual.html\"))))\n\n(defsystem \"nyxt/gtk\"\n  :defsystem-depends-on (\"nasdf\")\n  :class :nasdf-system\n  :depends-on (nyxt cl-webkit2)\n  :pathname #p\"NYXT:source;\"\n  :components ((:file \"renderer/gtk\")\n               ;; TODO: Port to other renderers.\n               (:file \"mode/blocker\")\n               (:file \"mode/certificate-exception\")\n               (:file \"mode/force-https\")\n               (:file \"mode/user-script\")\n               (:file \"mode/no-image\")\n               (:file \"mode/no-script\")\n               (:file \"mode/no-webgl\")\n               (:file \"mode/proxy\")))\n\n(defsystem \"nyxt/gi-gtk\"\n  :defsystem-depends-on (\"nasdf\")\n  :class :nasdf-system\n  :depends-on (nyxt/gtk cl-gobject-introspection)\n  :pathname #p\"NYXT:source;renderer;\"\n  :components ((:file \"gi-gtk\"))\n  :in-order-to ((test-op (test-op \"nyxt/gi-gtk/tests\")\n                         (test-op \"nyxt/tests\")\n                         ;; Dumping the manual may catch errors.\n                         (compile-op \"nyxt/documentation\")\n                         ;; Subsystems:\n                         (test-op \"nyxt/analysis\")\n                         (test-op \"nyxt/theme\"))))\n\n(defsystem \"nyxt/gi-gtk/tests\"\n  :defsystem-depends-on (\"nasdf\")\n  :class :nasdf-test-system\n  :depends-on (nyxt/gi-gtk lisp-unit2)\n  :pathname #p\"NYXT:tests;renderer;\"\n  :serial t\n  :components ((:file \"package\")\n               (:file \"set-url\")\n               (:file \"custom-schemes\")\n               (:file \"search-buffer\"))\n  :test-suite-args (:package :nyxt/tests/renderer))\n\n(defsystem \"nyxt/electron\"\n  :depends-on (nyxt cl-electron)\n  :pathname #p\"NYXT:source;renderer;\"\n  :components ((:file \"electron\")))\n\n;; We should not set the build-pathname in systems that have a component.\n;; Indeed, when an external program (like Guix) builds components, it needs to\n;; know the name of the output.  But ASDF/SYSTEM::COMPONENT-BUILD-PATHNAME is\n;; non-exported so the only reliable way to know the build pathname is to use\n;; the default.\n;;\n;; The workaround is to set a new dummy system of which the sole purpose is to\n;; produce the desired binary.\n\n(defsystem \"nyxt/gtk-application\"\n  :defsystem-depends-on (\"nasdf\")\n  :class :nasdf-system\n  :depends-on (nyxt/gtk)\n  :build-operation \"program-op\"\n  :build-pathname \"nyxt\"\n  :entry-point \"nyxt:entry-point\")\n\n(defsystem \"nyxt/gi-gtk-application\"\n  :defsystem-depends-on (\"nasdf\")\n  :class :nasdf-system\n  :depends-on (nyxt/gi-gtk)\n  :build-operation \"program-op\"\n  :build-pathname \"nyxt\"\n  :entry-point \"nyxt:entry-point\")\n\n(defsystem \"nyxt/electron-application\"\n  :defsystem-depends-on (\"nasdf\")\n  :class :nasdf-system\n  :depends-on (nyxt/electron)\n  :build-operation \"program-op\"\n  :build-pathname \"nyxt\"\n  :entry-point \"nyxt:entry-point\")\n\n(defsystem \"nyxt/install\"\n  :defsystem-depends-on (\"nasdf\")\n  :class :nyxt-renderer-system\n  :components ((:nasdf-desktop-file \"assets/nyxt.desktop\")\n               (:nasdf-appdata-file \"assets/nyxt.metainfo.xml\")\n               (:nasdf-icon-scalable-file \"assets/glyphs/nyxt.svg\")\n               (:nasdf-icon-directory \"assets/\")\n               (:nasdf-binary-file \"nyxt\")\n               (:nasdf-source-file \"nyxt.asd\")\n               (:nasdf-source-directory \"source\")\n               (:nasdf-source-directory \"nasdf\")\n               (:nasdf-source-directory \"libraries\"\n                :exclude-types (\"o\" \"c\" \"h\" ; C code and artifacts.\n                                    \"fasl\"))))\n\n;; Library subsystems:\n\n(defsystem \"nyxt/download-manager\"\n  :defsystem-depends-on (\"nasdf\")\n  :class :nasdf-system\n  :depends-on (calispel\n               cl-ppcre\n               dexador\n               log4cl\n               quri\n               str)\n  :pathname #p\"NYXT:libraries;download-manager;\"\n  :components ((:file \"package\")\n               (:file \"engine\")\n               (:file \"native\")))\n\n(defsystem \"nyxt/analysis\"\n  :defsystem-depends-on (\"nasdf\")\n  :class :nasdf-system\n  :depends-on (alexandria\n               cl-ppcre\n               serapeum\n               str)\n  :pathname #p\"NYXT:libraries;analysis;\"\n  :components ((:file \"package\")\n               (:file \"composite-sequence\")\n               (:file \"data\")\n               (:file \"stem\")\n               (:file \"tokenize\")\n               (:file \"analysis\")\n               (:file \"document-vector\")\n               (:file \"text-rank\")\n               (:file \"dbscan\")\n               (:file \"section\"))\n  :in-order-to ((test-op (test-op \"nyxt/analysis/tests\"))))\n\n(defsystem \"nyxt/analysis/tests\"\n  :defsystem-depends-on (\"nasdf\")\n  :class :nasdf-test-system\n  :depends-on (nyxt/analysis lisp-unit2)\n  :pathname #p\"NYXT:libraries;analysis;tests;\"\n  :components ((:file \"tests\"))\n  :test-suite-args (:package :analysis/tests))\n\n(defsystem \"nyxt/user-interface\"\n  :defsystem-depends-on (\"nasdf\")\n  :class :nasdf-system\n  :depends-on (spinneret)\n  :pathname #p\"NYXT:libraries;user-interface;\"\n  :components ((:file \"package\")\n               (:file \"user-interface\")))\n\n(defsystem \"nyxt/text-buffer\"\n  :defsystem-depends-on (\"nasdf\")\n  :class :nasdf-system\n  :depends-on (cluffer)\n  :pathname #p\"NYXT:libraries;text-buffer;\"\n  :components ((:file \"package\")\n               (:file \"text-buffer\")))\n\n(defsystem \"nyxt/password-manager\"\n  :defsystem-depends-on (\"nasdf\")\n  :class :nasdf-system\n  :depends-on (bordeaux-threads\n               cl-ppcre\n               nclasses\n               serapeum\n               str\n               trivial-clipboard\n               uiop)\n  :pathname #p\"NYXT:libraries;password-manager;\"\n  :components ((:file \"package\")\n               (:file \"password\")\n               (:file \"password-keepassxc\")\n               (:file \"password-security\")\n               ;; Keep password-pass as to ensure higher priority.\n               (:file \"password-pass\")))\n\n(defsystem \"nyxt/theme\"\n  :defsystem-depends-on (\"nasdf\")\n  :class :nasdf-system\n  :depends-on (alexandria\n               cl-colors-ng\n               lass\n               nclasses\n               serapeum)\n  :pathname #p\"NYXT:libraries;theme;\"\n  :components ((:file \"package\")\n               (:file \"utilities\")\n               (:file \"theme\"))\n  :in-order-to ((test-op (test-op \"nyxt/theme/tests\"))))\n\n(defsystem \"nyxt/theme/tests\"\n  :defsystem-depends-on (\"nasdf\")\n  :class :nasdf-test-system\n  :depends-on (nyxt/theme lisp-unit2)\n  :pathname #p\"NYXT:libraries;theme;tests;\"\n  :components ((:file \"tests\"))\n  :test-suite-args (:package :theme/tests))\n"
  },
  {
    "path": "source/about.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt)\n\n(define-internal-page-command-global about ()\n  (buffer \"*About*\")\n  \"Show the list of contributors.\"\n  (spinneret:with-html-string\n   (:nstyle (style buffer))\n   (:h1 \"Contributors\")\n   (:ul\n    (:li \"Adom Hartell (@4t0m)\")\n    (:li \"André A. Gomes (@aadcg)\")\n    (:li \"Artyom Bologov (@aartaka)\")\n    (:li \"John Mercouris (@jmercouris)\")\n    (:li \"@hendursaga\")\n    (:li \"@kssytsrk\")\n    (:li \"Pedro Delfino (@pdelfino)\")\n    (:li \"Pierre Neidhardt (@ambrevar)\")\n    (:li \"Solomon Bloch (@noogie13)\")\n    (:li \"Vincent Dardel (@vindarel)\"))\n   (:h1 \"Supporters\")\n   (:p \"Many thanks to all of our backers who've supported Nyxt development.\")\n   (:p \"Thank you to NLnet for supporting Nyxt!\")\n   (:h1 \"Crowdfunding backers\")\n   (:p \"Thank you to all who have supported and made Nyxt possible!\")\n   (:h2 \"2018-11 campaign: *NIX Support\")\n   (:h3 \"Digital Omnipresence\")\n   (:ul\n    (:li \"Alexander.Shendi\")\n    (:li \"Ashish SHUKLA\")\n    (:li \"Christopher Nascone\")\n    (:li \"dan.girsh\")\n    (:li \"Eric Monson\")\n    (:li \"Jack Randall\")\n    (:li \"James Anderson\")\n    (:li \"liweitian\")\n    (:li \"Marco Heisig\")\n    (:li \"Oluwafemi Agbabiaka\")\n    (:li \"pjb\")\n    (:li \"Robert Krahn\")\n    (:li \"Robert Uhl\")\n    (:li \"1 anonymous\"))\n   (:h3 \"Digital Magma\")\n   (:ul\n    (:li \"Daniel V\")\n    (:li \"Jason Hamilton\")\n    (:li \"Magnus Nyberg\")\n    (:li \"Marek Kochanowicz\")\n    (:li \"Rich Smith\")\n    (:li \"Robert Uhl\")\n    (:li \"simon\")\n    (:li \"slade\")\n    (:li \"Steve Last\")\n    (:li \"ulf.makestad\")\n    (:li \"1 anonymous\"))\n   (:h3 \"Digital Immortality\")\n   (:ul\n    (:li \"Alexey Abramov\")\n    (:li \"Are Jensen\")\n    (:li \"Joseph Mingrone\")\n    (:li \"Nikita Poliakov\")\n    (:li \"pjb\")\n    (:li \"Sainath Adapa\")\n    (:li \"Spencer Heywood\")\n    (:li \"Sungjin Chun\")\n    (:li \"Tom Delord\")\n    (:li \"2 anonymous\"))\n   (:h3 \"Others\")\n   (:ul\n    (:li \"Nicholas Zivkovic\")\n    (:li \"Pierre Neidhardt\")\n    (:li \"Simon Zugmeyer\")\n    (:li \"vindarel\")\n    (:li \"5 anonymous\"))\n   (:h2 \"2019-10 campaign: v1.4.0\")\n   (:h3 \"Digital Immortality\")\n   (:ul\n    (:li \"Tim Johann\")\n    (:li \"Julien Rousé\")\n    (:li \"ebababi\")\n    (:li \"Emil Oppeln-Bronikowski\")\n    (:li \"Fox Kiester\")\n    (:li \"Stefan Husmann\")\n    (:li \"Nils Grunwald\")\n    (:li \"Florian Adamsky\")\n    (:li \"Valentin Atanasov\")\n    (:li \"Pranav Vats\")\n    (:li \"Jörn Gersdorf\")\n    (:li \"Matt Skinner\")\n    (:li \"Jelle Dirk Licht\")\n    (:li \"Minori Yamashita\")\n    (:li \"Hugh Daschbach\")\n    (:li \"Niklas Carlsson\")\n    (:li \"mestelan\")\n    (:li \"Camille Troillard\")\n    (:li \"mace nicolas\")\n    (:li \"dan.girsh\")\n    (:li \"Michael Bruderer\")\n    (:li \"Patrice Rault\")\n    (:li \"Cees de Groot\")\n    (:li \"Sam Hedin\")\n    (:li \"rbarzic\")\n    (:li \"Jake Waksbaum\")\n    (:li \"Lukas Jenks\")\n    (:li \"Rodrigo Lazo\")\n    (:li \"Lucas Sifoni\")\n    (:li \"Calle Helmertz\")\n    (:li \"Kristian Nygaard Jensen\")\n    (:li \"Robert Uhl\")\n    (:li \"Francis Burstall\")\n    (:li \"Arnaud BEAUD'HUIN\")\n    (:li \"Daniel V\")\n    (:li \"Albin Heimerson\")\n    (:li \"Alexander ter Weele\")\n    (:li \"Jeremy Firth\")\n    (:li \"aim\")\n    (:li \"liweitian\")\n    (:li \"Philipe Dallaire\")\n    (:li \"Travis Brown\")\n    (:li \"Divan Santana\")\n    (:li \"John C Haprian\")\n    (:li \"Pierrick Maillard\")\n    (:li \"Dardel Renaud\")\n    (:li \"Dardel Renaud\")\n    (:li \"Nils Grunwald\")\n    (:li \"hector\")\n    (:li \"Jean Morel\")\n    (:li \"Jos van Bakel\")\n    (:li \"slade\")\n    (:li \"dietrich ayala\")\n    (:li \"bacon totem\")\n    (:li \"Pierre Neidhardt\")\n    (:li \"18 anonymous\"))))\n"
  },
  {
    "path": "source/browser.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt)\n\n(hooks:define-hook-type prompt-buffer (function (prompt-buffer))\n  \"Hook acting on `prompt-buffer'.\")\n(hooks:define-hook-type resource (function (request-data) (or request-data null))\n  \"Hook acting on `request-data' resource.\nReturns:\n- Possibly modified `request-data'---redirect/block request.\n- NIL---block request.\")\n(hooks:define-hook-type browser (function (browser))\n  \"Hook acting on `browser' (likely `*browser*').\")\n(export-always '(hook-resource))\n\n(define-class proxy ()\n  ((url\n    (quri:uri \"socks5://127.0.0.1:9050\")\n    :documentation \"The address of the proxy server.\nIt's made of three components: protocol, host and port.\nExample: \\\"http://192.168.1.254:8080\\\".\")\n   (allowlist\n    '(\"localhost\" \"localhost:8080\")\n    :type (list-of string)\n    :documentation \"A list of URIs not to forward to the proxy.\")\n   (proxied-downloads-p\n    t\n    :documentation \"Non-nil if downloads should also use the proxy.\"))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:documentation \"Enable forwarding of all network requests to a specific host.\nThis can apply to specific buffer.\"))\n\n(export-always 'combine-composed-hook-until-nil)\n(defmethod combine-composed-hook-until-nil ((hook hooks:hook) &optional arg)\n  \"Return the composition of the HOOK handlers on ARG, from oldest to youngest.\n\nStop processing when a handler returns nil. Without handlers, return ARG.\n\nThis is an acceptable `hooks:combination' for `hooks:hook'.\"\n  (labels ((compose-handlers (handlers result)\n             (if handlers\n                 (let ((new-result (funcall (first handlers) result)))\n                   (log:debug \"Handler (~a ~a): ~a\" (first handlers) result new-result)\n                   (when new-result\n                     (compose-handlers (rest handlers) new-result)))\n                 result)))\n    (compose-handlers (mapcar #'hooks:fn (hooks:handlers hook)) arg)))\n\n(export-always 'renderer-browser)\n(defclass renderer-browser ()\n  ()\n  (:metaclass interface-class)\n  (:documentation \"Renderer-specific representation for the global browser.\nShould be redefined by the renderer.\"))\n\n(define-class browser (renderer-browser)\n  ((search-engines\n    (mapcar #'make-instance '(ddg-search-engine\n                              wikipedia-search-engine))\n    :type (cons search-engine *)\n    :documentation \"A list of `search-engine' objects.\nThe first one is the default, as per `default-search-engine'.\")\n   (search-engine-suggestions-p\n    t\n    :type boolean\n    :documentation \"Whether search suggestions are displayed.\")\n   (remote-execution-p\n    nil\n    :type boolean\n    :documentation \"Whether code sent to the socket gets executed.  You must\nunderstand the risks before enabling this: a privileged user with access to your\nsystem can then take control of the browser and execute arbitrary code under\nyour user profile.\")\n   (exit-code\n    0\n    :type alex:non-negative-integer\n    :reader t\n    :export t\n    :documentation \"The exit code return to the operating system.\n0 means success.\nNon-zero means failure.\")\n   (socket-thread\n    nil\n    :type t\n    :documentation \"Thread that listens on socket.\nSee `*socket-file*'.\nThis slot is mostly meant to clean up the thread if necessary.\")\n   (messages-content\n    '()\n    :export t\n    :reader messages-content\n    :documentation \"A list of all echoed messages.\nMost recent messages are first.\")\n   (clipboard-ring\n    (make-ring)\n    :documentation \"The ring with all the clipboard contents Nyxt could cache.\nNote that it may be incomplete.\")\n   (command-model\n    (make-instance 'analysis:sequence-model)\n    :documentation \"This model is used to generate predictions for what the user will do.\nWhich commands will they invoke next?\")\n   (last-command\n    nil\n    :type (maybe function)\n    :documentation \"The last command invoked by the user.\")\n   (command-dispatcher\n    #'dispatch-command\n    :type (or sym:function-symbol function)\n    :documentation \"Function to process the command processed in `dispatch-input-event'.\nTakes the function/command as the only argument.\")\n   (prompt-buffer-generic-history\n    (make-ring)\n    :documentation \"The default history of all prompt buffer entries.\nThis history is used if no history is specified for a given prompt buffer.\")\n   (default-new-buffer-url\n    (quri:uri (nyxt-url 'new))\n    :type url-designator\n    :documentation \"The URL set to a new blank buffer opened by Nyxt.\")\n   (set-url-history\n    (make-ring)\n    :documentation \"A ring that keeps track of all URLs set by `set-url'.\")\n   (recent-buffers\n    (make-ring :size 50)\n    :export nil\n    :documentation \"A ring that keeps track of deleted buffers.\")\n   (windows\n    (make-hash-table)\n    :export nil\n    :documentation \"Table of all windows, indexed by their `id'.\")\n   (last-active-window\n    nil\n    :type (or window null)\n    :export nil\n    :documentation \"Records the last active window.  This is\nuseful when no Nyxt window is focused and we still want `ffi-window-active' to\nreturn something.\nSee `current-window' for the user-facing function.\")\n   (buffers\n    :initform (make-hash-table)\n    :documentation \"Table of all live buffers, indexed by their `id'.\nSee `buffer-list', `buffer-get', `buffer-set' and `buffer-delete'.\")\n   (startup-error-reporter-function\n    nil\n    :type (or function null)\n    :export nil\n    :documentation \"When supplied, upon startup, if there are errors, they will\nbe reported by this function.\")\n   (open-external-link-in-new-window-p\n    nil\n    :documentation \"Whether to open links issued by an external program or\nissued by Control+<button1> in a new window.\")\n   (downloads\n    :documentation \"List of downloads. Used for rendering by the download manager.\")\n   (startup-timestamp\n    (time:now)\n    :export nil\n    :documentation \"`time:timestamp' of when Nyxt was started.\")\n   (startup-promise\n    (lpara:promise)\n    :export nil\n    :accessor nil\n    :documentation \"Promise used to make `start-browser' synchronous.\nWithout it, `start-browser' would return before the `*browser*' is effectively usable.\nImplementation detail.\")\n   (init-time\n    0.0\n    :type alex:non-negative-real\n    :export nil\n    :documentation \"Initialization time in seconds.\")\n   (ready-p\n    nil\n    :reader ready-p\n    :documentation \"If non-nil, the browser is ready for operation (make\nbuffers, load data files, open prompt buffer, etc).\")\n   (native-dialogs\n    t\n    :type boolean\n    :documentation \"Whether to replace renderer specific dialog boxes with the\nprompt buffer.\")\n   (theme\n    theme:+light-theme+\n    :type theme:theme\n    :documentation \"The theme to use for all the browser interface elements.\")\n   (glyph-logo\n    (gethash \"nyxt.svg\" *static-data*)\n    :documentation \"The logo of Nyxt as an SVG.\")\n   (history-file\n    (make-instance 'history-file)\n    :type history-file\n    :documentation \"A file to persist history data across sessions.\")\n   (history-vector\n    (make-array 0 :fill-pointer t :adjustable t)\n    :type vector\n    :documentation \"A vector holding `history-entry' objects.\")\n   (default-cookie-policy\n    :no-third-party\n    :type cookie-policy\n    :documentation \"Cookie policy of new buffers.\nValid values are `:accept', `:never' and `:no-third-party'.\")\n   ;; Hooks follow:\n   (after-init-hook\n    (make-instance 'hook-browser)\n    :documentation \"The entry-point hook to configure everything in Nyxt.\nThe hook takes browser as the argument.\n\nThis hook is run after the `*browser*' is instantiated and before the\n`startup' is run.\n\nA handler can be added with:\n\\(define-configuration browser\n  (after-init-hook (hooks:add-hook %slot-value% 'my-init-handler)))\")\n   (after-startup-hook\n    (make-instance 'hook-browser)\n    :documentation \"Hook run when the browser is started and ready for interaction.\nThe handlers take browser as the argument.\n\nA handler can be added with:\n\\(define-configuration browser\n  (after-startup-hook (hooks:add-hook %slot-value% 'my-startup-handler)))\")\n   (before-exit-hook\n    (make-instance 'hooks:hook-void)\n    :type hooks:hook-void\n    :documentation \"Hook run before both `*browser*' and the renderer get terminated.\nThe handlers take no argument.\")\n   (window-make-hook\n    (make-instance 'hook-window)\n    :type hook-window\n    :documentation \"Hook run after `window-make'.\nThe handlers take the window as argument.\")\n   (buffer-make-hook\n    (make-instance 'hook-buffer)\n    :type hook-buffer\n    :documentation \"Hook run after `buffer' initialization and before the URL is\nloaded.\nIt is run before mode initialization so that the default mode list can still be\naltered from the hooks.\nThe handlers take the buffer as argument.\")\n   (buffer-before-make-hook\n    (make-instance 'hook-buffer)\n    :type hook-buffer\n    :documentation \"Hook run at the beginning of `buffer' initialization.\nThe buffer web view is not allocated, so it's not possible to run arbitrary\nparenscript from this hook.\nSee `buffer-make-hook' and `buffer-after-make-hook' for other hook options.\nThe handlers take the buffer as argument.\")\n   (buffer-after-make-hook\n    (make-instance 'hook-buffer)\n    :type hook-buffer\n    :documentation \"Hook run after `buffer' initialization and before the URL is\nloaded.\nIt is run as the very last step of buffer initialization, when everything else is ready.\nSee also `buffer-make-hook' and `buffer-before-make-hook'.\nThe handlers take the buffer as argument.\")\n   (prompt-buffer-make-hook\n    (make-instance 'hook-prompt-buffer)\n    :type hook-prompt-buffer\n    :documentation \"Hook run after the `prompt-buffer' class is instantiated and\nbefore initializing the `prompt-buffer' modes.\nThe handlers take the `prompt-buffer' as argument.\")\n   (prompt-buffer-ready-hook\n    (make-instance 'hook-prompt-buffer)\n    :type hook-prompt-buffer\n    :documentation \"Hook run while waiting for the prompt buffer to be available.\nThe handlers take the `prompt-buffer' as argument.\")\n   (external-editor-program\n    (or (uiop:getenvp \"VISUAL\")\n        (uiop:getenvp \"EDITOR\")\n        (when (sera:resolve-executable \"gio\") \"gio open\"))\n    :type (or string null)\n    :reader nil\n    :writer t\n    :export t\n    :documentation \"The external editor to use for editing files.\nThe full command, including its arguments, may be specified as list of strings\nor as a single string.\"))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:documentation \"The browser class defines the overall behavior of Nyxt, in\nthe sense that it manages the display of buffers.  For instance, it abstracts\nthe renderer, and lays the foundations to track and manipulate buffers and\nwindows.\n\nA typical Nyxt session encompasses a single instance of this class, but nothing\nprevents otherwise.\")\n  (:metaclass user-class))\n\n(export-always 'recent-history-entries)\n(defmethod recent-history-entries (n (browser browser) &key deduplicate-p)\n  \"Return the N most recent browsing history entries as a list.\n\nWhen DEDUPLICATE-P is non-nil, remove duplicated entries.\"\n  (nreverse (coerce (let ((recent-entries (sera:slice (history-vector browser) (- n))))\n                      (if deduplicate-p\n                          (remove-duplicates recent-entries :test #'equals)\n                          recent-entries))\n                    'list)))\n\n(defmethod theme ((ignored (eql nil)))\n  \"Fallback theme in case `*browser*' is NIL.\"\n  (declare (ignore ignored))\n  theme:+light-theme+)\n\n(defmethod external-editor-program ((browser browser))\n  \"Specialized reader for `external-editor-program' slot.\nA list of strings is returned, as to comply with `uiop:launch-program' or\n`uiop:run-program'.\"\n  (with-slots ((cmd external-editor-program)) browser\n    (if (str:blank? cmd)\n        (progn (echo-warning \"Invalid value of `external-editor-program' browser slot.\") nil)\n        (str:split \" \" cmd :omit-nulls t))))\n\n(defmethod default-search-engine ((browser browser))\n  (first (search-engines browser)))\n\n(defmacro on-renderer-ready (thread-name &body body)\n  \"Run BODY from a new thread when renderer is ready.\n`ffi-within-renderer-thread' runs its body on the renderer thread when it's\nidle, so it should do the job.\"\n  `(ffi-within-renderer-thread (lambda () (run-thread ,thread-name ,@body))))\n\n(defmethod finalize-startup ((browser browser) urls startup-timestamp)\n  \"Run `after-init-hook' then BROWSER's `startup'.\"\n  ;; `messages-appender' requires `*browser*' to be initialized.\n  (unless (find-if (sera:eqs 'messages-appender) (log4cl:all-appenders)\n                   :key #'sera:class-name-of)\n    (log4cl:add-appender log4cl:*root-logger* (make-instance 'messages-appender)))\n  (ignore-errors\n   (handler-bind ((error (lambda (c) (log:error \"In after-init-hook: ~a\" c))))\n     (hooks:run-hook (after-init-hook browser) browser))) ; TODO: Run outside the main loop?\n  ;; `startup' must be run _after_ this function returns; It's not enough since\n  ;; the `startup' may invoke the prompt buffer, which cannot be invoked from\n  ;; the renderer thread: this is why we run the `startup' in a new thread from\n  ;; there.\n  (on-renderer-ready \"finalize-startup\"\n    (window-make browser)\n    (let ((history-file-contents (files:content (history-file browser))))\n      (setf (history-vector browser)\n            (make-array (length history-file-contents)\n                        :fill-pointer t\n                        :adjustable t\n                        :initial-contents history-file-contents)))\n    (open-urls (or urls (list (default-new-buffer-url browser))))\n    (lpara:fulfill (slot-value browser 'startup-promise))\n    (hooks:run-hook (after-startup-hook browser) browser)\n    (funcall* (startup-error-reporter-function browser)))\n  ;; Set `init-time' at the end of finalize to take the complete startup time\n  ;; into account.\n  (setf (slot-value *browser* 'init-time)\n        (time:timestamp-difference (time:now) startup-timestamp))\n  (setf (slot-value *browser* 'ready-p) t))\n\n;; Catch a common case for a better error message.\n(defmethod buffers :before ((browser t))\n  (when (null browser)\n    (error \"There is no current *browser*. Is Nyxt started?\")))\n\n(-> set-window-title (&optional window) *)\n(export-always 'set-window-title)\n(defun set-window-title (&optional (window (current-window)))\n  \"Set WINDOW title.\"\n  (setf (ffi-window-title window) (titler window)))\n\n(-> open-urls ((maybe (cons quri:uri *))) *)\n(defun open-urls (urls)\n  \"Create new buffers and load URLS.\nThe buffer corresponding to the first URL is focused.\"\n  (with-protect (\"Could not make buffer to open ~a: ~a\" urls :condition)\n    (let ((first-buffer (first (mapcar (lambda (url) (make-buffer :url url))\n                                       urls))))\n      (when first-buffer\n        (if (open-external-link-in-new-window-p *browser*)\n            (ffi-window-set-buffer (window-make *browser*) first-buffer)\n            (set-current-buffer first-buffer))))))\n\n(defun get-keymap (buffer buffer-keyscheme-map)\n  \"Return the keymap in BUFFER-KEYSCHEME-MAP corresponding to BUFFER's `keyscheme'.\nIf none is found, fall back to `keyscheme:cua'.\"\n  (keymaps:get-keymap (or (keyscheme buffer) keyscheme:cua) buffer-keyscheme-map))\n\n(defun request-resource-open-url (&key url &allow-other-keys)\n  (make-buffer :url url))\n\n(defun request-resource-open-url-focus (&key url &allow-other-keys)\n  (make-buffer-focus :url url))\n\n(export-always 'renderer-request-data)\n(defclass renderer-request-data ()\n  ()\n  (:metaclass interface-class)\n  (:documentation \"Renderer-specific request object.\nShould be redefined by the renderer.\"))\n\n(define-class request-data (renderer-request-data)\n  ((buffer\n    (current-buffer)\n    :type buffer\n    :documentation \"Buffer targeted by the request.\")\n   (url\n    (quri:uri \"\")\n    :documentation \"URL of the request\")\n   (event-type\n    :other\n    :accessor nil ; TODO: No public accessor for now, we first need a use case.\n    :export nil\n    :documentation \"The type of request, e.g. `:link-click'.\")\n   (new-window-p\n    nil\n    :documentation \"Whether the request takes place in a\nnew window.\")\n   (http-method\n    nil\n    :type (maybe string)\n    :documentation \"The HTTP method (GET, POST and friends) of the request.\")\n   (request-headers\n    nil\n    :type trivial-types:association-list\n    :documentation \"Dotted alist of headers for the request.\")\n   (response-headers\n    nil\n    :type trivial-types:association-list\n    :documentation \"Dotted alist of headers for the response to the given request.\")\n   (toplevel-p\n    nil\n    :documentation \"Whether the request happens in a toplevel frame.\")\n   (resource-p\n    nil\n    :documentation \"Whether the request is a resource request.\nResource requests cannot be redirected or blocked.\")\n   (mime-type\n    nil\n    :type (maybe string)\n    :documentation \"The MIME type of the resource at the other end of the request.\")\n   (known-type-p\n    nil\n    :documentation \"Whether the request is for content with\nsupported MIME-type, such as a picture that can be displayed in the web\nview.\")\n   (file-name\n    nil\n    :type (maybe string)\n    :documentation \"The name this file will be saved on disk with, if downloaded.\")\n   (keys\n    '()\n    :type list\n    :documentation \"The key sequence that generated the request.\"))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:documentation \"Representation of HTTP(S) request.\nMost important slots are:\n- `buffer' request belongs to.\n- `url' requested.\n- `request-headers'/`response-headers' for headers it's requested with.\n- and `toplevel-p'/`resource-p' for whether it's a new page or resource\n  request (respectively).\"))\n\n(export-always 'url-dispatching-handler)\n(-> url-dispatching-handler\n    (symbol\n     (function (quri:uri) boolean)\n     (or string (function (quri:uri) (or quri:uri null))))\n    *)\n(defun url-dispatching-handler (name test action)\n  \"Return a `hook-request' handler apply its ACTION on the URLs conforming to TEST.\nFit for `request-resource-hook'.\n\nTEST should be function of one argument, the requested URL.\n\nACTION can be either\n- a shell command as a string,\n- or a function taking a URL as argument.\n\nIn case ACTION returns nil (always the case for shell command), URL request is\naborted. If ACTION returns a URL, it's loaded.\n\n`match-host', `match-scheme', `match-domain' and `match-file-extension' can be\nused to create TEST-functions, but any other function of one argument would fit\nthe TEST slot as well.\n\nThe following example does a few things:\n- Forward DOI links to the doi.org website.\n- Open magnet links with Transmission.\n- Open local files (file:// URIs) with Emacs.\n\n\\(define-configuration web-buffer\n  (request-resource-hook\n   (hooks:add-hook %slot-value%\n                   (url-dispatching-handler\n                    'doi-link-dispatcher\n                    (match-scheme \\\"doi\\\")\n                    (lambda (url)\n                      (quri:uri (format nil \\\"https://doi.org/~a\\\"\n                                        (quri:uri-path url))))))))\n\n\\(defmethod customize-instance ((buffer web-buffer))\n  (hooks:add-hook\n   (request-resource-hook buffer)\n   (url-dispatching-handler\n    'transmission-magnet-links\n    (match-scheme \\\"magnet\\\")\n    \\\"transmission-remote --add ~a\\\"))\n  (hooks:add-hook\n   (request-resource-hook buffer)\n   (url-dispatching-handler\n    'emacs-file\n    (match-scheme \\\"file\\\")\n    (lambda (url)\n      (uiop:launch-program\n       `(\\\"emacs\\\" ,(quri:uri-path url)))\n      nil))))\"\n  (make-instance\n   'hooks:handler\n   :fn (lambda (request-data)\n         (let ((url (url request-data)))\n           (if (funcall test url)\n               (etypecase action\n                 (function\n                  (let* ((new-url (funcall action url)))\n                    (log:info \"Applied ~s URL-dispatcher on ~s and got ~s\"\n                              (symbol-name name)\n                              (render-url url)\n                              (when new-url (render-url new-url)))\n                    (when new-url\n                      (setf (url request-data) new-url)\n                      request-data)))\n                 (string (let ((action (lambda (url)\n                                         (uiop:launch-program\n                                          (format nil action\n                                                  (render-url url)))\n                                         nil)))\n                           (funcall action url)\n                           (log:info \"Applied ~s shell-command URL-dispatcher on ~s\"\n                                     (symbol-name name)\n                                     (render-url url)))))\n               request-data)))\n   :name name))\n\n(defun javascript-error-handler (condition)\n  (echo-warning \"JavaScript error: ~a\" condition))\n\n(defun print-message (html-body &optional (window (current-window)))\n  (ffi-print-message (message-buffer window) html-body))\n\n(export-always 'current-window)\n(defun current-window (&optional no-rescan)\n  \"Return the current window.\nIf NO-RESCAN is non-nil, fetch the window from the `last-active-window' cache\ninstead of asking the renderer for the active window.  It is faster but\nmay yield the wrong result.\"\n  (when *browser*\n    (if (and no-rescan (slot-value *browser* 'last-active-window))\n        (slot-value *browser* 'last-active-window)\n        ;; No window when browser is not started.\n        (ignore-errors (ffi-window-active *browser*)))))\n\n(export-always 'set-current-buffer)\n(defmethod set-current-buffer ((buffer modable-buffer) &key (focus t))\n  \"Set the active BUFFER for the active window.\nReturn BUFFER.\"\n  (cond ((not (current-window)) (make-window buffer))\n        ((and (active-buffer-p buffer)\n              (not (eq (current-window) (window buffer))))\n         (ffi-window-set-buffer (window buffer) (get-inactive-buffer) :focus nil)\n         (ffi-window-set-buffer (current-window) buffer :focus focus))\n        ((and (not (active-buffer-p buffer))\n              (not (eq (current-window) (window buffer))))\n         (ffi-window-set-buffer (current-window) buffer :focus focus))\n        (t nil))\n  buffer)\n\n(export-always 'current-prompt-buffer)\n(defun current-prompt-buffer ()\n  \"Return the current prompt-buffer.\"\n  (first (active-prompt-buffers (current-window))))\n\n(export-always 'focused-buffer)\n(defun focused-buffer (&optional (window (current-window)) )\n  \"Return the currently focused buffer.\"\n  (find-if #'ffi-focused-p\n           (list (first (active-prompt-buffers window))\n                 (active-buffer window)\n                 (status-buffer window)\n                 (message-buffer window))))\n\n(define-internal-page-command-global reduce-to-buffer (&key (delete t))\n    (reduced-buffer \"*Reduced Buffers*\")\n  \"Query the buffer(s) to \\\"reduce \\\" by copying their titles/URLs to a\nsingle buffer, optionally delete them. This function is useful for archiving a\nset of useful URLs or preparing a list to send to a someone else.\"\n  (let ((buffers (prompt\n                  :prompt \"Reduce buffer(s)\"\n                  :sources (make-instance 'buffer-source\n                                          :constructor (remove-if #'internal-url-p (buffer-list)\n                                                                  :key #'url)\n                                          :actions-on-return #'identity\n                                          :enable-marks-p t))))\n    (unwind-protect\n         (spinneret:with-html-string\n           (:h1 \"Reduced Buffers:\")\n           (:div\n            (if buffers\n                (loop for buffer in buffers\n                      collect\n                      (with-current-buffer buffer\n                        (:div\n                         (:p (:b \"Title: \") (title buffer))\n                         (:p (:b \"URL: \") (:a :href (render-url (url buffer))\n                                              (render-url (url buffer))))\n                         (:p (:b \"Automatically generated summary: \")\n                             (:ul\n                              (loop for summary-bullet in (analysis:summarize-text\n                                                           (document-get-paragraph-contents :limit 10000))\n                                    collect (:li (str:collapse-whitespaces summary-bullet)))))\n                         (:hr \"\"))))\n                (:p \"None chosen.\"))))\n      (when delete (mapcar #'buffer-delete buffers)))))\n\n(export-always 'render-menu)\n(defun render-menu (mode-symbol &optional (buffer (current-buffer)))\n  \"Render a menu for a given mode symbol.\"\n  (spinneret:with-html\n    (:div :class \"mode-menu\"\n          (loop for command in (list-mode-commands mode-symbol)\n                collect\n                   (let ((name (string-downcase (closer-mop:generic-function-name command)))\n                         (bindings (keymaps:pretty-binding-keys\n                                    (name command)\n                                    (current-keymaps buffer)\n                                    :print-style (keymaps:name (keyscheme buffer)))))\n                     (:nbutton\n                       :class \"button binding\"\n                       :text (if bindings (first bindings) \"⏎\")\n                       `(nyxt::run-async ,command))\n                     (:nbutton\n                       :class \"button command\"\n                       :text name\n                       `(nyxt::run-async ,command)))))))\n"
  },
  {
    "path": "source/buffer.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt)\n\n(hooks:define-hook-type keymaps-buffer\n    (function ((list-of keymaps:keymap) buffer)\n              (values &optional (list-of keymaps:keymap) buffer))\n  \"Hook to modify keymaps.\nGet a list of `nkeymaps:keymap's and `buffer' and return a new list and buffer.\")\n(export-always '(hook-keymaps-buffer))\n(hooks:define-hook-type url->url (function (quri:uri) quri:uri)\n  \"Hook getting a `quri:uri' and returning same/another one. \")\n\n(export-always 'renderer-buffer)\n(defclass renderer-buffer ()\n  ()\n  (:metaclass interface-class)\n  (:documentation \"Renderer-specific buffer objects.\nShould be redefined by the renderer.\"))\n\n(defvar %default-modes '(base-mode)\n  \"The default modes for unspecialized buffers.\nThis is useful when there is no current buffer.\")\n\n(define-class buffer (renderer-buffer)\n  ((default-modes\n    %default-modes\n    :accessor nil\n    :type (list-of symbol)\n    :documentation \"The symbols of the modes to instantiate on buffer creation.\nThe mode instances are stored in the `modes' BUFFER slot.\n\nThe default modes returned by this method are appended to the default modes\ninherited from the superclasses.\")\n   (id\n    (new-id)\n    :type unsigned-byte\n    :documentation \"Unique identifier for a buffer.\")\n   (key-stack\n    '()\n    :documentation \"A stack of the key chords a user has pressed.\")\n   (last-access\n    (time:now)\n    :export nil\n    :documentation \"Timestamp when the buffer was last switched to.\")\n   (last-key\n    nil\n    :export nil\n    :type (or null keymaps:key)\n    :documentation \"Last pressed key.\")\n   (url (quri:uri \"\"))\n   (url-at-point (quri:uri \"\"))\n   (title \"\")\n   (style\n    (theme:themed-css (theme *browser*)\n      '(:font-face :font-family \"public sans\" :font-style \"normal\" :font-weight\n        \"400\" :src \"url('nyxt-resource:PublicSans-Regular.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"italic\" :font-weight\n        \"400\" :src \"url('nyxt-resource:PublicSans-Italic.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"normal\" :font-weight\n        \"100\" :src \"url('nyxt-resource:PublicSans-Thin.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"italic\" :font-weight\n        \"100\" :src \"url('nyxt-resource:PublicSans-ThinItalic.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"normal\" :font-weight\n        \"200\" :src \"url('nyxt-resource:PublicSans-ExtraLight.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"italic\" :font-weight\n        \"200\" :src \"url('nyxt-resource:PublicSans-ExtraLightItalic.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"normal\" :font-weight\n        \"300\" :src \"url('nyxt-resource:PublicSans-Light.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"italic\" :font-weight\n        \"300\" :src \"url('nyxt-resource:PublicSans-LightItalic.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"normal\" :font-weight\n        \"500\" :src \"url('nyxt-resource:PublicSans-Medium.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"italic\" :font-weight\n        \"500\" :src \"url('nyxt-resource:PublicSans-MediumItalic.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"normal\" :font-weight\n        \"600\" :src \"url('nyxt-resource:PublicSans-SemiBold.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"italic\" :font-weight\n        \"600\" :src \"url('nyxt-resource:PublicSans-SemiBoldItalic.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"normal\" :font-weight\n        \"700\" :src \"url('nyxt-resource:PublicSans-Bold.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"italic\" :font-weight\n        \"700\" :src \"url('nyxt-resource:PublicSans-BoldItalic.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"normal\" :font-weight\n        \"800\" :src \"url('nyxt-resource:PublicSans-ExtraBold.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"italic\" :font-weight\n        \"800\" :src \"url('nyxt-resource:PublicSans-ExtraBoldItalic.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"normal\" :font-weight\n        \"900\" :src \"url('nyxt-resource:PublicSans-Black.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"italic\" :font-weight\n        \"900\" :src \"url('nyxt-resource:PublicSans-BlackItalic.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"dejavu sans mono\"\n        :src \"url('nyxt-resource:DejaVuSansMono.ttf')\" \"format('ttf')\")\n      '(*\n        :box-sizing border-box)\n      `(body\n        :background-color ,theme:background-color\n        :color ,theme:on-background-color\n        :font-family ,theme:font-family\n        :margin-left \"20px\"\n        :margin-top \"20px\")\n      '(ul\n        :margin-top \"0\"\n        :margin-bottom \"0\")\n      '(\"details > *\"\n        :margin-left \"18px\")\n      '(\"details > ul\"\n        :margin-left \"inherit\")\n      '(\"details summary\"\n        :margin-left \"inherit\"\n        :margin-bottom \"8px\"\n        :cursor \"pointer\")\n      '(\"summary::-webkit-details-marker\"\n        :padding-bottom \"4px\")\n      '(\"details > summary\"\n        :list-style-type \"none\")\n      '(\"details > summary::-webkit-details-marker\"\n        :display \"none\")\n      '(\"details > summary::before\"\n        :font-weight \"bold\"\n        :content \"+\"\n        :margin-right \"5px\"\n        :display \"inline-block\")\n      '(\"details[open] > summary::before\"\n        :content \"−\")\n      '(.section\n        :margin-top \"2em\")\n      `(\"h1,h2,h3,h4,h5,h6\"\n        :color ,theme:primary-color)\n      `(hr\n        :background-color ,theme:secondary-color\n        :color ,theme:on-secondary-color\n        :height \"2px\"\n        :border-radius \"2px\"\n        :border-width \"0\")\n      '(button\n        :background \"transparent\"\n        :color \"inherit\"\n        :border \"none\"\n        :padding 0\n        :font \"inherit\"\n        :outline \"inherit\")\n      `(.button\n        :appearance \"menulist-button\"\n        :background-color ,theme:primary-color\n        :color ,theme:on-primary-color\n        :display \"inline-block\"\n        :text-decoration \"none\"\n        :border-radius \"4px\"\n        :border-color ,theme:primary-color\n        :border-style \"solid\"\n        :border-width \"0.2em\"\n        :padding \"0.2em\"\n        :margin \"0.2em\")\n      `(select.button\n        :appearance auto\n        :background-color ,theme:primary-color\n        :color ,theme:on-primary-color)\n      `(code\n        :font-family ,theme:monospace-font-family\n        :font-size \"0.9rem\")\n      `(.code-select\n        :position \"absolute\"\n        :top \"0\"\n        :right \"0\"\n        :padding-right \"8px !important\"\n        :direction \"rtl\"\n        :appearance \"none !important\"\n        :border \"none\"\n        :background-color \"transparent !important\"\n        :color \"black !important\")\n      `(\".code-select:hover\"\n        :color ,theme:action-color !important)\n      '((:and .button :hover)\n        :cursor \"pointer\"\n        :opacity 0.8)\n      `((:and .button (:or :visited :active))\n        :color ,theme:background-color)\n      `(.link\n        :appearance none\n        :text-decoration \"underline\"\n        :display \"inline\"\n        :color ,theme:primary-color)\n      '(\".link:hover\"\n        :opacity 0.8)\n      `(.action\n        :color ,theme:action-color)\n      `(.button.action\n        :background-color ,theme:action-color\n        :color ,theme:on-action-color\n        :border-color ,theme:action-color+)\n      `(.warning\n        :color ,theme:warning-color)\n      `(.button.warning\n        :background-color ,theme:warning-color\n        :color ,theme:on-warning-color\n        :border-color ,theme:warning-color+)\n      `(.success\n        :color ,theme:success-color)\n      `(.button.success\n        :background-color ,theme:success-color\n        :color ,theme:on-success-color\n        :border-color ,theme:success-color+)\n      `(.highlight\n        :color ,theme:highlight-color)\n      `(.button.highlight\n        :background-color ,theme:highlight-color\n        :color ,theme:on-highlight-color\n        :border-color ,theme:highlight-color+)\n      `(.plain\n        :color ,theme:on-background-color\n        :background-color ,theme:background-color)\n      `(.input\n        :appearance \"textfield\"\n        :display \"inline-block\"\n        :color \"black\"\n        :background-color \"white\"\n        :border \"0.2em\" solid ,theme:secondary-color\n        :border-radius \"4px\"\n        :outline \"none\"\n        :padding \"0.2em\"\n        :margin \"0.2em\")\n      `(a\n        :color ,theme:primary-color)\n      `(\"a:hover\"\n        :opacity 0.8)\n      `(pre\n        :font-family ,theme:monospace-font-family\n        :font-size \"0.9rem\"\n        :border-radius \"2px\"\n        :overflow \"auto\"\n        :padding \"5px\")\n      '(\"table\"\n        :border-radius \"2px\"\n        :border-spacing \"0\"\n        :width \"100%\")\n      `(\"pre, p code\"\n        :color ,theme:on-background-color\n        :background-color ,theme:background-color-)\n      '(\"a code\"\n        :text-decoration underline)\n      `(\"table, th, td\"\n        :border-color ,theme:primary-color\n        :border-width \"1px\"\n        :border-style \"solid\"\n        :background-color ,theme:background-color\n        :color ,theme:on-background-color)\n      '(\"td, th\"\n        :padding \"6px\")\n      `(th\n        :background-color ,theme:primary-color\n        :color ,theme:on-primary-color\n        :text-align \"left\")\n      '(\"th:first-of-type\"\n        :border-top-left-radius \"1px\")\n      '(\"th:last-of-type\"\n        :border-top-right-radius \"1px\")\n      '(\"tr:last-of-type td:first-of-type\"\n        :border-bottom-left-radius \"2px\")\n      '(\"tr:last-of-type td:last-of-type\"\n        :border-bottom-right-radius \"2px\")\n      '(\"table.resizable-table th\"\n        :resize \"horizontal\"\n        :overflow \"auto\")\n      `(\"::selection\"\n        :color ,theme:on-action-color\n        :background-color ,theme:action-color)\n      `(\".mode-menu\"\n        :overflow-x \"scroll\"\n        :white-space \"nowrap\"\n        :background-color ,theme:background-color-\n        :position \"sticky\"\n        :margin-top \"-20px\"\n        :top 0\n        :width \"100%\"\n        :height \"32px\")\n      `(\".mode-menu > button\"\n        :color ,theme:on-secondary-color\n        :padding-left \"8px\"\n        :padding-right \"8px\"\n        :font-size \"14px\"\n        :border-radius \"2px\"\n        :margin \"0\"\n        :margin-right \"12px\"\n        :border \"none\"\n        :height \"32px\")\n      `(\".mode-menu > .binding\"\n        :background-color ,theme:secondary-color)\n      `(\".mode-menu > .command\"\n        :background-color ,theme:background-color-)\n      '(\".mode-menu::-webkit-scrollbar\"\n        :display \"none\")\n      '(\"dl\"\n        :display \"grid\"\n        :grid-template-columns \"max-content auto\"\n        :row-gap \"10px\"\n        :column-gap \"10px\")\n      `(\"dt\"\n        :grid-column-start 1\n        :padding \"4px\"\n        :padding-left \"8px\"\n        :padding-right \"8px\"\n        :border-radius \"2px\"\n        :font-weight \"bold\"\n        :background-color ,theme:background-color-)\n      '(\"dd\"\n        :margin-inline-start \"0\"\n        :grid-column-start 2)\n      '(\"dd pre\"\n        :margin-top \"0\"\n        :margin-bottom \"0\")\n      '(\".nsection-anchor\"\n        :display \"none\")\n      '(\".nsection-summary:hover .nsection-anchor\"\n        :display \"inline-block\")))\n   (buffer-delete-hook\n    (make-instance 'hook-buffer)\n    :type hook-buffer\n    :documentation \"Hook run before `buffer-delete'.\nThe handlers take the buffer as argument.\"))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:export-predicate-name-p t)\n  (:metaclass user-class)\n  (:documentation \"A buffer is the fundamental unit of displayed content.\nBuffers result from the computations of a web renderer, which generates a visual\nrepresentation of HTML documents.\n\nRendered URLs or the Nyxt's manual qualify as examples.  Buffers are fully\nseparated from one another, so that each has its own behavior and settings.\"))\n\n(defmethod request-resource-hook ((buffer buffer))\n  \"A method to not error out if the buffer has no `request-resource-hook'.\n\nUseful in FFI functions where we usually specialize things against\n`renderer-buffer', not knowing the exact class of those.\"\n  nil)\n\n(defmethod initialize-instance :after ((buffer buffer) &key &allow-other-keys)\n  \"Dummy method to allow forwarding other key arguments.\"\n  buffer)\n\n(define-class modable-buffer (buffer)\n  ((modes\n    '()\n    :documentation \"The list of mode instances.\nModes are instantiated over the result of the `default-modes' method, with\n`customize-instance' and not in the initform so that the instantiation form can\naccess the initialized buffer.\")\n   (page-mode\n    nil\n    :documentation \"A single mode enabled for internal pages.\nThis slot stores the mode enabled by internal pages. When the user navigates\naway from the internal page, this mode is disabled.\")\n   (enable-mode-hook\n    (make-instance 'hook-mode)\n    :type hook-mode\n    :documentation \"Hook run on mode enabling, after the mode-specific hook.\")\n   (disable-mode-hook\n    (make-instance 'hook-mode)\n    :type hook-mode\n    :documentation \"Hook run on mode disabling, after the mode-specific hook.\"))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:export-predicate-name-p t)\n  (:metaclass user-class)\n  (:documentation \"A buffer whose behavior can be modified with `mode's.\"))\n\n(defmethod enable-page-mode ((modable-buffer modable-buffer) mode)\n  (enable-modes* mode modable-buffer)\n  (setf (page-mode modable-buffer) mode))\n\n(defmethod disable-page-mode ((modable-buffer modable-buffer))\n  (when (page-mode modable-buffer)\n    (disable-modes* (page-mode modable-buffer) modable-buffer)\n    (setf (page-mode modable-buffer) nil)))\n\n(defmethod modes ((buffer buffer))\n  \"Return the modes active in BUFFER.\n\nNon-`modable-buffer's never have modes.\nThe default specialization on `buffer' is useful to be able to call the method\nregardless of the buffer, with a meaningful result.\"\n  '())\n\n(export-always 'enabled-modes)\n(defmethod enabled-modes ((buffer modable-buffer))\n  \"Only return enabled modes.\"\n  (sera:filter #'enabled-p (modes buffer)))\n\n(defmethod enabled-modes ((buffer buffer))\n  \"Unless a modable buffer, return NIL for modes.\"\n  nil)\n\n(define-class input-buffer (buffer)\n  ((keyscheme\n    keyscheme:cua\n    :documentation \"The keyscheme that will be used for all modes.\")\n   (current-keymaps-hook\n    (make-instance 'hook-keymaps-buffer\n                   :combination #'hooks:combine-composed-hook)\n    :type hook-keymaps-buffer\n    :documentation \"Hook run as a return value of `current-keymaps'.\")\n   (conservative-word-move\n    t\n    :documentation \"If non-nil, the cursor moves to the end\n(resp. beginning) of the word when `move-forward-word'\n(resp. `move-backward-word') is called.\")\n   (forward-input-events-p\n    nil\n    :documentation \"When non-nil, keyboard events are\nforwarded to the renderer when no binding is found.  Pointer\nevents (e.g. mouse events) are not affected by this, they are always\nforwarded when no binding is found.\")\n   (last-event\n    nil\n    :type t\n    :export nil\n    ;; TODO: Store multiple events?  Maybe when implementing keyboard macros.\n    :documentation \"The last event received in the current buffer.\")\n   (lisp-url-callbacks\n    (sera:dict)\n    :type hash-table\n    :export nil\n    :documentation \"The index of callbacks for `lisp://' URLs.\nThey are populated by the `nyxt/ps:lisp-eval' Parenscript macro.\n\nIt's part of `input-buffer' since any (even offline) buffer that can be clicked\non may want to have dynamic interactions.\"))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:export-predicate-name-p t)\n  (:metaclass user-class)\n  (:documentation \"A buffer in which the user can input.\"))\n\n(define-class document-buffer (buffer)\n  ((document-model-delta-threshold\n    10\n    :documentation \"The `document-model' is updated when the changed elements\nexceed this amount.\"\n    :export nil)\n   (document-model\n    nil\n    :reader nil                         ; We use a custom reader.\n    :writer t\n    :export t\n    :type (or null plump:node)\n    :documentation \"A parsed representation of the rendered buffer.\nComputed by `plump:parse', see `update-document-model' for details.\")\n   (keep-search-marks-p\n    t\n    :type boolean\n    :documentation \"Whether to keep search marks after exiting the prompt\nbuffer.\")\n   (scroll-distance\n    32\n    :type integer\n    :documentation \"The distance in pixels for `scroll-down' or `scroll-up'.\")\n   (smooth-scrolling\n    nil\n    :documentation \"Whether to scroll smoothly.\")\n   (horizontal-scroll-distance\n    50\n    :type integer\n    :documentation \"The distance in pixels for `scroll-left' or `scroll-right'.\")\n   (zoom-ratio\n    nil\n    :type (or null float)\n    :reader t\n    :export t\n    :documentation \"The current zoom ratio as per `ffi-buffer-zoom-ratio'.\nIt is an implementation detail and must not be set by the user. For the\nuser-facing slot, see `zoom-ratio-default'.\")\n   (zoom-ratio-step\n    0.1\n    :type float\n    :documentation \"The step size for zooming in and out.\")\n   (zoom-ratio-default\n    1.0\n    :type float\n    :documentation \"The default zoom ratio.\")\n   (page-scroll-ratio\n    0.90\n    :type float\n    :documentation \"The ratio of the page to scroll.\nA value of 0.95 means that the bottom 5% will be the top 5% when scrolling\ndown.\"))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:export-predicate-name-p t)\n  (:metaclass user-class)\n  (:documentation \"Buffers holding structured documents.\"))\n\n(define-class context-buffer (buffer)\n  ((download-directory\n    (make-instance 'download-directory)\n    :type download-directory\n    :documentation \"Directory where downloads will be stored.\")\n   (download-engine\n    :initform :renderer\n    :type symbol\n    :documentation \"Select a download engine to use, such as `:lisp' or\n`:renderer'.\"))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:export-predicate-name-p t)\n  (:metaclass user-class)\n  (:documentation \"A buffer that holds buffer-specific settings (see its slots).\n\nGlobal settings should be stored in `browser' instead.\n\nConceptually, it's similar to \\\"private windows\\\" in popular browsers but the\nscope is that of buffers.\"))\n\n(defmethod print-object ((buffer buffer) stream)\n  (print-unreadable-object (buffer stream :type t)\n    (format stream \"~a ~a\" (id buffer) (url buffer))))\n\n(defmethod (setf url) :around (value (buffer document-buffer))\n  (declare (ignore value))\n  (call-next-method)\n  (set-window-title))\n(defmethod (setf title) :around (value (buffer document-buffer))\n  (declare (ignore value))\n  (call-next-method)\n  (set-window-title))\n\n(export-always 'default-modes)\n(defgeneric default-modes (buffer)\n  (:method-combination append)\n  ;; TODO: Add a warning method when passing NIL to guard the current buffer not\n  ;; bound errors?\n  (:method append ((buffer t))\n    %default-modes)\n  (:method append ((buffer buffer))\n    (slot-value buffer 'default-modes))\n  (:method :around ((buffer buffer))\n    \"Remove the duplicates from the `default-modes' and normalize them.\nThis allows setting modes as :DARK-MODE or 'EMACS-MODE in whatever package, and\nNsymbols will find the proper symbol, unless duplicate.\"\n    (mapcar (alex:rcurry #'resolve-user-symbol :mode (list-all-packages))\n            (remove-duplicates (call-next-method)\n                               ;; Modes at the beginning of the list have higher\n                               ;; priority.\n                               :from-end t)))\n  (:documentation \"BUFFER's default modes. `append's all the methods applicable\nto BUFFER to get the full list of modes.\"))\n\n(define-class network-buffer (buffer)\n  ((status\n    :unloaded\n    :type (member :loading :finished :unloaded :failed)\n    :export nil\n    :documentation \"The status of the buffer.\n- `:loading' when loading a web resource.\n- `:finished' when done loading a web resource.\n- `:unloaded' for buffers that have not been loaded yet, like\n  session-restored buffers, dead buffers or new buffers that haven't started the\n  loading process yet.\")\n   (buffer-load-hook\n    (make-instance 'hook-url->url\n                   :combination #'hooks:combine-composed-hook)\n    :type hook-url->url\n    :accessor nil\n    :export nil\n    :documentation \"Hook run in `buffer-load' before loading.\nThe handlers take the URL going to be loaded as argument and must return a\n(possibly new) URL.\")\n   (buffer-loaded-hook\n    (make-instance 'hook-buffer)\n    :type hook-buffer\n    :documentation \"Hook run on `on-signal-load-finished'.\nThe handlers take the buffer as argument.\")\n   (request-resource-keyscheme-map\n    (define-keyscheme-map \"request-resource\" ()\n      keyscheme:default\n      (list\n       \"C-button1\" 'request-resource-open-url\n       \"button2\" 'request-resource-open-url\n       \"C-shift-button1\" 'request-resource-open-url-focus\n       \"shift-button2\" 'request-resource-open-url-focus))\n    :documentation \"Looked up when `request-resource-hook' handlers run.  The\nkeymap takes functions whose key arguments are `:url' and `:buffer'.\")\n   (request-resource-hook\n    (make-instance 'hook-resource\n                   :combination #'combine-composed-hook-until-nil)\n    :type hook-resource\n    :documentation \"Hook run on every resource load.\nThe handlers are composed, passing a `request-data'\nuntil one of them returns nil or all handlers apply successfully.\n\nNewest hook is run first.\nIf a `request-data' object is returned, it gets passed to other handlers\nor right to the renderer if there are no more handlers.\nIf nil is returned, stop the hook and cancel the resource load.\n\nThe current buffer URL should not be relied upon.  With WebKitGTK, it is the\nsame as (url REQUEST-DATA).  If you need to access the URL before this request,\ninspect the document-mode history.\n\nExample:\n\n\\(defmethod configure-instance ((buffer buffer))\n  (reduce #'hooks:add-hook\n          '(old-reddit-handler auto-proxy-handler)\n          :initial-value (request-resource-hook buffer)))\")\n   (proxy\n    nil\n    :accessor nil\n    :type (or proxy null)\n    :documentation \"Proxy for buffer.\")\n   (certificate-exceptions\n    '()\n    :type (list-of string)\n    :documentation \"A list of hostnames for ignoring certificate errors.\"))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:export-predicate-name-p t)\n  (:metaclass user-class)\n  (:documentation \"Buffers that must interact with resources over the network.\"))\n\n(define-class web-buffer\n    (context-buffer network-buffer modable-buffer document-buffer input-buffer)\n  ((keywords\n    nil\n    :reader nil\n    :writer t\n    :documentation \"The keywords parsed from the current web buffer.\"))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:export-predicate-name-p t)\n  (:metaclass user-class)\n  (:documentation \"Buffer for browsing the web.\"))\n\n(defmethod customize-instance :after\n    ((buffer buffer) &key (browser *browser*) no-hook-p\n     &allow-other-keys)\n  \"Finalize buffer.\nReturn the created buffer.\"\n  (unless (or no-hook-p (not browser))\n    (hooks:run-hook (buffer-before-make-hook browser) buffer))\n  buffer)\n\n(defmethod customize-instance :after\n    ((buffer modable-buffer)\n     &key (browser *browser*) no-hook-p extra-modes\n     &allow-other-keys)\n  \"Finalize instantiation of modable BUFFER.\nIn particular,\n- Run `buffer-make-hook';\n- `enable' the modes from the `modes' slot, `default-modes', and the\nEXTRA-MODES\n- Run `buffer-after-make-hook'.\"\n  (unless no-hook-p\n    (hooks:run-hook (buffer-make-hook browser) buffer))\n  (mapc #'enable (modes buffer))\n  (enable-modes* (append (reverse (default-modes buffer))\n                         (uiop:ensure-list extra-modes))\n                 buffer)\n  (unless no-hook-p\n    (hooks:run-hook (buffer-after-make-hook browser) buffer))\n  buffer)\n\n(defmethod customize-instance :after ((buffer context-buffer)\n                                      &key &allow-other-keys)\n  \"Finalize buffer.\nReturn the created buffer.\"\n  (buffer-set (id buffer) buffer)\n  buffer)\n\n(export-always 'update-document-model)\n(defun update-document-model (&key (buffer (current-buffer)))\n  \"Update BUFFER's `document-model' as to include Nyxt identifiers.\"\n  (ps-eval :buffer buffer\n    (defvar nyxt-identifier-counter 0)\n    (defun add-nyxt-identifiers (node)\n      (unless (ps:chain node (has-attribute \"nyxt-identifier\"))\n        (ps:chain node (set-attribute \"nyxt-identifier\"\n                                      (ps:stringify nyxt-identifier-counter))))\n      (incf nyxt-identifier-counter)\n      (dolist\n          (child\n           (if (ps:chain node shadow-root)\n               (ps:chain *array\n                         (from (ps:@ node shadow-root children))\n                         (concat (ps:chain *array (from (ps:@ node children)))))\n                   (ps:chain node children)))\n        (add-nyxt-identifiers child))\n      (when (ps:@ node shadow-root)\n        (ps:chain node (set-attribute \"nyxt-shadow-root\" \"\")))\n      nyxt-identifier-counter)\n    (setf nyxt-identifier-counter\n          (add-nyxt-identifiers (ps:chain document body))))\n  (when-let ((body-json (with-current-buffer buffer\n                          (nyxt/dom::get-document-body-json))))\n    (let ((dom (nyxt/dom::named-json-parse body-json)))\n      (unless (uiop:emptyp (plump:text dom))\n        (when (slot-boundp buffer 'keywords) (setf (keywords buffer) nil))\n        (setf (document-model buffer) dom)))))\n\n(defun dead-buffer-p (buffer)\n  (not (buffer-get (id buffer))))\n\n(defmethod document-model ((buffer buffer) &key use-cached-p)\n  \"A wraparound accessor to BUFFER's `document-model'.\n\nIn case the page changed more than `document-model-delta-threshold', runs\n`update-document-model'.\"\n  (if use-cached-p\n      (slot-value buffer 'document-model)\n      (ps-labels :buffer buffer\n        ((%count-dom-elements\n          ()\n          (defvar dom-counter 0)\n          (defun count-dom-elements (node)\n            (incf dom-counter)\n            (dolist (child (ps:chain node children))\n              (count-dom-elements child))\n            dom-counter)\n          (setf dom-counter 0)\n          (count-dom-elements (nyxt/ps:qs document \"html\"))))\n        (if (dead-buffer-p buffer)\n            (slot-value buffer 'document-model)\n            (let ((value (slot-value buffer 'document-model))\n                  (element-count (%count-dom-elements)))\n              (if (and value\n                       element-count\n                       ;; Check the difference in element count.\n                       (< (abs (- (length (clss:select \"*\" value))\n                                  (truncate element-count)))\n                          (document-model-delta-threshold buffer)))\n                  value\n                  (progn\n                    (update-document-model :buffer buffer)\n                    (slot-value buffer 'document-model))))))))\n\n(defmethod proxy ((buffer buffer))\n  (slot-value buffer 'proxy))\n\n(defmethod (setf proxy) (proxy (buffer buffer))\n  (setf (slot-value buffer 'proxy) proxy)\n  (if proxy\n      (setf (ffi-buffer-proxy buffer)\n            (list (url proxy)\n                  (allowlist proxy)))\n      (setf (ffi-buffer-proxy buffer)\n            (quri:uri \"\"))))\n\n(defmethod keywords ((buffer web-buffer))\n  \"Return the terms that best describe the contents of BUFFER.\"\n  (or (slot-value buffer 'keywords)\n      (when-let ((document (document-model buffer)))\n        (setf (slot-value buffer 'keywords)\n              (analysis:extract-keywords\n               (str:join \" \"\n                         (map 'list #'plump:text\n                              (clss:select \"p\" document))))))))\n\n(define-class keyword-source (prompter:source)\n  ((prompter:name \"Keywords\")\n   (buffer\n    (current-buffer)\n    :type buffer)\n   (prompter:enable-marks-p t)\n   (prompter:constructor (lambda (source)\n                           (mapcar #'first (nyxt::keywords (buffer source))))))\n  (:export-class-name-p t)\n  (:documentation \"Source listing the keywords for source `buffer'.\"))\n\n(-> proxy-url (buffer &key (:downloads-only boolean)) *)\n(defun proxy-url (buffer &key (downloads-only nil))\n  \"Return the proxy address, nil if not set.\nIf DOWNLOADS-ONLY is non-nil, then it only returns the proxy address (if any)\nwhen `proxied-downloads-p' is true.\"\n  (let* ((proxy (and buffer (proxy buffer)))\n         (proxied-downloads (and proxy (proxied-downloads-p proxy))))\n    (when (or (and (not downloads-only) proxy)\n              proxied-downloads)\n      (url proxy))))\n\n(defun load-failed-p (buffer)\n  \"Only `network-buffer' loads can fail.\"\n  (and (network-buffer-p buffer)\n       (eq (slot-value buffer 'status) :failed)))\n\n(hooks:define-hook-type buffer (function (buffer))\n  \"Hook acting on `buffer's.\")\n\n(define-command make-buffer (&rest args &key (title \"\") modes\n                             (url (if *browser*\n                                      (default-new-buffer-url *browser*)\n                                      (quri:uri (nyxt-url 'new))))\n                             (load-url-p t) (buffer-class 'web-buffer)\n                             &allow-other-keys)\n  \"Create a new buffer.\nMODES is a list of mode symbols.\nIf URL is empty, the `default-new-buffer-url' browser slot is used instead.\nTo load nothing, set it to 'about:blank'.\nLOAD-URL-P controls whether to load URL right at buffer creation.\"\n  (let* ((url (url url))\n         (buffer (apply #'make-instance\n                        buffer-class\n                        :title title\n                        :extra-modes modes\n                        (append (unless (url-empty-p url) (list :url url))\n                                (uiop:remove-plist-keys '(:title :modes :url)\n                                                        args)))))\n    (when load-url-p\n      (ffi-buffer-load buffer url))\n    buffer))\n\n(define-command make-buffer-focus (&key (url (default-new-buffer-url *browser*)))\n  \"Switch to a new buffer.\nSee `make-buffer'.\"\n  (let ((buffer (make-buffer :url url)))\n    (set-current-buffer buffer)\n    buffer))\n\n(-> add-to-recent-buffers (buffer) *)\n(defun add-to-recent-buffers (buffer)\n  \"Push BUFFER to the front of `recent-buffers'.\nThe notion of first element is dictated by `containers:first-item'.\"\n  (when (web-buffer-p buffer)\n    (containers:delete-item (recent-buffers *browser*) buffer)\n    (containers:insert-item (recent-buffers *browser*) buffer)))\n\n(export-always 'buffer-list)\n(defun buffer-list ()\n  \"Order is stable.\"\n  (sort (alex:hash-table-values (buffers *browser*))\n        #'>\n        :key #'id))\n\n(export-always 'internal-buffers)\n(defun internal-buffer-list (&key (all nil))\n  ;; Note that the `buffers' slot only keeps track of \"main\" buffers.\n  (append (sera:filter #'internal-url-p (buffer-list))\n          (when all\n            (alex:flatten (loop for window in (window-list)\n                                collect (active-prompt-buffers window)\n                                collect (status-buffer window)\n                                collect (message-buffer window))))))\n\n(defun buffer-get (id)\n  \"Get the `buffer' with the corresponding ID.\"\n  (or (gethash id (slot-value *browser* 'buffers))\n      (find-if\n       (lambda (prompt-buffer) (eql (id prompt-buffer) id))\n       (mapcan\n        #'active-prompt-buffers\n        (alexandria:hash-table-values (windows *browser*))))))\n\n(defun buffer-set (id buffer)\n  \"Ensure that entry ID->BUFFER belongs to `buffers' hash table.\"\n  (when *browser*\n    ;; Mutate state of the hash table.\n    (setf (gethash id (slot-value *browser* 'buffers)) buffer)\n    ;; Notify `buffers' of the new hash table state. Useful, for example, to\n    ;; update the status buffer.\n    (setf (buffers *browser*) (buffers *browser*))))\n\n(defun buffer-delete (id)\n  \"Remove `buffers' hash table entry matching key ID.\n\nThis is a low-level function.  See `buffer-delete' and `delete-buffer'.\"\n  ;; Mutate state of the hash table.\n  (when *browser*\n    (remhash id (slot-value *browser* 'buffers))\n    ;; Notify `buffers' of the new hash table state. Useful, for example, to\n    ;; update the status buffer.\n    (setf (buffers *browser*) (buffers *browser*))))\n\n(export-always 'window-list)\n(defun window-list ()\n  \"Return a list of all the open `windows'.\"\n  (when *browser*\n    (alex:hash-table-values (windows *browser*))))\n\n(defmethod window ((buffer buffer))\n  \"Get the window containing a buffer.\"\n  (find buffer (alex:hash-table-values (windows *browser*))\n        :key #'active-buffer))\n\n(defun last-active-buffer ()\n  \"Return buffer with most recent `last-access'.\"\n  (first (sort-by-time (buffer-list))))\n\n(defmethod active-buffer-p ((buffer buffer))\n  (find buffer (mapcar #'active-buffer (window-list))))\n\n(defun get-inactive-buffer ()\n  \"Return inactive buffers sorted by `last-access', when applicable.\nIf none exist, make a new inactive buffer.\"\n  (if-let ((inactive (set-difference (buffer-list)\n                                     (mapcar #'active-buffer (window-list)))))\n    (first (sort-by-time inactive))\n    (make-buffer)))\n\n(define-command copy-url ()\n  \"Save current URL to clipboard.\"\n  (echo \"~s copied to clipboard.\"\n        (copy-to-clipboard (render-url (url (current-buffer))))))\n\n(define-command copy-title ()\n  \"Save current page title to clipboard.\"\n  (echo \"~a copied to clipboard.\"\n        (copy-to-clipboard (title (current-buffer)))))\n\n(define-class buffer-source (prompter:source)\n  ((prompter:name \"Buffer list\")\n   (prompter:constructor (append (list (active-buffer (current-window)))\n                                 (remove (active-buffer (current-window))\n                                         (buffer-list))))\n   (prompter:filter-preprocessor #'prompter:filter-exact-matches)\n   (prompter:enable-marks-p t)\n   (prompter:actions-on-return (list (lambda-unmapped-command set-current-buffer)\n                                     (lambda-mapped-command ffi-buffer-delete)\n                                     'reload-buffers))\n   (prompter:actions-on-current-suggestion-enabled-p t)\n   (prompter:actions-on-current-suggestion\n    (lambda-command set-current-buffer* (buffer)\n      \"Set current BUFFER for the active window.\"\n      (set-current-buffer buffer :focus nil)))\n   (prompter:destructor (let ((buffer (current-buffer)))\n                          (lambda (prompter source)\n                            (declare (ignore source))\n                            (unless (or (prompter:returned-p prompter)\n                                        (eq buffer (current-buffer)))\n                              (set-current-buffer buffer)))))\n   (prompter:active-attributes-keys\n    '(\"Title\" \"URL\" \"Keywords\")\n    :accessor nil))\n  (:export-class-name-p t)\n  (:metaclass user-class)\n  (:documentation \"Source for choosing one (or several) of the open buffers.\n\nThe `prompter:actions-on-current-suggestion' are set up to preview/switch to the\nbuffer currently chosen as suggestion.\"))\n\n(defmethod prompter:object-attributes ((buffer buffer) (source prompter:source))\n  (declare (ignore source))\n  `((\"Title\" ,(title buffer) (:width 3))\n    (\"URL\" ,(render-url (url buffer)) (:width 2))\n    ,(when (web-buffer-p buffer)\n       `(\"Keywords\" ,(format nil \"~:{~a~^ ~}\" (keywords buffer)) (:width 2)))\n    (\"ID\" ,(id buffer) (:width 1))))\n\n(define-command switch-buffer ()\n  \"Switch buffer using fuzzy completion.\"\n  (prompt\n   :prompt \"Switch to buffer\"\n   :sources (make-instance 'buffer-source)))\n\n(define-command switch-buffer-domain (&key domain (buffer (current-buffer)))\n  \"Switch to buffer sharing the same domain as the current one.\"\n  (let ((domain (or domain (quri:uri-domain (url buffer)))))\n    (prompt\n     :prompt \"Switch to buffer in current domain\"\n     :sources (make-instance 'buffer-source\n                             :constructor\n                             (sera:filter (match-domain domain)\n                                          (sort-by-time (buffer-list)))))))\n\n(define-command toggle-prompt-buffer-focus ()\n  \"Toggle the focus between the current buffer and the current prompt buffer.\"\n  (let ((prompt-buffer (current-prompt-buffer)))\n    (if (ffi-focused-p prompt-buffer)\n        (prog1 (ffi-focus-buffer (current-buffer))\n          (ps-eval :buffer prompt-buffer\n            (setf (ps:@ (nyxt/ps:qs document \"*\") style opacity) \"0.5\")))\n        (prog1 (ffi-focus-buffer prompt-buffer)\n          (ps-eval :buffer prompt-buffer\n            (setf (ps:@ (nyxt/ps:qs document \"*\") style opacity) \"1\"))))))\n\n\n(flet ((delete-all (buffers &optional predicate)\n         (mapcar #'ffi-buffer-delete\n                 (sera:filter (or predicate #'identity) buffers))))\n  (define-command delete-buffer\n      (&key\n       (buffers\n        (prompt\n         :prompt \"Delete buffer(s)\"\n         :sources (make-instance\n                   'buffer-source\n                   :enable-marks-p t\n                   :actions-on-return\n                   (list\n                    (lambda-mapped-command ffi-buffer-delete)\n                    (lambda-command buffer-delete-duplicates* (buffers)\n                      \"Delete all buffers with same URLs, except selected.\"\n                      (delete-all\n                       (set-difference (buffer-list) buffers)\n                       (lambda (buffer)\n                         (member (url buffer) buffers\n                                 :key #'url :test #'quri:uri-equal))))\n                    (lambda-command buffer-delete-same-host* (buffers)\n                      \"Delete all the buffers with the same website open.\"\n                      (delete-all\n                       (buffer-list)\n                       (lambda (buffer)\n                         (member (quri:uri-host (url buffer))\n                                 (mapcar #'url buffers)\n                                 :key #'quri:uri-host\n                                 :test #'string-equal))))\n                    (lambda-command buffer-delete-same-url* (buffers)\n                      \"Delete all the buffers with the same page open.\"\n                      (delete-all\n                       (buffer-list)\n                       (lambda (buffer)\n                         (member (url buffer) buffers\n                                 :key #'url :test #'quri:uri-equal)))))))\n        buffers-supplied-p))\n    \"Query the buffer(s) to delete.\n\nBUFFERS should be a list of `buffer's.\"\n    (when buffers-supplied-p\n      (delete-all (uiop:ensure-list buffers)))))\n\n(define-command delete-all-buffers ()\n  \"Delete all buffers, with confirmation.\"\n  (if-confirm ((format nil \"Delete ~a buffer(s)?\" (length (buffer-list))))\n      (mapcar #'ffi-buffer-delete (buffer-list))))\n\n(define-command delete-current-buffer ()\n  \"Delete the current buffer and switch to the last visited one.\nIf no other buffers exist, load the start page.\"\n  (ffi-buffer-delete (current-buffer)))\n\n(define-command delete-other-buffers (&optional (buffer (current-buffer)))\n  \"Delete all buffers except BUFFER.\nWhen BUFFER is omitted, it defaults to the current one.\"\n  (let ((buffers-to-delete (remove buffer (buffer-list))))\n    (if-confirm ((format nil \"Delete ~a buffer(s)?\" (length buffers-to-delete)))\n        (mapcar #'ffi-buffer-delete buffers-to-delete))))\n\n;; Useful to be used by prompt buffer actions, since they take a list as\n;; argument.\n(export-always 'buffer-load*)\n(defun buffer-load* (url-list)\n  \"Load first element of URL-LIST in current buffer and the rest in new buffers.\"\n  (mapc (lambda (url) (make-buffer :url (url url))) (rest url-list))\n  (ffi-buffer-load (current-buffer) (url (first url-list))))\n\n(define-class global-history-source (prompter:source)\n  ((prompter:name \"Global history\")\n   (prompter:constructor (recent-history-entries 200 *browser* :deduplicate-p t))\n   (prompter:enable-marks-p t)\n   (prompter:filter-preprocessor #'prompter:filter-exact-matches)\n   (prompter:actions-on-return #'buffer-load*))\n  (:export-class-name-p t)\n  (:metaclass user-class)\n  (:documentation \"Source listing all the entries in history.\nLoads the entry with default `prompter:actions-on-return'.\"))\n\n(define-class url-or-query ()\n  ((data\n    \"\"\n    :type string\n    :documentation \"A string to be resolved to a URL via `url'.\")\n   (kind\n    :initarg nil\n    :type (maybe keyword)\n    :documentation \"A keyword that classifies `data' based on its content.\nOne of `:url' or `:search-query'.\")\n   (search-engine\n    :type (maybe search-engine)\n    :documentation \"Applicable when `kind' is `:search-query'.\")\n   (search-query\n    :initarg nil\n    :type (maybe string)\n    :documentation \"Applicable when `kind' is `:search-query'.\"))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:documentation \"Helper structure that resolves user input to a URL.\n\nDetermine whether a valid https URL, local file or a search engine query is\nrequested.  When the first word of `data' matches the `shortcut' of a\n`search-engine', then it is interpreted as a search engine query.\"))\n\n(defmethod print-object ((query url-or-query) stream)\n  (print-unreadable-object (query stream :type t)\n    (format stream \"~a\" (data query))))\n\n(defmethod initialize-instance :after ((query url-or-query)\n                                       &key &allow-other-keys)\n  (with-slots (data kind search-engine search-query) query\n    (setf data (str:trim data))\n    (cond ((str:blankp data) t)\n          ((valid-url-p data :check-tld-p nil) (setf kind :url))\n          ((ignore-errors (valid-url-p (str:concat \"https://\" data)\n                                       :check-tld-p t))\n           (setf kind :url\n                 data (str:concat \"https://\" data)))\n          ((uiop:file-exists-p data)\n           (setf kind :url\n                 data (str:concat \"file://\" (uiop:native-namestring data))))\n          (t\n           (let* ((terms (sera:tokens data))\n                  (explicit-engine\n                    (find (first terms) (search-engines *browser*)\n                          :key #'shortcut :test #'string-equal))\n                  (engine (or explicit-engine\n                              (default-search-engine *browser*))))\n             (setf kind :search-query\n                   search-engine engine)\n             (if explicit-engine\n                 (setf search-query (str:join \" \" (rest terms)))\n                 (setf search-query data\n                       data (format-query data engine))))))))\n\n(export-always 'search-suggestions)\n(defmethod search-suggestions ((query url-or-query))\n  (with-slots (search-engine search-query) query\n    (when search-engine\n      (let ((suggestions (suggestions search-query search-engine)))\n        (mapcar (lambda (suggestion)\n                  (make-instance 'url-or-query\n                                 :data (format-query suggestion search-engine)))\n                ;; Ensure that search-query is the first suggestion.\n                (if (string-equal search-query (first suggestions))\n                    suggestions\n                    (append (list search-query) suggestions)))))))\n\n(defmethod url ((query url-or-query))\n  (with-slots (data kind search-engine search-query) query\n    (quri:uri (if (eq :search-query kind)\n                  (format-url search-query search-engine)\n                  data))))\n\n(define-class url-or-query-source (prompter:source)\n  ((prompter:name \"URL or search query\")\n   (prompter:filter-preprocessor\n    (lambda (suggestions source input)\n      (declare (ignore suggestions source))\n      (list (make-instance 'url-or-query :data input))))\n   (prompter:filter-postprocessor\n    (lambda (prompt-suggestions source input)\n      (declare (ignore source input))\n      (sleep 0.15) ; Delay search suggestions while typing.\n      (if-let ((_ (search-engine-suggestions-p *browser*))\n               (completion (search-suggestions\n                            (prompter:value (first prompt-suggestions)))))\n        completion\n        prompt-suggestions)))\n   (prompter:filter nil)\n   (prompter:actions-on-return #'buffer-load*))\n  (:export-class-name-p t)\n  (:metaclass user-class)\n  (:documentation \"Source listing URL queries from user input in a DWIM fashion.\nSee `url-or-query'.\"))\n\n(defmethod prompter:object-attributes ((query url-or-query)\n                                       (source url-or-query-source))\n  (declare (ignore source))\n  (with-slots (data kind search-engine search-query) query\n    `((\"Input\" ,(or search-query data) (:width 5))\n      (\"Type\" ,(cond ((null kind) \"\")\n                     ((eq kind :search-query) (name search-engine))\n                     (t kind))\n              (:width 2)))))\n\n(export-always 'url-sources)\n(defmethod url-sources ((buffer buffer) actions-on-return)\n  \"Return list of `set-url' sources.\nThe returned sources should have `url' or `prompter:actions-on-return' methods\nspecified for their contents.\"\n  (let ((actions-on-return (uiop:ensure-list actions-on-return)))\n    (append\n     (list (make-instance 'url-or-query-source\n                          :actions-on-return actions-on-return)\n           (make-instance 'global-history-source\n                          :actions-on-return actions-on-return))\n     (mappend (rcurry #'url-sources (uiop:ensure-list actions-on-return))\n              (enabled-modes buffer)))))\n\n(define-command set-url (&key (default-action #'buffer-load*))\n  \"Set the URL for the current buffer, completing with history.\"\n  (let* ((history (set-url-history *browser*))\n         (actions-on-return\n           (list #'buffer-load*\n                 (lambda-command copy-url* (suggestions)\n                   \"Copy the URL of the chosen suggestion.\"\n                   (trivial-clipboard:text\n                    (render-url (url (first suggestions))))))))\n    (pushnew default-action actions-on-return)\n    (prompt :prompt \"Open URL\"\n            :input (render-url (url (current-buffer)))\n            :history history\n            :sources (url-sources (current-buffer) actions-on-return))\n    (current-buffer)))\n\n(define-command set-url-new-buffer ()\n  (set-url :default-action\n           (lambda-command new-buffer-load* (suggestion-values)\n             \"Load URL(s) in new buffer(s).\"\n             (mapc (lambda (suggestion) (make-buffer :url (url suggestion)))\n                   (rest suggestion-values))\n             (make-buffer-focus :url (url (first suggestion-values))))))\n\n(define-command reload-current-buffer ()\n  \"Reload current buffer.\nReturn it.\"\n  (ffi-buffer-reload (current-buffer)))\n\n(define-command reload-buffers\n    (&optional (buffers\n                (prompt\n                 :prompt \"Reload buffer(s)\"\n                 :sources (make-instance 'buffer-source :enable-marks-p t))))\n  \"Prompt for BUFFERS to be reloaded.\nReturn BUFFERS.\"\n  (mapcar #'ffi-buffer-reload (alex:ensure-list buffers))\n  buffers)\n\n(define-command switch-buffer-previous (&key (offset 1) (buffer (current-buffer)))\n  \"Switch to the previous buffer.\"\n  (let ((buffer-list (buffer-list)))\n    (set-current-buffer (nth (mod (+ offset (position buffer buffer-list))\n                                  (length buffer-list))\n                             buffer-list))))\n\n(define-command switch-buffer-next\n    (&key (offset 1) (buffer (current-buffer)))\n  \"Switch to the next buffer.\"\n  (switch-buffer-previous :offset (- offset) :buffer buffer))\n\n(define-command switch-buffer-last ()\n  \"Switch to the last visited buffer.\n\nThe buffer with the most recent access time is returned.\"\n  (when-let ((buffer (second (sort-by-time (buffer-list)))))\n    (set-current-buffer buffer)))\n\n(define-command open-inspector ()\n  \"Open the inspector, a graphical tool to inspect the buffer.\"\n  (ffi-inspector-show (current-buffer))\n  (current-buffer))\n"
  },
  {
    "path": "source/clipboard.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt)\n\n(-> ring-insert-clipboard (containers:ring-buffer-reverse) (maybe string))\n(export-always 'ring-insert-clipboard)\n(defun ring-insert-clipboard (ring)\n  \"Check if clipboard-content is most recent entry in RING.\nIf not, insert clipboard-content into RING.\nReturn most recent entry in RING.\"\n  (let ((clipboard-content (handler-case (trivial-clipboard:text)\n                             (uiop:subprocess-error ()\n                               nil))))\n    (when clipboard-content\n      (unless (string= clipboard-content (unless (containers:empty-p ring)\n                                           (containers:first-item ring)))\n        (containers:insert-item ring clipboard-content)))\n    (unless (containers:empty-p ring)\n      (string (containers:first-item ring)))))\n\n(export-always 'copy-to-clipboard)\n(defun copy-to-clipboard (input)\n  \"Save INPUT text to clipboard, and ring.\"\n  (containers:insert-item (clipboard-ring *browser*)\n                          (trivial-clipboard:text input)))\n"
  },
  {
    "path": "source/color.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt)\n\n(defvar *css-colors*\n  '(\"AliceBlue\" \"AntiqueWhite\" \"Aqua\" \"Aquamarine\" \"Azure\" \"Beige\" \"Bisque\"\n    \"Black\" \"BlanchedAlmond\" \"Blue\" \"BlueViolet\" \"Brown\" \"BurlyWood\" \"CadetBlue\"\n    \"Chartreuse\" \"Chocolate\" \"Coral\" \"CornflowerBlue\" \"Cornsilk\" \"Crimson\" \"Cyan\"\n    \"DarkBlue\" \"DarkCyan\" \"DarkGoldenRod\" \"DarkGray\" \"DarkGrey\" \"DarkGreen\"\n    \"DarkKhaki\" \"DarkMagenta\" \"DarkOliveGreen\" \"DarkOrange\" \"DarkOrchid\" \"DarkRed\"\n    \"DarkSalmon\" \"DarkSeaGreen\" \"DarkSlateBlue\" \"DarkSlateGray\" \"DarkSlateGrey\"\n    \"DarkTurquoise\" \"DarkViolet\" \"DeepPink\" \"DeepSkyBlue\" \"DimGray\" \"DimGrey\"\n    \"DodgerBlue\" \"FireBrick\" \"FloralWhite\" \"ForestGreen\" \"Fuchsia\" \"Gainsboro\"\n    \"GhostWhite\" \"Gold\" \"GoldenRod\" \"Gray\" \"Grey\" \"Green\" \"GreenYellow\" \"HoneyDew\"\n    \"HotPink\" \"IndianRed\" \"Indigo\" \"Ivory\" \"Khaki\" \"Lavender\" \"LavenderBlush\"\n    \"LawnGreen\" \"LemonChiffon\" \"LightBlue\" \"LightCoral\" \"LightCyan\"\n    \"LightGoldenRodYellow\" \"LightGray\" \"LightGrey\" \"LightGreen\" \"LightPink\"\n    \"LightSalmon\" \"LightSeaGreen\" \"LightSkyBlue\" \"LightSlateGray\" \"LightSlateGrey\"\n    \"LightSteelBlue\" \"LightYellow\" \"Lime\" \"LimeGreen\" \"Linen\" \"Magenta\" \"Maroon\"\n    \"MediumAquaMarine\" \"MediumBlue\" \"MediumOrchid\" \"MediumPurple\" \"MediumSeaGreen\"\n    \"MediumSlateBlue\" \"MediumSpringGreen\" \"MediumTurquoise\" \"MediumVioletRed\"\n    \"MidnightBlue\" \"MintCream\" \"MistyRose\" \"Moccasin\" \"NavajoWhite\" \"Navy\" \"OldLace\"\n    \"Olive\" \"OliveDrab\" \"Orange\" \"OrangeRed\" \"Orchid\" \"PaleGoldenRod\" \"PaleGreen\"\n    \"PaleTurquoise\" \"PaleVioletRed\" \"PapayaWhip\" \"PeachPuff\" \"Peru\" \"Pink\" \"Plum\"\n    \"PowderBlue\" \"Purple\" \"RebeccaPurple\" \"Red\" \"RosyBrown\" \"RoyalBlue\"\n    \"SaddleBrown\" \"Salmon\" \"SandyBrown\" \"SeaGreen\" \"SeaShell\" \"Sienna\" \"Silver\"\n    \"SkyBlue\" \"SlateBlue\" \"SlateGray\" \"SlateGrey\" \"Snow\" \"SpringGreen\" \"SteelBlue\"\n    \"Tan\" \"Teal\" \"Thistle\" \"Tomato\" \"Turquoise\" \"Violet\" \"Wheat\" \"White\"\n    \"WhiteSmoke\" \"Yellow\" \"YellowGreen\")\n  \"All the named CSS colors to construct `color-source' from.\")\n\n(defvar copy-actions\n  (list (lambda-command copy-as-hex* (colors)\n          \"Copy the color as hex #XXXXXX string.\"\n          (let ((hex (cl-colors-ng:print-hex (first colors))))\n            (ffi-buffer-copy (current-buffer) hex)\n            (echo \"~s copied to clipboard.\" hex)))\n        (lambda-command copy-as-rgb* (colors)\n          \"Copy the color as CSS rgb() function string.\"\n          (let ((rgb (cl-colors-ng:print-css-rgb/a (first colors))))\n            (ffi-buffer-copy (current-buffer) rgb)\n            (echo \"Copied ~a to clipboard!\" rgb)))\n        (lambda-command copy-as-hsl* (colors)\n          \"Copy the color as CSS hsl() function string.\"\n          (let ((hsl (cl-colors-ng:print-css-hsl (first colors))))\n            (ffi-buffer-copy (current-buffer) hsl)\n            (echo \"Copied ~a to clipboard!\" hsl)))))\n\n(export-always 'color-source)\n(define-class color-source (prompter:source)\n  ((prompter:name \"Color\")\n   (prompter:constructor *css-colors*)\n   (prompter:filter-preprocessor #'prompter:filter-exact-matches)\n   (prompter:filter-postprocessor\n    (lambda (suggestions source input-color)\n      (unless (str:empty? input-color) (sleep 0.2))\n      (append (when (ignore-errors (cl-colors-ng:as-rgb input-color))\n                (list (make-instance 'prompter:suggestion\n                                     :value input-color\n                                     :attributes (prompter:object-attributes input-color source))))\n            suggestions)))\n   (prompter:actions-on-current-suggestion-enabled-p t)\n   (prompter:actions-on-return\n    (cons #'identity copy-actions)))\n  (:documentation \"A source for color search and copying.\nAllows looking through the colors based on their names, HEX values, and\nrgb()/hsl() CSS functions representing them.\"))\n\n(defmethod prompter:object-attributes ((color string) (source color-source))\n  `((\"Color\" ,color)\n    (\"HEX\" ,(cl-colors-ng:print-hex color))\n    (\"RGB\" ,(cl-colors-ng:print-css-rgb/a color))))\n\n(define-command-global pick-color ()\n  \"Pick a color and copy it to clipboard.\nThe current color is previewed in the prompt buffer's input area.\n\nColor can be entered as:\n- CSS color name: \\\"PapayaWhip\\\" (capitalization is optional.)\n- HEX code: \\\"#37A8E4\\\".\n- HSL and RGB functions inspired by CSS.\"\n  (prompt :prompt \"Color\"\n          :sources (make-instance 'color-source\n                                  :actions-on-return copy-actions)))\n"
  },
  {
    "path": "source/command-commands.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt)\n\n(define-class hook-description ()\n  ((name\n    \"\"\n    :documentation \"The hook name.\")\n   (value\n    nil\n    :documentation \"The hook value.\")))\n\n(defun command-attributes\n    (command &optional (buffer (active-buffer (current-window :no-rescan))))\n  (let ((command-name (name command)))\n    `((\"Name\" ,(string-downcase command-name) (:width 1))\n      (\"Bindings\" ,(format nil \"~{~a~^, ~}\"\n                              (keymaps:pretty-binding-keys\n                               command-name\n                               (current-keymaps buffer)\n                               :print-style (keymaps:name (keyscheme buffer))))\n                     (:width 1))\n      (\"Docstring\" ,(documentation-line command 'function \"\") (:width 4))\n      (\"Mode\" ,(let ((package-name (uiop:symbol-package-name command-name)))\n                 (if (str:starts-with-p \"NYXT/MODE/\" package-name)\n                     (string-downcase (str:replace-first \"NYXT/MODE/\" \"\"\n                                                         package-name))\n                     \"\"))\n              (:width 1)))))\n\n(define-class command-source (prompter:source)\n  ((prompter:name \"Commands\")\n   (global-p\n    t\n    :type boolean\n    :documentation \"Whether global commands are included in the suggestions.\")\n   (buffer\n    (current-buffer)\n    :type buffer)\n   (prompter:constructor\n    (lambda (source)\n      (sort-by-time\n       (list-commands\n        :global-p (global-p source)\n        :mode-symbols (mapcar #'sera:class-name-of\n                              (sera:filter #'enabled-p\n                                           (enabled-modes (buffer source))))))))\n   (prompter:active-attributes-keys\n    '(\"Name\" \"Bindings\" \"Docstring\")\n    :accessor nil)\n   (prompter:filter-preprocessor #'prompter:filter-exact-matches))\n  (:export-class-name-p t)\n  (:documentation \"Prompter source to execute commands.\nGlobal commands are listed if `global-p' is non-nil.\nMode commands of enabled modes are also listed.\nWhile disabled-mode commands are not listed, it's still possible to call them\nfrom a key binding.\")\n  (:metaclass user-class))\n\n(defmethod predict-next-command ((browser browser))\n  (when-let ((prediction (analysis:predict (command-model browser)\n                                           (list (last-command browser)))))\n    (analysis:element prediction)))\n\n(define-class predicted-command-source (prompter:source)\n  ((prompter:name \"Predicted Command\")\n   (prompter:constructor\n    (lambda (source)\n      (declare (ignore source))\n      (list (predict-next-command *browser*))))\n   (prompter:filter-preprocessor #'prompter:filter-exact-matches))\n  (:export-class-name-p t)\n  (:documentation \"Prompter source to predict commands.\")\n  (:metaclass user-class))\n\n(define-command execute-predicted-command ()\n  \"Execute the predicted next command.\"\n  (run-async (predict-next-command *browser*)))\n\n(defmethod prompter:object-attributes\n    ((command command) (source prompter:source))\n  (declare (ignore source))\n  (command-attributes command))\n\n(define-command execute-command ()\n  \"Execute a command by name.\n\nAlso accepts arbitrary Lisp expressions (even without the outermost level of\nparentheses), and lists the possible completions for the incomplete symbols,\ntogether with the arglists and documentations of the functions typed in.\"\n  (unless (active-prompt-buffers (current-window))\n    (prompt\n     :prompt \"Execute command\"\n     :sources\n     (list (make-instance\n            'command-source\n            :actions-on-return\n            (list (lambda-command run-command* (commands)\n                    \"Run the chosen command.\"\n                    (let ((command (first commands)))\n                      (setf (last-access command) (local-time:now))\n                      (run-async command)))\n                  (lambda-command describe-command* (commands)\n                    \"Show the documentation and properties of this command.\"\n                    (describe-command :command (name (first commands))))))\n           (make-instance\n            'predicted-command-source\n            :actions-on-return\n            (lambda-command run-command* (commands)\n              \"Run the chosen command.\"\n              (when-let ((command (first commands)))\n                (setf (last-access command) (time:now))\n                (run-async command)))))\n     :hide-suggestion-count-p t)))\n\n(defun get-hooks ()\n  (flet ((list-hooks (object)\n           (mapcar (lambda (hook)\n                     (make-instance\n                      'hook-description\n                      :name (str:downcase (closer-mop:slot-definition-name hook))\n                      :value (funcall (symbol-function\n                                       (closer-mop:slot-definition-name hook))\n                                      object)))\n                   (remove-if-not\n                    (lambda (s)\n                      (let ((name (closer-mop:slot-definition-name s)))\n                        (and (str:ends-with-p \"-hook\" (string name)\n                                              :ignore-case t)\n                             (fboundp name))))\n                    (closer-mop:class-slots (class-of object))))))\n    (let ((window-hooks (list-hooks (current-window)))\n          (buffer-hooks (list-hooks (current-buffer)))\n          (browser-hooks (list-hooks *browser*)))\n      (append window-hooks\n              buffer-hooks\n              browser-hooks))))\n\n(define-class hook-source (prompter:source)\n  ((prompter:name \"Hooks\")\n   (prompter:constructor (get-hooks))\n   (prompter:actions-on-return (lambda-mapped-command value))))\n\n(defmethod prompter:object-attributes\n    ((hook-description hook-description) (source hook-source))\n  (declare (ignore source))\n  `((\"Name\" ,(name hook-description))))\n\n(define-class handler-source (prompter:source)\n  ((prompter:name \"Handlers\")\n   (hook\n    nil\n    :documentation \"The hook for which to retrieve handlers for.\")\n   (prompter:constructor (lambda (source) (hooks:handlers (hook source))))))\n\n(defmethod prompter:object-attributes ((handler symbol) (source handler-source))\n  (declare (ignore source))\n  `((\"Name\" ,(str:downcase (hooks:name handler)))))\n\n(define-class disabled-handler-source (handler-source)\n  ((prompter:constructor\n    (lambda (source) (hooks:disabled-handlers (hook source))))))\n\n(defun manage-hook-handler (action)\n  (let ((hook (prompt1 :prompt \"Hook\"\n                       :sources 'hook-source)))\n    (funcall (case action\n               (:enable #'hooks:enable-hook)\n               (:disable #'hooks:disable-hook))\n             hook\n             (prompt1 :prompt \"Handler\"\n                      :sources (make-instance\n                                (case action\n                                  (:enable 'disabled-handler-source)\n                                  (:disable 'handler-source))\n                                :hook hook)))))\n\n(define-command-global disable-hook-handler ()\n  \"Remove handler of a hook.\"\n  (manage-hook-handler :disable))\n\n(define-command-global enable-hook-handler ()\n  \"Add handler of a hook.\"\n  (manage-hook-handler :enable))\n"
  },
  {
    "path": "source/command.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt)\n\n(defvar *command-list* '()\n  \"The list of known commands, for internal use only.\")\n\n(define-class command (standard-generic-function)\n  ((visibility\n    :mode\n    :type (member :global :mode :anonymous)\n    :reader t\n    :writer nil\n    :documentation \"Sets whether command is listed in `command-source'.\n\n- `:global' always lists it.  This is mostly useful for third-party packages to\ndefine globally-accessible commands without polluting the official Nyxt\npackages.\n\n- `:mode' lists it when the corresponding mode is active.\n\n- `:anonymous' never lists it.\")\n   (last-access\n    (time:now)\n    :type time:timestamp\n    :documentation \"Last time this command was called from prompt buffer.\nUseful to sort the commands by most recent use.\"))\n  (:metaclass closer-mop:funcallable-standard-class)\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:export-predicate-name-p t)\n  (:documentation \"Commands are interactive functions.\n(As in Emacs.)\n\nCommands are funcallable.\n\nWe need a `command' class for multiple reasons:\n- Identify commands uniquely.\n\n- Customize prompt buffer display value with properties.\n\n- Last access: This is useful to sort command by the time they were last\n  called.  The only way to do this is to persist the command instances.\n\nSince they are generic functions, they can be specialize with `:before',\n`:after' and `:around' qualifiers, effectively acting as hooks.\nThese specializations are reserved to the user.\"))\n\n(defmethod name ((command command))\n  \"A useful shortcut.\"\n  (closer-mop:generic-function-name command))\n\n(defun initialize-command (command lambda-expression)\n  (when (uiop:emptyp (closer-mop:generic-function-name command))\n    (alex:required-argument 'name))\n  (when lambda-expression\n    ;; `closer-mop:ensure-method' calls `add-method' which reinitializes the\n    ;; command / generic function, thus running `initialize-command' twice each\n    ;; time.  TODO: Can we avoid this?\n    (closer-mop:ensure-method command lambda-expression)\n    (when (uiop:emptyp (documentation command t))\n      (let ((doc (nth-value 2 (alex:parse-body (rest (rest lambda-expression)) :documentation t))))\n        (if (and (uiop:emptyp doc)\n                 (not (eq :anonymous (visibility command))))\n            (error \"Command ~a requires documentation.\" (name command))\n            (setf (documentation command 'function) doc)))))\n  (unless (eq :anonymous (visibility command))\n    ;; Overwrite previous command:\n    (setf *command-list* (delete (closer-mop:generic-function-name command) *command-list*\n                                 :key #'closer-mop:generic-function-name))\n    (push command *command-list*)))\n\n(defmethod initialize-instance :after ((command command) &key lambda-expression &allow-other-keys)\n  (initialize-command command lambda-expression))\n(defmethod reinitialize-instance :after ((command command) &key lambda-expression &allow-other-keys)\n  (initialize-command command lambda-expression))\n\n(defun find-command (name)\n  (find name *command-list* :key #'name))\n\n;; TODO: Can we use `alex:named-lambda'?  How do we get the name then?\n(export-always 'make-command)\n(defun make-command (name lambda-expression &optional (visibility :anonymous))\n  \"Return an non-globally defined command named NAME.\"\n  (make-instance 'command :name name\n                          :lambda-expression lambda-expression\n                          :visibility visibility))\n\n(export-always 'lambda-command)\n(defmacro lambda-command (name args &body body)\n  \"ARGS may only be a list of required arguments (optional and keyword argument\nnot allowed).\n\nExample:\n\n\\(let ((source (make-my-source)))\n  (lambda-command open-file* (files)\n    \\\"Open files in some way.\\\"\n    ;; Note that `source' is captured in the closure.\n    (mapc (opener source) files)))\"\n  (let ((doc (nth-value 2 (alex:parse-body body :documentation t))))\n    (alex:with-gensyms (closed-over-body)\n      ;; Warning: `make-command' takes a lambda-expression as an unevaluated list,\n      ;; thus the BODY environment is not that of the lexical environment\n      ;; (closures would thus fail to close over).  To avoid this problem, we capture\n      ;; the lexical environment in a lambda.\n      ;;\n      ;; Note that this relies on the assumption that ARGS is just a list of\n      ;; _required arguments_, which is a same assumption for prompt buffer actions.\n      ;; We could remove this limitation with some argument parsing.\n      `(let ((,closed-over-body (lambda ,args ,@body)))\n         (make-command ',name\n                       (list 'lambda ',args ,doc (list 'apply ,closed-over-body  '(list ,@args))))))))\n\n(export-always 'lambda-mapped-command)\n(defmacro lambda-mapped-command (function-symbol)\n  \"Define a command which `mapcar's FUNCTION-SYMBOL over a list of arguments.\"\n  (let ((name (intern (str:concat (string function-symbol) \"-*\"))))\n    `(lambda-command ,name (arg-list)\n       ,(documentation function-symbol 'function)\n       (mapcar ',function-symbol arg-list))))\n\n(export-always 'lambda-unmapped-command)\n(defmacro lambda-unmapped-command (function-symbol)\n  \"Define a command which calls FUNCTION-SYMBOL over the first element of a list\nof arguments.\"\n  (let ((name (intern (str:concat (string function-symbol) \"-1\"))))\n    `(lambda-command ,name (arg-list)\n       ,(documentation function-symbol 'function)\n       (,function-symbol (first arg-list)))))\n\n(eval-always\n  (defun generalize-lambda-list (lambda-list)\n    \"Return a lambda-list compatible with generic-function definitions.\nGeneric function lambda lists differ from ordinary lambda list in some ways;\nsee HyperSpec '3.4.2 Generic Function Lambda Lists'.\"\n    (multiple-value-bind (required optional rest keywords aok? aux key?)\n        (alex:parse-ordinary-lambda-list lambda-list)\n      (declare (ignore aux))\n      (sera:unparse-ordinary-lambda-list required (mapcar #'first optional) rest (mapcar #'cadar keywords) aok? nil key?))))\n\n(export-always 'define-command)\n(defmacro define-command (name (&rest arglist) &body body)\n  \"Define new command NAME.\n`define-command' syntax is similar to `defmethod'.\n\nExample:\n\n\\(define-command play-video-in-current-page (&optional (buffer (current-buffer)))\n  \\\"Play video in the currently open buffer.\\\"\n  (uiop:run-program (list \\\"mpv\\\" (render-url (url buffer)))))\"\n  (let ((doc (or (nth-value 2 (alex:parse-body body :documentation t)) \"\")))\n    `(progn\n       (export-always ',name (symbol-package ',name))\n       ;; Warning: We use `defgeneric' instead of `make-instance' (or even\n       ;; `ensure-generic-function') so that the compiler stores source location\n       ;; information (for \"go to definition\" to work).\n       (sera:lret ((gf (defgeneric ,name (,@(generalize-lambda-list arglist))\n                         (:documentation ,doc)\n                         (:method (,@arglist) ,@body)\n                         (:generic-function-class command))))\n         (setf (slot-value gf 'visibility) :mode)))))\n\n(export-always 'define-command-global)\n(defmacro define-command-global (name (&rest arglist) &body body)\n  \"Like `define-command' but mark the command as global.\nThis means it will be listed in `command-source' when the global option is on.\nThis is mostly useful for third-party packages to define globally-accessible\ncommands without polluting Nyxt packages.\"\n  `(sera:lret ((cmd (define-command ,name (,@arglist) ,@body)))\n     (setf (slot-value cmd 'visibility) :global)))\n\n(export-always 'delete-command)\n(defun delete-command (name)\n  \"Remove command NAME, if any.\nAny function or macro definition of NAME is also removed,\nregardless of whether NAME is defined as a command.\"\n  (setf *command-list* (delete name *command-list* :key #'name))\n  (fmakunbound name))\n\n(-> list-all-maybe-subpackages () (list-of types:package-designator))\n(defun list-all-maybe-subpackages ()\n  (remove-if-not (lambda (pkg) (find #\\/ (package-name pkg)))\n                 (list-all-packages)))\n\n(export-always 'subpackage-p)\n(-> subpackage-p (types:package-designator types:package-designator) (values boolean &optional))\n(defun subpackage-p (subpackage package)\n  \"Return non-nil if SUBPACKAGE is a subpackage of PACKAGE or is PACKAGE itself.\nA subpackage has a name that starts with that of PACKAGE followed by a '/' separator.\"\n  (or (eq (find-package subpackage) (find-package package))\n      (uiop:string-prefix-p (uiop:strcat (package-name package) \"/\")\n                            (package-name subpackage))))\n\n(export-always 'subpackages)\n(-> subpackages (types:package-designator) (list-of types:package-designator))\n(defun subpackages (package)\n  \"Return all subpackages of PACKAGE, including itself.\"\n  (append (list package)\n          (remove-if-not (lambda (p) (subpackage-p p package))\n                         (list-all-maybe-subpackages))))\n\n(-> nyxt-subpackage-p (types:package-designator) boolean)\n(defun nyxt-subpackage-p (package)\n  \"Return non-nil if PACKAGE is a sub-package of `nyxt'.\"\n  (subpackage-p package :nyxt))\n\n(-> nyxt-user-subpackage-p (types:package-designator) boolean)\n(defun nyxt-user-subpackage-p (package)\n  \"Return non-nil if PACKAGE is a sub-package of `nyxt' or `nyxt-user'.\"\n  (subpackage-p package :nyxt-user))\n\n(defun nyxt-packages ()\n  \"Return all Nyxt packages.\nSee also `nyxt-user-packages', `nyxt-extension-packages' and `non-nyxt-packages'.\"\n  (sera:filter #'nyxt-subpackage-p (list-all-packages)))\n\n(defun nyxt-user-packages ()\n  \"Return all Nyxt user packages.\"\n  (sera:filter #'nyxt-user-subpackage-p (list-all-packages)))\n\n(defun nyxt-extension-packages ()\n  \"Return all the Nyxt extension packages.\nA package is considered an extension one if its name is \\\"nx-\\\"-prefixed.\"\n  (remove-if-not (curry #'str:starts-with-p \"NX-\") (list-all-packages)\n                 :key #'package-name))\n\n(defun non-nyxt-packages ()\n  \"Return the packages that are not related to Nyxt.\nIt's the complement of `nyxt-packages' and `nyxt-user-packages'.\"\n  (set-difference (list-all-packages)\n                  (append (nyxt-packages) (nyxt-user-packages))))\n\n(define-class slot ()\n  ((name nil\n         :type (or symbol null))\n   (class-sym nil\n              :type (or symbol null))))\n\n(defun class-slots (class-sym &key (visibility :any))\n  \"Return the list of slots with VISIBILITY.\"\n  (sym:filter-symbols visibility (mopu:slot-names class-sym)))\n\n(defmethod prompter:object-attributes ((slot slot) (source prompter:source))\n  (declare (ignore source))\n  `((\"Name\" ,(string (name slot)))\n    (\"Class\" ,(string (class-sym slot)))))\n\n(defun package-slots (packages &optional (visibility :any))\n  \"Return the list of all slot symbols in PACKAGES.\nSee `sym:package-symbols'.\"\n  (mappend (lambda (class-sym)\n             (mapcar (lambda (slot) (make-instance 'slot\n                                                   :name slot\n                                                   :class-sym class-sym))\n                     (class-slots class-sym :visibility visibility)))\n           (sym:package-classes packages)))\n\n(sym:define-symbol-type command (function)\n  (command-p (ignore-errors (symbol-function sym:%symbol%))))\n\n(defun list-commands (&key global-p mode-symbols)\n  \"List commands.\nCommands are instances of the `command' class.\nWhen MODE-SYMBOLS are provided, list only the commands that belong to the\ncorresponding mode packages or of a parent mode packages.  Otherwise list all\ncommands. Additionally, list all commands within the Nyxt package.\nWith MODE-SYMBOLS and GLOBAL-P, include global commands.\"\n  ;; TODO: Make sure we list commands of inherited modes.\n  (if mode-symbols\n      (lpara:premove-if\n       (lambda (command)\n         (and (or (not global-p)\n                  (not (eq :global (visibility command))))\n              (notany\n               (lambda (mode-symbol)\n                 (or (eq (symbol-package (name command))\n                         (symbol-package mode-symbol))\n                     (member\n                      (symbol-package (name command))\n                      (mapcar #'symbol-package\n                              (sera:filter (symbol-function (uiop:safe-read-from-string \"sym:mode-symbol-p\"\n                                                                                        :package :nyxt))\n                                           (mapcar #'class-name (mopu:superclasses mode-symbol)))))))\n               mode-symbols)))\n       *command-list*)\n      *command-list*))\n\n(defun list-mode-commands (mode-symbol)\n  \"List commands.\nCommands are instances of the `command' class.  Only commands defined within the\ncontext of a mode are listed.\"\n  (remove-if-not\n   (lambda (command)\n     (eq (symbol-package (name command))\n         (symbol-package mode-symbol)))\n   *command-list*))\n\n(defun run-command (command &optional args)\n  ;; Bind current buffer for the duration of the command.  This\n  ;; way, if the user switches buffer after running a command\n  ;; but before command termination, `current-buffer' will\n  ;; return the buffer from which the command was invoked.\n  (with-current-buffer (current-buffer)\n    (handler-case (apply #'funcall command args)\n      (prompt-buffer-canceled ()\n        (log:debug \"Prompt buffer interrupted\")\n        nil))))\n\n(defun run (command &optional args)\n  \"Run COMMAND over ARGS and return its result.\nThis is blocking, see `run-async' for an asynchronous way to run commands.\"\n  (let ((channel (make-channel 1))\n        (error-channel (make-channel 1)))\n    (run-thread \"run command\"\n      ;; TODO: This `handler-case' overlaps with `with-protect' from `run-thread'.  Factor them!\n      (handler-case (calispel:! channel (run-command command args))\n        (condition (c)\n          (calispel:! error-channel c))))\n    (calispel:fair-alt\n      ((calispel:? channel result)\n       result)\n      ((calispel:? error-channel c)\n       (echo-warning \"Error when running ~a: ~a\" command c)))))\n\n(defun run-async (command &optional args)\n  \"Run COMMAND over ARGS asynchronously.\nSee `run' for a way to run commands in a synchronous fashion and return the\nresult.\"\n  (run-thread \"run-async command\"\n    (run-command command args)))\n\n(define-command nothing ()                 ; TODO: Replace with ESCAPE special command that allows dispatched to cancel current key stack.\n  \"A command that does nothing.\nThis is useful to override bindings to do nothing.\"\n  (values))\n"
  },
  {
    "path": "source/concurrency.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt)\n\n(defun initialize-lparallel-kernel (&key (worker-count (sera:count-cpus)))\n  \"Initialize the lparallel kernel with WORKER-COUNT, if not supplied set it to\nthe amount of CPU cores.\"\n  (unless lpara:*kernel*\n    (setf lpara:*kernel* (lpara:make-kernel worker-count))))\n\n(defun restart-browser (c)\n  \"Restart browser reporting condition C.\"\n  (funcall 'restart-with-message        ; Not defined yet.\n           :condition c\n           :backtrace (with-output-to-string (stream)\n                        (uiop:print-backtrace :stream stream :condition c))))\n\n(export-always 'with-protect)\n(defmacro with-protect ((format-string &rest args) &body body)\n  \"Run body with muffled conditions when `*run-from-repl-p*' is nil, run normally otherwise.\nWhen the condition is muffled, a warning is reported to the user as per\nFORMAT-STRING and ARGS.\nAs a special case, the first `:condition' keyword in ARGS is replaced with the\nraised condition.\"\n  (alex:with-gensyms (c sub-c)\n    `(if (or *run-from-repl-p*)\n         (handler-case (progn ,@body)\n           (prompt-buffer-canceled ()\n             (log:debug \"Prompt buffer interrupted\")))\n         (ignore-errors\n          (handler-bind\n              ((error\n                 (lambda (,c)\n                   (declare (ignorable ,c))\n                   (if *restart-on-error*\n                       (restart-browser ,c)\n                       ,(let ((condition-index (position :condition args)))\n                          (flet ((new-args (condition condition-index &optional escaped-p)\n                                   (if condition-index\n                                       (append (subseq args 0 condition-index)\n                                               (list (if escaped-p\n                                                         `(plump:encode-entities (princ-to-string ,condition))\n                                                         `,condition))\n                                               (subseq args (1+ condition-index)))\n                                       'args)))\n                            `(handler-bind ((t (lambda (,sub-c)\n                                                 (declare (ignore ,sub-c))\n                                                 (log:error ,format-string ,@(new-args c condition-index))\n                                                 (invoke-restart 'continue))))\n                               (echo-warning ,format-string ,@(new-args c condition-index :escaped-p)))))))))\n            ,@body)))))\n\n(defun make-channel (&optional size)\n  \"Return a channel of capacity SIZE.\nIf SIZE is NIL, capacity is infinite.\"\n  (cond\n    ((null size)\n     (make-instance 'calispel:channel\n                    :buffer (make-instance 'jpl-queues:unbounded-fifo-queue)))\n    ((zerop size)\n     (make-instance 'calispel:channel))\n    ((plusp size)\n     (make-instance 'calispel:channel\n                    :buffer (make-instance 'jpl-queues:bounded-fifo-queue :capacity size)))))\n\n(defun drain-channel (channel &optional timeout)\n  \"Listen to CHANNEL until a value is available, then return all CHANNEL values\nas a list.\nTIMEOUT specifies how long to wait for a value after the first one.\nThis is a blocking operation.\"\n  (labels ((fetch ()\n             (multiple-value-bind (value received?)\n                 (calispel:? channel timeout)\n               (if received?\n                   (cons value (fetch))\n                   nil))))\n    (cons (calispel:? channel)\n          (nreverse (fetch)))))\n\n(export-always 'run-thread)\n(defmacro run-thread (name &body body)\n  \"Run body in a new protected thread.\nThis supersedes `bt:make-thread' in Nyxt.  Don't use the latter unless you know\nwhat you are doing!\"\n  `(lparallel.thread-util:with-thread (:name ,(str:concat \"Nyxt \" name)\n                                       :bindings (append '((*run-from-repl-p* . *run-from-repl-p*)\n                                                           (*headless-p* . *headless-p*))\n                                                         bt:*default-special-bindings*))\n     (with-protect (\"Error on separate thread: ~a\" :condition)\n       ,@body)))\n\n(defun evaluate (string)\n  \"Evaluate all expressions in STRING and return the last result as a list of values.\nThe list of values is useful when the last result is multi-valued, e.g. (values 'a 'b).\nYou need not wrap multiple values in a PROGN, all top-level expressions are\nevaluated in order.\"\n  (let ((channel (make-channel 2)))\n    (run-thread \"evaluator\"\n      (let ((*standard-output* (make-string-output-stream)))\n        (calispel:!\n         channel\n         (with-input-from-string (input string)\n           (first\n            (last\n             (mapcar (lambda (s-exp)\n                       (multiple-value-list\n                        (with-protect (\"Error in s-exp evaluation: ~a\" :condition)\n                          (eval s-exp))))\n                     (safe-slurp-stream-forms input))))))\n        (calispel:! channel (get-output-stream-string *standard-output*))))\n    (values (calispel:? channel) (calispel:? channel))))\n\n(defun evaluate-async (string)\n  \"Like `evaluate' but does not block and does not return the result.\"\n  (run-thread \"async evaluator\"\n    (with-input-from-string (input string)\n      (dolist (s-exp (safe-slurp-stream-forms input))\n        (funcall (lambda () (with-protect (\"Error in s-exp evaluation: ~a\" :condition)\n                              (eval s-exp))))))))\n"
  },
  {
    "path": "source/conditions.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt)\n\n(export-always 'nyxt-error)\n(define-condition nyxt-error (error)\n  ((message :initarg :message :accessor message))\n  (:report (lambda (c stream)\n             (format stream \"~a\" (slot-value c 'message))))\n  (:documentation \"An error internal to Nyxt.\nIt should abort the ongoing command, but not the whole process.\"))\n\n(export-always 'browser-already-started)\n(define-condition browser-already-started (nyxt-error)\n  ()\n  (:documentation \"An existing instance of Nyxt is already running.\"))\n\n(export-always 'prompt-buffer-canceled)\n(define-condition prompt-buffer-canceled (error)\n  ()\n  (:documentation \"Signaled when prompt buffer is exited abnormally (via ESC key, for example).\"))\n"
  },
  {
    "path": "source/configuration-commands.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt)\n\n(defun error-in-new-window (title condition-string backtrace)\n  (sera:lret* ((window (window-make *browser*))\n               (error-buffer (make-instance 'document-buffer)))\n    (with-current-buffer error-buffer\n      (html-set\n       (values\n        (spinneret:with-html-string\n          (:head\n           (:title title)\n           (:nstyle (style (current-buffer))))\n          (:body\n           (:h1 title)\n           (:h2 \"Condition\")\n           (:pre condition-string)\n           (:h2 \"Backtrace\")\n           (:pre backtrace)))\n        \"text/html;charset=utf8\")\n       error-buffer))\n    (ffi-window-set-buffer window error-buffer)))\n\n(-> load-lisp\n    ((or null types:pathname-designator) &key (:package (or null package)))\n    *)\n(defun load-lisp (file &key package)\n  \"Load the Lisp FILE (can also be a stream).\nReturn T on success.\nOn error, return the condition as a first value and the backtrace as second value.\"\n  (unless (files:nil-pathname-p file)\n    (let ((*package* (or (find-package package) *package*)))\n      (flet ((unsafe-load ()\n               (cond\n                 ((streamp file)\n                  (load file))\n                 ((uiop:file-exists-p file)\n                  (log:info \"Loading Lisp file ~s.\" file)\n                  (load file))\n                 (t\n                  (log:debug \"Lisp file ~s does not exist.\" file)))\n               nil))\n        (if *run-from-repl-p*\n            (tagbody\n             loop\n               (restart-case (unsafe-load)\n                 (load-lisp-retry ()\n                   :report \"Retry loading Lisp file.\"\n                   (go loop))))\n            (catch 'lisp-file-error\n              (handler-bind ((error (lambda (c)\n                                      (let ((backtrace (with-output-to-string (stream)\n                                                         (uiop:print-backtrace :stream stream :condition c))))\n                                        (throw 'lisp-file-error\n                                          (if *browser*\n                                              (error-in-new-window \"*Config file errors*\" (princ-to-string c) backtrace)\n                                              (values c backtrace)))))))\n                (unsafe-load))))))))\n\n(define-command load-file ()\n  \"Load the prompted Lisp file.\"\n  (prompt :prompt \"Load file\"\n          :input (uiop:native-namestring\n                  (let ((config-path (files:expand *config-file*)))\n                    (if (uiop:file-exists-p config-path)\n                        (uiop:pathname-directory-pathname config-path)\n                        (uiop:getcwd))))\n          :extra-modes 'nyxt/mode/file-manager:file-manager-mode\n          :sources\n          (make-instance 'nyxt/mode/file-manager:file-source\n                         :extensions '(\"lisp\")\n                         :actions-on-return (lambda-command load-file* (files)\n                                              (dolist (file files)\n                                                (load-lisp file))))))\n\n(export-always 'clean-configuration)\n(defun clean-configuration ()\n  \"Undo user configuration set by `define-configuration' or `customize-instance'.\"\n  (dolist (class (sera:filter #'user-class-p (sym:package-classes* (nyxt-packages))))\n    (setf (hooks:handlers-alist (slot-value class 'customize-hook)) nil))\n  (dolist (method (mopu:generic-function-methods #'customize-instance))\n    (unless (or (equal (list (find-class t)) ; Don't remove default method.\n                       (mopu:method-specializers method))\n                ;; We only preserve :after methods for ourselves.\n                (equal (list :after) (method-qualifiers method))))))\n\n(define-command load-config-file (&key (config-file (files:expand *config-file*)))\n  \"Load or reload the CONFIG-FILE.\"\n  (if (files:nil-pathname-p config-file)\n      (echo \"No config file.\")\n      (progn\n        (clean-configuration)\n        (load-lisp config-file :package (find-package :nyxt-user))\n        (echo \"~a loaded.\" config-file))))\n\n#+(and unix (not darwin))\n(define-command add-desktop-entry ()\n  \"Install the running AppImage to the system menu via a `.desktop' entry.\nThe path installed to is `~/.local/share/applications/'.\"\n  (let* ((appimage-path (uiop:getenv \"APPIMAGE_PATH\"))\n         (desktop-entry-dir \"~/.local/share/applications/\")\n         (icons-dir \"~/.local/share/icons/hicolor/\")\n         (desktop-entry-path (uiop:merge-pathnames*\n                              (make-pathname :name \"nyxt.desktop\")\n                              desktop-entry-dir)))\n    (ensure-directories-exist desktop-entry-dir)\n    (uiop:with-output-file (stream desktop-entry-path :if-exists :supersede)\n      (format stream (gethash \"nyxt.desktop\" *static-data*) appimage-path))\n    (loop for resolution in '(\"16x16\" \"32x32\" \"128x128\" \"256x256\")\n          do (let* ((icon (gethash (format nil \"nyxt_~a.png\" resolution)\n                                   *static-data*))\n                    (icon-dir (uiop:merge-pathnames*\n                               (format nil \"~a/apps/\" resolution) icons-dir))\n                    (icon-path\n                      (uiop:merge-pathnames* \"nyxt.png\" icon-dir)))\n               (ensure-directories-exist icon-dir)\n               (uiop:with-output-file (stream icon-path :element-type\n                                              '(unsigned-byte 8)\n                                                        :if-exists :supersede)\n                 (write-sequence icon stream))))\n    (uiop:launch-program \"update-desktop-database\")\n    (echo \"Added Nyxt to the system menu.~%\")))\n"
  },
  {
    "path": "source/configuration.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt)\n\n(define-class config-directory-file (files:config-file nyxt-file)\n  ((files:base-path #p\"\"))\n  (:export-class-name-p t)\n  (:documentation \"Nyxt directory for config files.\"))\n\n(define-class config-special-file (config-directory-file)\n  ((files:base-path #p\"\")\n   (command-line-option :config\n                        :accessor nil\n                        :type keyword))\n  (:export-class-name-p t)\n  (:documentation \"Like `config-directory-file' but can be controlled from command line options.\"))\n\n(define-class config-file (config-special-file nyxt-lisp-file)\n  ((files:base-path #p\"config\")\n   (command-line-option :config\n                        :accessor nil\n                        :type keyword))\n  (:export-class-name-p t)\n  (:documentation \"Lisp configuration file which path can be controlled from command line options.\nUnlike `auto-config-file', it can only be loaded with `cl:load', it is not meant to be read with\n`nfiles:read-file' or `nfiles:content'.\"))\n\n(defmethod files:read-file ((profile nyxt-profile) (file config-file) &key)\n  \"Don't load anything for `config-file's since they are Lisp file to be loaded with `cl:load'.\"\n  nil)\n\n(define-class auto-config-file (config-special-file nyxt-lisp-file)\n  ((files:base-path (files:join #p\"auto-config.\" (princ-to-string (version))))\n   (command-line-option :auto-config\n                        :accessor nil\n                        :type keyword))\n  (:export-class-name-p t)\n  (:documentation \"Lisp configuration file which path can be controlled from command line options.\nUnlike `config-file', it can both loaded with `cl:load' and read with\n`nfiles:read-file'.  The latter should return a structured reification of the configuration.\"))\n\n(defmethod files:resolve ((profile nyxt-profile) (config-file config-special-file))\n  (let* ((option (slot-value config-file 'command-line-option))\n         (no-option (alex:make-keyword\n                     (uiop:strcat \"NO-\" (symbol-name option)))))\n    (if (getf *options* no-option)\n        #p\"\"\n        (let ((path (or (uiop:ensure-pathname (getf *options* option))\n                        (call-next-method))))\n          (unless (uiop:emptyp path)\n            (when (and (getf *options* option) (not (uiop:file-exists-p path)))\n              (log:warn \"File ~s does not exist.\" path))\n            path)))))\n\n(export-always '*auto-config-file*)\n(defvar *auto-config-file* (make-instance 'auto-config-file)\n  \"The generated configuration file.\")\n\n(export-always '*config-file*)\n(defvar *config-file* (make-instance 'config-file)\n  \"The configuration file entry point.\")\n\n(define-class log-file (files:data-file nyxt-file)\n  ((files:base-path #p\"nyxt.log\")\n   (files:name \"log-file\"))\n  (:export-class-name-p t)\n  (:documentation \"Data file for Nyxt logs.\"))\n\n(export-always '*log-file*)\n(defvar *log-file* (make-instance 'log-file)\n  \"Path to the file where log is saved.\nThis is global because logging starts before the `*browser*' is even initialized.\")\n\n(defvar *log-pattern* \"<%p> [%D{%H:%M:%S}] %m%n\"\n  \"Non-verbose log pattern.\")\n\n(define-class nyxt-source-directory (nyxt-file)\n  ((files:name \"source\"))\n  (:export-class-name-p t)\n  (:documentation \"Directory with Nyxt sources.\"))\n\n(defmethod files:resolve ((profile nyxt-profile) (directory nyxt-source-directory))\n  \"Try hard to find Nyxt source on disk.\nReturn #p\\\"\\\" if not found.\"\n  (let ((asd-path (ignore-errors (asdf:system-source-directory :nyxt))))\n    (if (uiop:directory-exists-p asd-path)\n        asd-path\n        (or\n         ;; XDG / FHS:\n         (find-if (lambda (d)\n                    (uiop:file-exists-p (uiop:merge-pathnames* \"nyxt.asd\" d)))\n                  (uiop:xdg-data-dirs \"nyxt\"))\n         ;; Location relative to the binary:\n         (let ((relative-dir (uiop:merge-pathnames*\n                              \"share/nyxt/\"\n                              (files:parent\n                               (files:parent\n                                (uiop:ensure-pathname\n                                 (first (uiop:raw-command-line-arguments)) :truenamize t))))))\n           (when (uiop:file-exists-p (uiop:merge-pathnames* \"nyxt.asd\" relative-dir))\n             relative-dir))\n         ;; Not found:\n         #p\"\"))))\n\n(export-always '*source-directory*)\n(defvar *source-directory* (make-instance 'nyxt-source-directory)\n  \"The directory where the source code is stored.\nThis is set globally so that it can be looked up if there is no\n`*browser*' instance.\")\n\n(define-class extensions-directory (files:data-file nyxt-file)\n  ((files:base-path #p\"extensions/\")\n   (files:name \"extensions\"))\n  (:export-class-name-p t)\n  (:documentation \"Nyxt data subdirectory for Lisp extensions.\"))\n\n(export-always '*extensions-directory*)\n(defvar *extensions-directory* (make-instance 'extensions-directory)\n  \"The directory where extensions are stored.\nThis is set globally so that extensions can be loaded even if there is no\n`*browser*' instance.\")\n\n(export-always 'nyxt-source-registry)\n(defun nyxt-source-registry ()\n  \"Return Nyxt-specific ASDF registry, with source and extension directories.\"\n  (let ((source-dir (files:expand *source-directory*)))\n    `(:source-registry\n      (:tree ,(files:expand *extensions-directory*))\n      ,@(unless (uiop:absolute-pathname-p source-dir)\n          `((:tree ,source-dir))) ; Probably useless since systems are immutable.\n      :inherit-configuration)))\n\n(defun set-nyxt-source-location (pathname) ; From `sb-ext:set-sbcl-source-location'.\n  \"Initialize the NYXT logical host based on PATHNAME, which should be the\ntop-level directory of the Nyxt sources. This will replace any existing\ntranslations for \\\"NYXT:source;\\\" and \\\"NYXT:libraries;\\\". Other \\\"NYXT:\\\"\ntranslations are preserved.\"\n  (let ((truename (truename pathname))\n        (current-translations\n          (remove-if (lambda (translation)\n                       (or (pathname-match-p \"NYXT:source;\" translation)\n                           (pathname-match-p \"NYXT:libraries;\" translation)))\n                     (logical-pathname-translations \"NYXT\")\n                     :key #'first)))\n    (flet ((physical-target (component)\n             (merge-pathnames\n              (make-pathname :directory (list :relative component\n                                              :wild-inferiors)\n                             :name :wild\n                             :type :wild)\n              truename)))\n      (setf (logical-pathname-translations \"NYXT\")\n            `((\"NYXT:source;**;*.*.*\" ,(physical-target \"source\"))\n              (\"NYXT:libraries;**;*.*.*\" ,(physical-target \"libraries\"))\n              ,@current-translations)))))\n\n(define-class slot-form ()\n  ((name\n    nil\n    :type symbol)\n   (value\n    nil\n    :type t))\n  (:export-class-name-p t)\n  (:documentation \"A form to set slot with `name' to `value'.\"))\n\n(define-class class-form ()\n  ((class-name\n    nil\n    :type symbol)\n   (forms\n    '()\n    :type (maybe (cons (or cons slot-form) *))))\n  (:export-class-name-p t)\n  (:documentation \"A set of `forms' for class configuration.\"))\n\n(defun read-init-form-slot (class-name sexp)\n  \"Return 2 values:\n- the slot name;\n- the slot value.\nReturn NIL if not a slot setting.\"\n  (when (and (= 3 (length sexp))\n             (eq (first sexp) 'setf)\n             (eq (first (second sexp)) 'slot-value)\n             (eq (second (second sexp)) class-name))\n    (let ((slot-name (second (third (second sexp))))\n          (slot-value (third sexp)))\n      (values slot-name slot-value))))\n\n(defun write-init-form-slot (class-name slot-form)\n  `(setf (slot-value ,class-name ',(name slot-form)) ,(value slot-form)))\n\n(defun read-init-form-class (form)\n  \"Return:\n- the class name\n- the list of forms, either `slot-form' or a raw s-exp.\nReturn NIL if not a class form.\"\n  (when-let ((class-name (when (and (eq (first form) 'defmethod)\n                                    (eq (second form) 'customize-instance))\n                           (second (first (find-if #'consp form))))))\n    (let ((body (alex:parse-body (sera:nlet lp ((sexp form))\n                                   (if (consp (first sexp))\n                                       (rest sexp)\n                                       (lp (rest sexp))))\n                                 :documentation t)))\n      (values class-name\n              (mapcar (lambda (sexp)\n                        (multiple-value-bind (name value)\n                            (read-init-form-slot class-name sexp)\n                          (if name\n                              (make-instance 'slot-form\n                                             :name name\n                                             :value value)\n                              sexp)))\n                      body)))))\n\n(defun write-init-form-class (class-form)\n  `(defmethod customize-instance ((,(class-name class-form) ,(class-name class-form)) &key)\n     ,@(mapcar (lambda (form)\n                 (if (slot-form-p form)\n                     (write-init-form-slot (class-name class-form) form)\n                     form))\n               (forms class-form))))\n\n;; TODO: Instantiate directly in read-init-*?\n(defmethod files:deserialize ((profile nyxt-profile) (file auto-config-file) raw-content &key)\n  (flet ((make-init-form (form)\n           (multiple-value-bind (name forms)\n               (read-init-form-class form)\n             (if name\n                 (make-instance 'class-form\n                                :class-name name\n                                :forms forms)\n                 form))))\n    (mapcar #'make-init-form\n            (uiop:slurp-stream-forms raw-content))))\n\n(defmethod files:serialize ((profile nyxt-profile) (file auto-config-file) stream &key)\n  (loop for form in (files:content file)\n        for i from 0\n        do (when (> i 0) (terpri stream))\n           (write\n            (if (class-form-p form)\n                (write-init-form-class form)\n                form)\n            :stream stream)\n           (fresh-line stream)))\n\n(defmethod files:write-file ((profile nyxt-profile) (file auto-config-file) &key &allow-other-keys)\n  (let ((*print-case* :downcase)\n        (*package* (find-package :nyxt-user)))\n    (log:info \"Writing auto configuration to ~s.\" (files:expand file))\n    (call-next-method)))\n\n(defun auto-configure (&key form class-name slot (slot-value nil slot-value-supplied-p))\n  (files:with-file-content (config *auto-config-file*)\n    (if class-name\n        (flet ((ensure-class-form (class-name)\n                 (or (when config\n                       (find-if (sera:eqs class-name) (sera:filter #'class-form-p config) :key #'class-name))\n                     (sera:lret ((form (make-instance 'class-form :class-name class-name)))\n                       (alex:appendf config (list form)))))\n               (ensure-slot-form (class-form slot)\n                 (or (find-if (sera:eqs slot) (sera:filter #'slot-form-p (forms class-form)) :key #'name)\n                     (sera:lret ((form (make-instance 'slot-form :name slot)))\n                       (alex:appendf (forms class-form) (list form)))))\n               (delete-slot-form (class-form slot)\n                 (delete-if (sera:eqs slot) (sera:filter #'slot-form-p (forms class-form)) :key #'name)))\n          (let ((class-form (ensure-class-form class-name)))\n            (if slot\n                (if slot-value-supplied-p\n                    (sera:lret ((slot-form (ensure-slot-form class-form slot)))\n                      (setf (value slot-form) slot-value))\n                    (setf (forms class-form) (delete-slot-form class-form slot)))\n                (alex:appendf (forms class-form) (list form)))))\n        (alex:appendf config (list form))))\n  (echo \"Updated configuration in ~s.\" (files:expand *auto-config-file*)))\n\n(export-always '%slot-value%)\n(defvar %slot-value% nil\n  \"Holds the value of the slot being configured when in `define-configuration'.\")\n\n(export-always '%slot-default%)\n(defvar %slot-default% nil\n  \"Holds the default value of the slot being configured when in `define-configuration'.\")\n\n(export-always 'define-configuration)\n(defmacro define-configuration (classes &body slots-and-values)\n  \"Helper macro to customize the class slots of the CLASSES.\n\nCLASSES is either a symbol or a list of symbols.  Only user-configurable classes\nare valid, such as `browser', `buffer', `prompt-buffer', `window' or modes such\nas `nyxt/mode/hint:hint-mode'.\n\nSLOTS-AND-VALUES is a list of slot re-definitions, optionally preceded by a\ndocstring. The `%slot-default%' variable is replaced by the slot's initform,\nwhile `%slot-value%' is replaced by the slot's current value .\n\nExample:\n\n\\(define-configuration web-buffer\n  ((default-modes (pushnew 'nyxt/mode/force-https:force-https-mode %slot-value%))))\n\nExample to get the `blocker-mode' command to use a new default hostlists:\n\n\\(define-configuration nyxt/mode/blocker:blocker-mode\n  ((nyxt/mode/blocker:hostlists (append (list *my-blocked-hosts*) %slot-default%)\n                                :doc \\\"You have to define *my-blocked-hosts* first.\\\")))\n\nTo discover the default value of a slot or all slots of a class, use the\n`describe-slot' or `describe-class' commands, respectively.\"\n  (alex:with-gensyms (handler hook)\n    `(progn\n       ,@(loop\n           ;; Strip off the docstring, it's merely cosmetic\n           with slots-and-values = (if (stringp (first slots-and-values))\n                                       (rest slots-and-values)\n                                       slots-and-values)\n           for class-name in (uiop:ensure-list classes)\n           ;; NOTE: `or' here because `sym:resolve-symbol' only searches through\n           ;; Nyxt packages, while one may try to configure the\n           ;; extension/application-specific class too. If `sym:resolve-symbol'\n           ;; fails, then hope that `find-class' will either work or highlight\n           ;; the problem.\n           for class = (resolve-user-symbol class-name :class)\n           append (loop for ((slot-name value . rest)) on (first slots-and-values)\n                        ;; FIXME: It's alarming that we resolve the slot name at\n                        ;; compile-time instead of run-time. Move to the handler\n                        ;; body maybe?\n                        for slot = (find (symbol-name slot-name) (mopu:slot-names class)\n                                         :key #'symbol-name :test #'equal)\n                        ;; TODO: Shall we really make the name unique?  Since we\n                        ;; are configuring slots, maybe not.\n                        for handler-name = (gensym (format nil \"CONFIGURE-~a-~a\" class slot))\n                        when slot\n                          collect\n                        `(let ((,hook (slot-value (find-class (quote ,class)) 'nyxt::customize-hook))\n                               (,handler (make-instance\n                                          'hooks:handler\n                                          :fn (lambda (object)\n                                                ,@(when (or (getf rest :documentation)\n                                                            (getf rest :doc))\n                                                    (list (or (getf rest :documentation)\n                                                              (getf rest :doc))))\n                                                (declare (ignorable object))\n                                                (setf (slot-value object (quote ,slot))\n                                                      (let* ((%slot-value% (slot-value object (quote ,slot)))\n                                                             (%slot-default%\n                                                               ,(if (c2mop:class-finalized-p (find-class class))\n                                                                    (getf (mopu:slot-properties class slot) :initform)\n                                                                    (progn\n                                                                      (echo-warning\n                                                                       \"Slot default not found for slot ~a of class ~a, falling back to its current value\"\n                                                                       slot class)\n                                                                      '%slot-value%))))\n                                                        (declare (ignorable %slot-value% %slot-default%))\n                                                        ,value)))\n                                          :name (quote ,handler-name))))\n                           (hooks:add-hook ,hook ,handler :append t))\n                        else\n                          do (log:warn \"Not found slot ~a in class ~a, generating the wrapper method for configuration.\"\n                                       slot-name class)\n                          and collect `(handler-bind ((warning #'muffle-warning))\n                                         (defmethod ,slot-name :around ((object ,class))\n                                           (let* ((%slot-value% (call-next-method))\n                                                  (%slot-default% %slot-value%))\n                                             ,value))))))))\n\n\n(defparameter %buffer nil)              ; TODO: Make a monad?\n\n(export-always 'current-buffer)\n(defun current-buffer (&optional window)\n  \"Get the active buffer for WINDOW, or the active window otherwise.\"\n  (or %buffer\n      (if-let ((w (or window (current-window))))\n        (active-buffer w)\n        (when *browser*\n          (log:debug \"No active window, picking last active buffer.\")\n          (last-active-buffer)))))\n\n(export-always 'with-current-buffer)\n(defmacro with-current-buffer (buffer &body body)\n  \"Execute BODY in a context in which `current-buffer' returns BUFFER.\"\n  ;; We `unwind-protect' to restore the right buffer when nesting this macro.\n  `(let ((old-%buffer %buffer))\n     (if (buffer-p ,buffer)\n         (unwind-protect\n              (let ((%buffer ,buffer))\n                ,@body)\n           (setf %buffer old-%buffer))\n         ;; TODO: Raise error instead?\n         (log:warn \"Expected buffer, got ~a\" ,buffer))))\n\n;; TODO: Disallow canceling the prompt? Allow changing order of YES and NO so\n;; that one makes a conscious effort to choose a YES?\n;; TODO: Add an \"always (yes|no)\" answers/clauses and do something with those?\n;; - Remembering prompt answers in history.\n;; - Serializing thing (like notification permissions) to disk.\n;; - Or simply leaving the interpretation of this clause to the user.\n;; But maybe that's beyond if-confirm.\n(export-always 'if-confirm)\n(defmacro if-confirm ((prompt &key (yes \"yes\" yes-supplied-p) (no \"no\" no-supplied-p))\n                      &optional (yes-form t) no-form)\n  \"Ask the user for confirmation before executing either YES-FORM or NO-FORM.\nYES-FORM is executed on YES answer, NO-FORM -- otherwise (including NO and\nprompt cancellation).\nPROMPT should evaluate to a string.\n\nExamples:\n\n;; Return t/nil on user decision.\n\\(if-confirm (\\\"you agree?\\\"))\n\n;; Customize the yes/no answers, and get the mood of the user as boolean.\n\\(if-confirm ((format nil \\\"How are you?\\\") :yes \\\"Good!\\\" :no \\\"Don't even ask...\\\"))\n\n;; Commit an action in case of yes, clean up on no\n\\(if-confirm (\\\"Overwrite the file?\\\" :no \\\"cancel\\\")\n            (overwrite-file-because-confirmed)\n            (clean-up/abort/stop))\"\n  `(let ((answer (handler-case\n                     (prompt1\n                      :prompt ,prompt\n                      :sources (make-instance 'prompter:yes-no-source\n                                              ,@(when yes-supplied-p\n                                                  (list :yes yes))\n                                              ,@(when no-supplied-p\n                                                  (list :no no)))\n                      :hide-suggestion-count-p t)\n                   (prompt-buffer-canceled () nil))))\n     (if answer\n         ,yes-form\n         ,no-form)))\n\n(defun set-as-default-browser (&key (name \"nyxt\")\n                                 (targets\n                                  (list (uiop:xdg-config-home \"mimeapps.list\")\n                                        (uiop:xdg-data-home \"applications/mimeapps.list\"))))\n  \"Return the modified MIME apps list.\nReturn the persisted file as second value.\"\n  (declare (ignorable name targets))\n  #+(and unix (not darwin))\n  (let* ((target (or (first (sera:filter #'uiop:file-exists-p targets))\n                     (first targets)))\n         (config (py-configparser:read-files (py-configparser:make-config)\n                                             (list target)))\n         (desktop-file (uiop:strcat name \".desktop\")))\n    (dolist (section '(\"Added Associations\" \"Default Applications\"))\n      (dolist (key '(\"text/html\"\n                     \"text/gemini\"\n                     \"x-scheme-handler/http\"\n                     \"x-scheme-handler/https\"\n                     \"x-scheme-handler/chrome\"\n                     \"application/x-extension-htm\"\n                     \"application/x-extension-html\"\n                     \"application/x-extension-shtml\"\n                     \"application/xhtml+xml\"\n                     \"application/x-extension-xhtml\"\n                     \"application/x-extension-xht\"))\n        (py-configparser:set-option config section key desktop-file)))\n    (with-open-file (s target\n                       :direction :output\n                       :if-does-not-exist :create\n                       :if-exists :supersede)\n      (py-configparser:write-stream config s))\n\n    (values config target))\n  #-(and unix (not darwin))\n  (log:warn \"Only supported on GNU / BSD systems running XDG-compatible desktop environments.\"))\n\n\n;; TODO: Report compilation errors.\n\n(export-always 'nyxt-user-system)\n(defclass nyxt-user-system (asdf:system)\n  ;; We cannot use :pathname because ASDF forces its value.\n  ((config-directory\n    :initarg :config-directory\n    :initform nil\n    :accessor config-directory))\n  (:documentation \"Specialized systems for Nyxt users.\nThis automatically defaults :pathname to the `*config-file*' directory unless\noverridden by the `:config-directory' option.\nSee `define-nyxt-user-system' and `define-nyxt-user-system-and-load'.\"))\n\n(defvar *nyxt-user-systems-with-missing-dependencies* '())\n\n(defmethod asdf:component-pathname ((system nyxt-user-system))\n  \"Default to `config-directory-file'.\"\n  (or (config-directory system)\n      (files:expand (make-instance 'config-directory-file))) )\n\n(export-always 'load-system*)\n(defun load-system* (system &rest keys &key force force-not verbose version &allow-other-keys)\n  \"Like `asdf:load-system' but, instead of signaling an error on missing\ndependency, it warns the user, skips the load gracefully and returns NIL.\n\nWhen loading succeeds, it goes through the list of all the systems that failed\nto load and attempts to load them if their dependencies now seem to be met.\"\n  ;; TODO: Ideally we would make this the default behavior of\n  ;; `nyxt-user-system' by specializing a method Unfortunately\n  ;; `resolve-dependency-name' is a function and `find-component' is called\n  ;; against the `depends-on' element but not the system itself.\n  (declare (ignore force force-not verbose version))\n  (block done\n    (flet ((report (c)\n             (pushnew (asdf:coerce-name system) *nyxt-user-systems-with-missing-dependencies*\n                      :test #'string=)\n             (log:warn \"Could not load system ~a: ~a\" system c)\n             (return-from done nil)))\n      (handler-bind ((asdf:missing-dependency #'report)\n                     (asdf:missing-dependency-of-version #'report))\n        (prog1 (apply #'asdf:load-system system keys)\n          (alex:removef *nyxt-user-systems-with-missing-dependencies*\n                        system\n                        :test #'string=)\n          (dolist (system *nyxt-user-systems-with-missing-dependencies*)\n            (when (every (rcurry #'asdf:find-system nil) (asdf:system-depends-on (asdf:find-system system)))\n              (log:info \"Load system ~s\" system)\n              (load-system* system))))))))\n\n(defun ensure-component (component-designator)\n  (if (consp component-designator)\n      component-designator\n      (list :file (sera:drop-suffix \".lisp\" component-designator :test #'string-equal))))\n\n(asdf:defsystem \"nyxt-user\") ; Dummy parent needs to exist for `define-nyxt-user-system' to define subsystems.\n\n(export-always 'define-nyxt-user-system)\n(defmacro define-nyxt-user-system (name &rest args &key depends-on components\n                                   &allow-other-keys)\n  \"Define a user system, usually meant to load configuration files.\nExample to load the \\\"my-slynk-config\\\" file in your configuration directory.\n\n  (define-nyxt-user-system nyxt-user/slynk\n    :components (\\\"my-slynk-config\\\"))\n  (asdf:load-system :nyxt-user/slynk)\n\nSee also `define-nyxt-user-system-and-load'.\n\nIt catches potential load dependency cycles.\n\nArguments are the same as for `asdf:defsystem'.\nFor convenience, we also support `string's or `pathname's directly in COMPONENTS.\nSo instead of\n\n:components `((:file \\\"foo\\\")\n              (:file #p\\\"bar\\\"))\n\nyou can write\n\n:components `(\\\"foo\\\" #p\\\"bar\\\")\n\nIt only works for top-level components, so if you introduce a module you'll have\nto use the full syntax.\n\nTo change the base directory, pass the `:config-directory' option.\"\n  ;; We specify DEPENDS-ON to emphasize its availability.\n  (declare (ignore depends-on))\n  (unless (sera:string-prefix-p \"nyxt-user/\" (string name) )\n    (error \"User system name must start with 'nyxt-user/'.\"))\n  ;; We cannot call `make-instance 'asdf:system' because we need to register the\n  ;; system, and `register-system' is unexported.\n  `(asdf:defsystem ,name\n     :class nyxt-user-system\n     ,@(uiop:remove-plist-key :components args)\n     :components ,(mapcar #'ensure-component\n                          components)))\n\n(export-always 'define-nyxt-user-system-and-load)\n(defmacro define-nyxt-user-system-and-load (name &rest args &key depends-on components\n                                            &allow-other-keys)\n  \"Like `define-nyxt-user-system' but schedule to load the system when all\nDEPENDS-ON packages are loaded.\nIf they already are, load the system now.\nReturn the system.\"\n  ;; We specify DEPENDS-ON and COMPONENTS to emphasize their availability.\n  (declare (ignore depends-on components))\n  `(prog1 (define-nyxt-user-system ,name ,@args)\n     (load-system* ',name)))\n"
  },
  {
    "path": "source/describe.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt)\n\n(defun description-constructor (lister &key (test #'eql))\n  \"LISTER is a function that returns a list of symbols or objects representing them\nfrom the given packages. TEST is a function that checks for equality for the purpose of\ndeduplicating the result.\nSee `sym:package-functions' for an example of LISTER.\"\n  (lambda (source)\n    (delete-duplicates\n     (append\n      (funcall lister (packages source) (visibility source))\n      (funcall lister (internal-visibility-packages source) :internal)\n      (funcall lister (external-visibility-packages source) :external)\n      (funcall lister (inherited-visibility-packages source) :inherited))\n     :test test)))\n\n(define-class describe-nyxt-source (prompter:source)\n  ((visibility\n    :any\n    :type (member :internal :external :inherited :any)\n    :documentation \"Include symbol of this visibility from `packages'.\")\n   (packages\n    (nyxt-user-packages)\n    :type (maybe (list-of package))\n    :documentation \"Include symbols of `visibility' from the given packages.\")\n   (internal-visibility-packages\n    nil\n    :type (maybe (list-of package))\n    :documentation \"Include internal symbols from the given packages.\")\n   (external-visibility-packages\n    (nyxt-packages)\n    :type (maybe (list-of package))\n    :documentation \"Include internal symbols from the given packages.\")\n   (inherited-visibility-packages\n    nil\n    :type (maybe (list-of package))\n    :documentation \"Include internal symbols from the given packages.\")\n   (prompter:name (alex:required-argument 'prompter:name))\n   (prompter:constructor (alex:required-argument 'prompter:constructor))\n   (prompter:filter-preprocessor #'prompter:filter-exact-matches))\n  (:export-class-name-p nil)              ; Internal class.\n  (:export-accessor-names-p t))\n\n(define-class describe-non-nyxt-source (describe-nyxt-source)\n  ((packages\n    nil\n    :type (maybe (list-of package))\n    :documentation \"Include symbols of `visibility' from the given packages.\")\n   (external-visibility-packages\n    (non-nyxt-packages)\n    :type (maybe (list-of package))\n    :documentation \"Include internal symbols from the given packages.\"))\n  (:export-class-name-p nil)              ; Internal class.\n  (:export-accessor-names-p t))\n\n(define-class describe-internal-source (describe-nyxt-source)\n  ((packages\n    nil\n    :type (maybe (list-of package))\n    :documentation \"Include symbols of `visibility' from the given packages.\")\n   (internal-visibility-packages\n    (nyxt-packages)\n    :type (maybe (list-of package))\n    :documentation \"Include internal symbols from the given packages.\")\n   (external-visibility-packages\n    nil\n    :type (maybe (list-of package))\n    :documentation \"Include internal symbols from the given packages.\"))\n  (:export-class-name-p nil)              ; Internal class.\n  (:export-accessor-names-p t))\n\n(define-class function-source (describe-nyxt-source)\n  ((prompter:name \"Functions\")\n   (prompter:constructor (description-constructor #'sym:package-functions)))\n  (:export-accessor-names-p t))\n\n(define-class function-non-nyxt-source (function-source describe-non-nyxt-source)\n  ((prompter:name \"Non-Nyxt Functions\")))\n\n(define-class function-internal-source (function-source describe-internal-source)\n  ((prompter:name \"Internal Functions\")))\n\n(defun first-line (string)\n  \"Return first non-empty line in STRING.\"\n  (find-if (complement #'uiop:emptyp) (sera:lines string)))\n\n(defmethod prompter:object-attributes ((symbol symbol) (source prompter:source))\n  (declare (ignore source))\n  `((\"Name\" ,(prini-to-string symbol) (:width 1))\n    (\"Documentation\" ,(or (cond\n                            ((fboundp symbol)\n                             (first-line (documentation symbol 'function)))\n                            ((and (find-class symbol nil)\n                                  (mopu:subclassp (find-class symbol)\n                                                  (find-class 'standard-object)))\n                             (first-line (documentation symbol 'type)))\n                            ((find-package symbol)\n                             (first-line (documentation (find-package symbol) t)))\n                            (t\n                             (first-line (documentation symbol 'variable))))\n                          \"\")\n                     (:width 4))\n    (\"Visibility\" ,(prini-to-string (sym:symbol-visibility symbol)) (:width 1))))\n\n;; Note that `package-source' is populated by symbols, not packages.\n(defmethod prompter:object-attributes ((package package) (source prompter:source))\n  (declare (ignore source))\n  `((\"Name\" ,(package-name package) (:width 1))\n    (\"Documentation\" ,(or (first-line (documentation package t)) \"\") (:width 4))\n    (\"Nicknames\" ,(append (package-nicknames package)\n                          ;; Old ASDF/UIOP don't know about package-local-nicknames.\n                          (ignore-errors (uiop:symbol-call\n                                          :uiop :package-local-nicknames package)))\n                 (:width 1))))\n\n(define-class class-source (describe-nyxt-source)\n  ((prompter:name \"Classes\")\n   (prompter:constructor (description-constructor #'sym:package-classes)))\n  (:export-accessor-names-p t))\n\n(define-class class-non-nyxt-source (class-source describe-non-nyxt-source)\n  ((prompter:name \"Non-Nyxt Classes\")))\n\n(define-class class-internal-source (class-source describe-internal-source)\n  ((prompter:name \"Internal Classes\")))\n\n(define-class slot-source (describe-nyxt-source)\n  ((prompter:name \"Slots\")\n   (prompter:constructor\n    (description-constructor #'package-slots\n                             :test (lambda (slot-a slot-b)\n                                     (equal `(,(name slot-a) ,(class-sym slot-a))\n                                            `(,(name slot-b) ,(class-sym slot-b)))))))\n  (:export-accessor-names-p t))\n\n(define-class slot-non-nyxt-source (slot-source describe-non-nyxt-source)\n  ((prompter:name \"Non-Nyxt Slots\")))\n\n(define-class slot-internal-source (slot-source describe-internal-source)\n  ((prompter:name \"Internal Slots\")))\n\n(defun non-keyword-package-variables (packages visibility)\n  (remove-if #'keywordp (sym:package-variables packages visibility)))\n\n(define-class variable-source (describe-nyxt-source)\n  ((prompter:name \"Variables\")\n   (prompter:constructor (description-constructor #'non-keyword-package-variables)))\n  (:export-accessor-names-p t))\n\n(define-class variable-non-nyxt-source (variable-source describe-non-nyxt-source)\n  ((prompter:name \"Non-Nyxt Variables\")))\n\n(define-class variable-internal-source (variable-source describe-internal-source)\n  ((prompter:name \"Internal Variables\")))\n\n(define-class package-source (prompter:source)\n  ((prompter:name \"Packages\")\n   (prompter:constructor (mapcar (compose #'intern #'package-name) (list-all-packages)))\n   (prompter:filter-preprocessor #'prompter:filter-exact-matches)))\n\n(define-internal-page-command-global describe-any (&key input)\n    (buffer (format nil \"*Describe-~a*\" input) 'nyxt/mode/help:help-mode)\n  \"Inspect anything and show it in a help buffer.\nWhen input exists, list all the symbols that may match it.\nOtherwise prompt for matches.\"\n  (when (symbolp input)\n    (spinneret:with-html-string\n      (:h1 (princ-to-string input))\n      (:p (princ-to-string input)\n          \" may refer to several things. Please choose the one that you need.\")\n      (:dl\n       (when (boundp input)\n         (:dt \"Variable\")\n         (:dd (:nxref :variable input)))\n       (cond\n         ((sym:mode-symbol-p input)\n          (:dt \"Mode\")\n          (:dd (:nxref :mode input)))\n         ((sym:class-symbol-p input)\n          (:dt \"Class\")\n          (:nxref :class-name input))\n         (t nil))\n       (cond\n         ((sym:command-symbol-p input)\n          (:dt \"Command\")\n          (:dd (:nxref :command input)))\n         ((sym:macro-symbol-p input)\n          (:dt \"Macro\")\n          (:dd (:nxref :macro input)))\n         ((sym:function-symbol-p input)\n          (:dt \"Function\")\n          (:dd (:nxref :function input))))\n       (when (find-package input)\n         (:dt \"Package\")\n         (:dd (:nxref :package input)))\n       (dolist (class (sym:package-classes (union (nyxt-packages) (list (symbol-package input)))\n                                           :external))\n         (when (find input (class-slots class))\n           (:dt \"Slot in \" (:nxref :class-name class))\n           (:dd (:nxref :class-name class :slot input))))))))\n\n(defmethod describe-any :around (&key (input nil input-supplied-p)\n                                   %buffer%)\n  (declare (ignorable input %buffer%))\n  (cond\n    ((and input-supplied-p (symbolp input))\n     (call-next-method))\n    (t\n     (let ((sources\n             (list (make-instance\n                    'command-source\n                    :actions-on-return (lambda-command describe-command* (commands)\n                                         (describe-command :command (name (first commands)))))\n                   (make-instance\n                    'mode-source\n                    :actions-on-return (lambda-command describe-command* (modes)\n                                         (describe-mode :mode (first modes))))\n                   (make-instance\n                    'variable-source\n                    :actions-on-return (lambda-command describe-variable* (variables)\n                                         (describe-variable :variable (first variables))))\n                   (make-instance\n                    'function-source\n                    :actions-on-return (lambda-command describe-function* (functions)\n                                         (describe-function :fn (first functions))))\n                   (make-instance\n                    'class-source\n                    :actions-on-return (lambda-command describe-class* (classes)\n                                         (describe-class :class (first classes))))\n                   (make-instance\n                    'slot-source\n                    :actions-on-return (lambda-command describe-slot** (slots)\n                                         (describe-slot :class (class-sym (first slots))\n                                                        :name (name (first slots)))))\n                   (make-instance\n                    'variable-non-nyxt-source\n                    :actions-on-return (lambda-command describe-variable* (variables)\n                                         (describe-variable :variable (first variables))))\n                   (make-instance\n                    'function-non-nyxt-source\n                    :actions-on-return (lambda-command describe-function* (functions)\n                                         (describe-function :fn (first functions))))\n                   (make-instance\n                    'class-non-nyxt-source\n                    :actions-on-return (lambda-command describe-class* (classes)\n                                         (describe-class :class (first classes))))\n                   (make-instance\n                    'slot-non-nyxt-source\n                    :actions-on-return (lambda-command describe-slot** (slots)\n                                         (describe-slot :class (class-sym (first slots))\n                                                        :name (name (first slots)))))\n                   (make-instance\n                    'variable-internal-source\n                    :actions-on-return (lambda-command describe-variable* (variables)\n                                         (describe-variable :variable (first variables))))\n                   (make-instance\n                    'function-internal-source\n                    :actions-on-return (lambda-command describe-function* (functions)\n                                         (describe-function :fn (first functions))))\n                   (make-instance\n                    'class-internal-source\n                    :actions-on-return (lambda-command describe-class* (classes)\n                                         (describe-class :class (first classes))))\n                   (make-instance\n                    'slot-internal-source\n                    :actions-on-return (lambda-command describe-slot** (slots)\n                                         (describe-slot :class (class-sym (first slots))\n                                                        :name (name (first slots))))))))\n       (prompt\n        :prompt \"Describe\"\n        :input input\n        :sources sources)))))\n\n(define-internal-page describe-value\n    (&key id)\n    (:title \"*Help-value*\" :page-mode 'nyxt/mode/help:help-mode)\n  \"Inspect value under ID and show it in a help buffer.\"\n  (and-let* ((id id)\n             (value (inspected-value id)))\n    (spinneret:with-html-string\n      (:h1 (:raw (escaped-literal-print value)))\n      (:dl\n       (:dt \"Type\")\n       (:dd (:pre (if (sym:class-symbol-p (type-of value))\n                      (:nxref :class-name (type-of value))\n                      (prini-to-string (type-of value))))))\n      (:p (:raw (value->html value))))))\n\n(define-internal-page-command-global describe-package\n    (&key (package\n           (prompt1 :prompt \"Describe package\"\n                    :sources 'package-source)))\n    (buffer (str:concat \"*Help-\" (package-name (find-package package)) \"*\") 'nyxt/mode/help:help-mode)\n  \"Inspect a package and show it in a help buffer.\"\n  (let* ((package (find-package package))\n         (total-symbols (sym:package-symbols (list package)))\n         (external-symbols (sym:package-symbols (list package) :visibility :external)))\n    (flet ((package-markup (package)\n             (spinneret:with-html\n               (:a :href (nyxt-url 'describe-package :package (package-name package))\n                   (package-name package)))))\n      (spinneret:with-html-string\n        (:nstyle (style buffer))\n        (:h1 (package-name package))\n        (:pre (:code (:raw (resolve-backtick-quote-links (documentation (find-package package) t) package))))\n        (:h2 \"Symbols:\")\n        (:ul\n         (:li \"External: \" (length external-symbols))\n         (:li \"Internal: \" (- (length total-symbols) (length external-symbols)))\n         (:li \"Total: \" (length total-symbols)))\n        (when (package-use-list package)\n          (:h2 \"Use list:\")\n          (:ul\n           (dolist (use (safe-sort (package-use-list package) :key #'package-name))\n             (:li (package-markup use)))))\n        (when (package-used-by-list package)\n          (:h2 \"Used by list:\")\n          (:ul\n           (dolist (use (safe-sort (package-used-by-list package) :key #'package-name))\n             (:li (package-markup use)))))))))\n\n(define-internal-page-command-global describe-variable\n    (&key\n     (variable (prompt1 :prompt \"Describe variable\"\n                        :sources '(variable-source\n                                   variable-non-nyxt-source\n                                   variable-internal-source))))\n    (buffer (str:concat \"*Help-\" (symbol-name variable) \"*\") 'nyxt/mode/help:help-mode)\n  \"Inspect a variable and show it in a help buffer.\"\n  (let ((*print-case* :downcase))\n    (if (boundp variable)\n        (spinneret:with-html-string\n          (:nstyle (style buffer))\n          (:h1 (format nil \"~s\" variable)) ; Use FORMAT to keep package prefix.\n          (:pre (:code (:raw (resolve-backtick-quote-links (documentation variable 'variable)\n                                                           (symbol-package variable)))))\n          (:h2 \"Type\")\n          (:pre (princ-to-string (type-of (symbol-value variable))))\n          (:h2 \"Current Value:\")\n          (:p (:raw (value->html (symbol-value variable))))\n          (:nsection\n            :title \"Describe\"\n           (:pre (:code (with-output-to-string (s) (describe variable s))))))\n        (spinneret:with-html-string\n          (:nstyle (style buffer))\n          (:h1 (format nil \"~s\" variable))\n          (:p \"Unbound\")))))\n\n(defun format-function-type (function-type)\n  (match function-type\n    ((list 'function argument-types return-types)\n     (with-output-to-string (s)\n       (format s \"Argument types: ~s~&\" argument-types)\n       (format s \"Return types: ~s~&\" return-types)))))\n\n(define-internal-page-command-global describe-function\n    (&key\n     (fn (prompt1 :prompt \"Describe function\"\n                  :sources '(function-source\n                             function-non-nyxt-source\n                             function-internal-source)))\n     ;; This is to have a full-word alternative to :fn for those that prefer it.\n     (function fn))\n    (buffer (str:concat \"*Help-\" (symbol-name function) \"*\") 'nyxt/mode/help:help-mode)\n  \"Inspect a function and show it in a help buffer.\nFor generic functions, describe all the methods.\"\n  (if function\n      (let ((input function))\n        (flet ((fun-desc (input)\n                 (spinneret:with-html-string\n                   (:pre (:code (:raw (resolve-backtick-quote-links (documentation input 'function) (symbol-package input)))))\n                   (when (sym:command-symbol-p input)\n                     (let* ((key-keymap-pairs (nth-value 1 (keymaps:pretty-binding-keys input (all-keymaps) :print-style (keymaps:name (keyscheme buffer)))))\n                            (key-keymapname-pairs (mapcar (lambda (pair)\n                                                            (list (first pair)\n                                                                  (keymaps:name (second pair))))\n                                                          key-keymap-pairs)))\n                       (when key-keymapname-pairs\n                         (:nsection\n                           :title \"Bindings\"\n                           (:table\n                            (:tr\n                             (:th \"Binding\")\n                             (:th \"Keymap name\"))\n                            (loop for (binding keymapname) in key-keymapname-pairs\n                                  collect (:tr (:td binding)\n                                               (:td keymapname))))))))\n                   (:nsection\n                     :title \"Argument list\"\n                     (:pre (:code (prini-to-string (trivial-arguments:arglist input)\n                                                   :package (symbol-package input)))))\n                   #+sbcl\n                   (unless (or (macro-function input)\n                               (eq 'function (sb-introspect:function-type input)))\n                     (:nsection\n                       :title \"Type\"\n                       (:p (:pre (format-function-type (sb-introspect:function-type input))))))\n                   (:nsection\n                     :title \"Describe\"\n                     (:pre (:code (with-output-to-string (s) (describe (symbol-function input) s)))))))\n               (method-desc (method)\n                 (spinneret:with-html-string\n                   (:details\n                    (:summary\n                     (:h3 :style \"display: inline\"\n                          (format nil \"~s\" input) \" \"\n                          (:raw (format\n                                 nil \"(~{~a~^ ~})\"\n                                 (mapcar (lambda (class)\n                                           (cond\n                                             ((ignore-errors (mopu:subclassp class 'standard-object))\n                                              (spinneret:with-html-string\n                                                (:a :href (nyxt-url 'describe-class\n                                                                    :class (class-name class))\n                                                    (prini-to-string (class-name class)))))\n                                             ((ignore-errors (eq t (class-name class)))\n                                              \"t\")\n                                             (t (nyxt::escaped-literal-print class))))\n                                         (mopu:method-specializers method))))))\n                    (:button\n                     :class \"button\"\n                     :onclick (ps:ps (nyxt/ps:lisp-eval (:buffer buffer :title \"describe-function\")\n                                                        (remove-method (closer-mop:method-generic-function method)\n                                                                       method)\n                                                        (reload-current-buffer)))\n                     \"Remove method\")\n                    (:pre (:code (:raw (resolve-backtick-quote-links\n                                        (documentation method 't) (symbol-package (mopu:method-name method))))))\n                    (:nsection\n                      :level 4\n                      :title \"Argument list\"\n                      (:pre (:code (prini-to-string (closer-mop:method-lambda-list method)\n                                                    :package (symbol-package input)))))))))\n          (spinneret:with-html-string\n            (:nstyle (style buffer))\n            (:h1 (format nil \"~s\" input) ; Use FORMAT to keep package prefix.\n                 (cond\n                   ((macro-function input) \" (macro)\")\n                   ((sym:command-symbol-p input)\n                    \" (command)\")\n                   ((typep (symbol-function input) 'generic-function)\n                    \" (generic function)\")))\n            (cond\n              ((not (fboundp input))\n               (:p \"Unbound.\"))\n              ((typep (symbol-function input) 'generic-function)\n               (:raw (fun-desc input))\n               (unless (sym:command-symbol-p input)\n                 (:nsection\n                   :title \"Methods\"\n                   (:raw (sera:string-join\n                          (mapcar #'method-desc\n                                  (mopu:generic-function-methods\n                                   (symbol-function input)))\n                          \"\")))))\n              (t\n               (:raw (fun-desc input)))))))\n      (prompt :prompt \"Describe function\"\n              :sources 'function-source)))\n\n(define-command-global describe-command\n    (&key (command (name (prompt1 :prompt \"Describe command\"\n                                  :sources 'command-source))))\n  \"Inspect a command and show it in a help buffer.\nA command is a special kind of function that can be called with\n`execute-command' and can be bound to a key.\"\n  (when command\n    (describe-function :fn command)))\n\n(define-internal-page-command-global describe-slot\n    (&key class name)\n    (buffer (str:concat \"*Help-\" (symbol-name name) \"*\") 'nyxt/mode/help:help-mode)\n  \"Inspect a slot and show it in a help buffer.\"\n  (if (and class name)\n      (describe-slot* name class :independent-p t)\n      (let ((slot (prompt1\n                   :prompt \"Describe slot\"\n                   :sources '(slot-source\n                              slot-non-nyxt-source\n                              slot-internal-source))))\n        (describe-slot :class (class-sym slot) :name (name slot))\n        \"\")))\n\n(defun describe-slot* (slot class &key independent-p)\n  \"Create the HTML that represents a slot.\"\n  ;; TODO: Adapt HTML sections / lists to describe-slot and describe-class.\n  ;; TODO: Parse docstrings and highlight code samples.\n  (let ((props (mopu:slot-properties (find-class class) slot))\n        (*package* (symbol-package slot)))\n    (spinneret:with-html-string\n      (if independent-p\n          (:h1 (prini-to-string slot))\n          (:h3 (prini-to-string slot)))\n      (:dl\n       (when independent-p\n         (:dt \"Class\")\n         (:dd (:pre (:a :href (nyxt-url 'describe-class :class class) class))))\n       (when (getf props :type)\n         (:dt \"Type \")\n         (:dd (:pre (if (or (subtypep (getf props :type) 'standard-object)\n                            (subtypep (getf props :type) 'structure-object))\n                        (:a :href (nyxt-url 'describe-class\n                                            :class (getf props :type))\n                            (prini-to-string (getf props :type)))\n                        (prini-to-string (getf props :type))))))\n       (when (getf props :initform)\n         (:dt \"Default value\")\n         (:dd (:ncode (prini-to-string (getf props :initform)))))\n       (when (getf props :documentation)\n         (:dt \"Documentation\")\n         (:dd (:pre (:code (:raw (resolve-backtick-quote-links\n                                  (getf props :documentation) (symbol-package slot))))))))\n      (unless independent-p\n        (:br \"\")))))\n\n(define-internal-page-command-global describe-class\n    (&key\n     (class (prompt1\n             :prompt \"Describe class\"\n             :sources '(class-source\n                        class-non-nyxt-source\n                        class-internal-source))))\n    (buffer (str:concat \"*Help-\" (symbol-name class) \"*\") 'nyxt/mode/help:help-mode)\n  \"Inspect a class and show it in a help buffer.\"\n  (if (find-class class nil)\n      (let* ((slots (safe-sort (class-slots class :visibility :external)))\n             (slot-descs (sera:string-join (mapcar (rcurry #'describe-slot* class) slots) \"\"))\n             (*print-case* :downcase)\n             (mode-p (subtypep class 'mode)))\n        (spinneret:with-html-string\n          (:nstyle (style buffer))\n          (:h1 (symbol-name class) \" (\" (sera:class-name-of (find-class class)) \")\")\n          (:pre (:code (:raw (resolve-backtick-quote-links (documentation class 'type) (symbol-package class)))))\n          ;; TODO: Show mode keybindings for a better mode help (would be a\n          ;; killer one)? We'd need to do some hack to inspect the keybindings\n          ;; from the class somehow. Maybe :allocation :class so that keymap is\n          ;; allocated/modified in place?\n          ;; REVIEW: Maybe (make-instance MODE-NAME)?\n          (:nsection\n            :title \"Slots\"\n            (:raw slot-descs))\n          (when mode-p\n            (:nsection\n              :title \"Commands\"\n              (:ul\n               (dolist (command (sym:package-commands (symbol-package class)))\n                 (:li (:nxref :command command))))))\n          (when-let ((methods (safe-sort\n                               (remove-if\n                                #'listp (mapcar #'mopu:generic-function-name\n                                                (mopu:generic-functions class))))))\n            (:nsection\n              :title \"Methods\"\n              (:ul (loop for method in methods\n                         collect (:li (:a :href (nyxt-url 'describe-function :fn method) method))))))\n          (when (mopu:direct-superclasses class)\n            (:nsection\n              :title \"Direct superclasses\"\n              (:ul (loop for class-name in (mapcar #'class-name (mopu:direct-superclasses class))\n                         collect (:li (:a :href (nyxt-url 'describe-class :class class-name) class-name))))))\n          (when (mopu:direct-subclasses class)\n            (:nsection\n              :title \"Direct subclasses\"\n              (:ul (loop for class-name in (safe-sort (mapcar #'class-name (mopu:direct-subclasses class)))\n                         collect (:li (:a :href (nyxt-url 'describe-class :class class-name) class-name))))))\n          (:nsection\n            :title \"Describe\"\n            (:pre (:code (with-output-to-string (s) (describe class s)))))))\n      (spinneret:with-html-string\n        (:nstyle (style buffer))\n        (:h2 (format nil \"~s\" class))\n        (:p \"Unbound.\"))))\n\n(define-command-global describe-mode (&key (mode (prompt1 :prompt \"Describe mode\"\n                                                          :sources 'mode-source)))\n  \"Inspect a mode and show it in a help buffer.\"\n  (when mode\n    (describe-class :class mode)))\n\n;; Buffers can't be passed as argument since Nyxt URLs don't handle unreadable\n;; objects.\n(define-internal-page describe-bindings (&key (buffer-id (id (current-buffer))))\n    (:title \"*Help-bindings*\" :page-mode 'nyxt/mode/help:help-mode)\n  \"Show a list of all available keybindings for buffer corresponding to BUFFER-ID.\"\n  (if-let ((buffer (nyxt::buffer-get buffer-id)))\n    (spinneret:with-html-string\n      (:h1 \"Bindings\")\n      (:p (loop for keymap in (current-keymaps buffer)\n                collect (:div\n                         (:h2 (keymaps:name keymap))\n                         (:table\n                          (:tr\n                           (:th \"Command\")\n                           (:th \"Documentation\"))\n                          (loop for keyspec being the hash-keys\n                                  in (keymaps:keymap-with-parents->map keymap)\n                                    using (hash-value bound-value)\n                                collect (:tr\n                                         (:td (typecase bound-value\n                                                (sym:command-symbol (:nxref :command bound-value))\n                                                (command (:nxref :command (name bound-value)))\n                                                (t (prini-to-string bound-value))))\n                                         (:td (documentation-line bound-value 'function \"\")))))))))\n    (spinneret:with-html-string\n      (:h1 \"Bindings\")\n      (:p (format nil \"Buffer with ID ~a does not exist.\" buffer-id)))))\n\n(define-command-global describe-bindings (&key (buffer (current-buffer)))\n  \"Show a list of all available keybindings in the current buffer.\"\n  (buffer-load-internal-page-focus 'describe-bindings :buffer-id (id buffer)))\n\n(defun describe-key-dispatch (command)\n  ;; TODO: Show when something is NOT bound!\n  (unwind-protect (describe-command :command (typecase command\n                                               (symbol command)\n                                               (command (name command))))\n    (setf (command-dispatcher *browser*) #'dispatch-command)\n    (echo-dismiss)))\n\n(define-command describe-key ()\n  \"Display binding of user-inputted keys.\"\n  (setf (command-dispatcher *browser*) #'describe-key-dispatch)\n  (echo \"Press a key sequence to describe:\"))\n\n(export-always 'system-information)\n(defun system-information ()\n  (str:concat\n   \"Nyxt version: \" +version+ +newline+\n   \"Web Renderer: \" (name *renderer*) +newline+\n   \"OS: \" (software-type) \" \" (software-version) +newline+\n   \"Lisp implementation: \" (lisp-implementation-type) \" \" (lisp-implementation-version)\n   #+sbcl\n   (format nil \" (Dynamic space: ~aMB)~%\" (/ (sb-ext:dynamic-space-size) 1024 1024))\n   \"ASDF version: \" (asdf:asdf-version) +newline+\n   \"Features: \" (prin1-to-string *features*)))\n"
  },
  {
    "path": "source/dom.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/dom\n    (:documentation \"Nyxt-specific DOM classes and functions operating on them.\n\nThe classes are generated for every HTML element, including `h1-element',\n`div-element', `img-element' and others.\n\nClasses corresponding to non-HTML elements, such as `text-element',\n`semantic-element', or `h-element', act as higher hierarchical entities of\nelement classes.  For instance, every `h<n>-element' inherits from `h-element'.\n\nThe most useful functions are:\n- `named-html-parse' and `named-json-parse' to turn HTML and JSON documents into\n  a representation of type `plump:root', converting elements into its matching\n  classes.\n- `copy' to produce a full recursive copy of a DOM.\n- `parents', `url' and `body' to access element-specific features in a unified\n  fashion.\n- `click-element', `focus-select-element', `select-option-element' and others to\n  interact with the page using `nyxt/dom' elements as representations of the\n  actual DOM elements.\"))\n(in-package :nyxt/dom)\n\n;; TODO: Factor out into a library?\n\n(defvar *nyxt-dom-classes* (make-hash-table :test #'equalp)\n  \"A table associating the HTML tag name (e.g., \\\"a\\\") with the corresponding\n  nyxt/dom class.\")\n\n(defmacro define-element-classes (&body names)\n  (loop for name in names\n        collect (let* ((class-name (alex:ensure-car name))\n                       (tag (str:replace-all \"-element\"  \"\" (str:downcase (symbol-name class-name))))\n                       (additional-superclasses (when (listp name) (rest name))))\n                  `(progn\n                     (define-class ,class-name (,@(if additional-superclasses\n                                                      additional-superclasses\n                                                      '(plump:element)))\n                       ()\n                       (:export-class-name-p t)\n                       (:export-accessor-names-p t)\n                       (:export-predicate-name-p t)\n                       (:documentation ,(format nil \"An autogenerated class for <~a> HTML tag.\" tag)))\n                     (setf (gethash ,tag *nyxt-dom-classes*)\n                           (quote ,class-name))))\n          into classes\n        finally (return `(progn ,@classes))))\n\n\n(define-element-classes\n  ;; All HTML5 tags, including experimental ones. Scraped with:\n  ;;\n  ;; (format t \"~{~a-element~^ ~}\"\n  ;;         (map 'list (lambda (item)\n  ;;                        (slot-value (elt (slot-value item 'plump-dom::%children) 0)\n  ;;                                    'plump-dom::%text))\n  ;;              (clss:select \".item-name\" (plump:parse (dex:get \"https://htmlreference.io/\")))))\n  ;; Pseudo-tags:\n  text-element (h-element text-element) list-element structure-element semantic-element\n  (checkbox-element input-element) (radio-element input-element) (file-chooser-element input-element)\n  ;; HTML5 elements:\n  (a-element text-element) abbr-element address-element area-element\n  (article-element semantic-element) (aside-element semantic-element)\n  audio-element (b-element text-element) base-element bdi-element bdo-element\n  blockquote-element body-element br-element button-element canvas-element\n  caption-element (cite-element text-element) (code-element text-element) col-element\n  colgroup-element data-element datalist-element (dd-element list-element)\n  (del-element text-element) details-element dfn-element div-element (dl-element list-element)\n  (dt-element list-element) em-element embed-element fieldset-element\n  (figcaption-element semantic-element) figure-element (footer-element semantic-element)\n  form-element (h1-element h-element) (h2-element h-element) (h3-element h-element)\n  (h4-element h-element) (h5-element h-element) (h6-element h-element) head-element\n  (header-element semantic-element) hr-element html-element (i-element text-element) iframe-element\n  img-element input-element ins-element kbd-element label-element legend-element\n  (li-element list-element) link-element (main-element semantic-element) map-element\n  (mark-element semantic-element) meta-element meter-element (nav-element semantic-element)\n  (noscript-element plump:fulltext-element) object-element (ol-element list-element) optgroup-element\n  (option-element text-element) output-element (p-element text-element) param-element\n  (pre-element text-element) progress-element q-element rp-element rt-element rtc-element\n  ruby-element samp-element (script-element plump:fulltext-element)\n  (section-element semantic-element) select-element small-element source-element\n  (span-element text-element) (strong-element text-element) (style-element plump:fulltext-element)\n  (sub-element text-element) summary-element (sup-element text-element) table-element\n  tbody-element td-element textarea-element tfoot-element th-element thead-element\n  (time-element semantic-element) title-element tr-element track-element (ul-element list-element)\n  var-element video-element\n  ;; obsolete elements (from https://www.w3.org/TR/2010/WD-html5-20100304/obsolete.html):\n  applet-element acronym-element bgsound-element dir-element frame-element frameset-element\n  noframes-element isindex-element (listing-element text-element) (xmp-element text-element)\n  nextid-element noembed-element (plaintext-element text-element) (rb-element ruby-element)\n  (basefont-element text-element) (big-element text-element) (blink-element text-element)\n  (center-element text-element) (font-element text-element) (marquee-element text-element)\n  (multicol-element text-element) (nobr-element text-element) (s-element text-element)\n  (spacer-element text-element) (strike-element text-element) (tt-element text-element)\n  (u-element text-element)\n  ;; Experimental elements:\n  dialog-element hgroup-element picture-element slot-element template-element\n  (wbr-element text-element))\n\n(defmethod name-dom-elements ((node plump:node))\n  (when-let* ((tag-p (plump:element-p node))\n              (class (gethash (plump:tag-name node) *nyxt-dom-classes*)))\n    (change-class node class))\n  (when (plump:nesting-node-p node)\n    (loop for child across (plump:children node)\n          do (name-dom-elements child)))\n  node)\n\n(export-always 'named-html-parse)\n(-> named-parse (string) (values (or plump-dom:root null) &optional))\n(defun named-html-parse (input)\n  \"Assign tag classes (e.g., `input-element') to the nodes in the `plump:parse'-d input.\"\n  (name-dom-elements (plump:parse input)))\n\n(define-parenscript get-document-body-json ()\n  (defun process-element (element)\n    (let ((object (ps:create :name (ps:@ element node-name)))\n          (attributes (ps:chain element attributes)))\n      (unless (or (ps:undefined attributes)\n                  (= 0 (ps:@ attributes length)))\n        (setf (ps:@ object :attributes) (ps:create))\n        (loop for i from 0 below (ps:@ attributes length)\n              do (setf (ps:@ object :attributes (ps:chain attributes (item i) name))\n                       (ps:chain attributes (item i) value))))\n      (unless (or (ps:undefined (ps:chain element child-nodes))\n                  (= 0 (ps:chain element child-nodes length)))\n        (setf (ps:chain object :children)\n              (loop for child in (ps:chain element child-nodes)\n                    collect (process-element child))))\n      (when (and (ps:@ element shadow-root)\n                 (ps:@ element shadow-root first-child))\n        (setf (ps:chain object :children)\n              (loop for child in (ps:chain *array\n                                           (from (ps:@ element shadow-root children))\n                                           (concat (ps:chain *array (from (ps:@ element children)))))\n                    collect (process-element child))))\n      (when (or (equal (ps:@ element node-name) \"#text\")\n                (equal (ps:@ element node-name) \"#comment\")\n                (equal (ps:@ element node-name) \"#cdata-section\"))\n        (setf (ps:@ object :text) (ps:@ element text-content)))\n      object))\n  (ps:chain -j-s-o-n (stringify (process-element (nyxt/ps:qs document \"html\")))))\n\n(export-always 'named-json-parse)\n(-> named-json-parse (string) (values (or plump-dom:root null) &optional))\n(defun named-json-parse (json)\n  \"Return a `plump:root' of a DOM-tree produced from the JSON.\n\nJSON should have the format like what `get-document-body-json' produces:\n- A nested hierarchy of objects (with only one root object), where\n  - Every object has a 'name' (usually a tag name or '#text'/'#comment').\n  - Some objects can have 'attributes' (a string->string dictionary).\n  - Some objects have a subarray ('children') of objects working by these three\n    rules.\"\n  (labels ((json-to-plump (json parent)\n             (let ((element\n                     (cond\n                       ((string-equal (j:get \"name\" json) \"#text\")\n                        (plump:make-text-node parent (j:get \"text\" json)))\n                       ((string-equal (j:get \"name\" json) \"#cdata-section\")\n                        (plump:make-cdata parent :text (j:get \"text\" json)))\n                       ((string-equal (j:get \"name\" json) \"#comment\")\n                        (plump:make-comment parent (j:get \"text\" json)))\n                       (t (plump:make-element parent (str:downcase\n                                                      (j:get \"name\" json)))))))\n               (when (typep element 'plump:nesting-node)\n                 (setf (plump:children element)\n                       (plump:ensure-child-array\n                        (map 'vector (rcurry #'json-to-plump element)\n                             (let ((children (j:get \"children\" json)))\n                               (if (stringp children)\n                                   (j:decode children)\n                                   children))))))\n               (when (typep element 'plump:element)\n                 (setf (plump:attributes element)\n                       (sera:lret ((map (plump:make-attribute-map)))\n                         (when (j:get \"attributes\" json)\n                           (maphash (lambda (key val)\n                                      (setf (gethash key map) val))\n                                    (j:get \"attributes\" json))))))\n               element)))\n    (let ((json (j:decode json))\n          (root (plump:make-root)))\n      (json-to-plump json root)\n      (name-dom-elements root))))\n\n(export-always 'copy)\n(defgeneric copy (node &optional parent)\n  (:method ((element plump:root) &optional parent)\n    (declare (ignore parent))\n    (serapeum:lret ((copy (plump:make-root)))\n      (map nil (lambda (c) (plump:append-child copy (copy c copy))) (plump:children element))))\n  (:method ((element plump:element) &optional parent)\n    (serapeum:lret ((copy (make-instance\n                           'plump:element\n                           :parent parent\n                           :attributes (alex:copy-hash-table (plump:attributes element))\n                           :tag-name (plump:tag-name element))))\n      (map nil (lambda (c) (plump:append-child copy (copy c copy))) (plump:children element))))\n  (:method ((element plump:text-node) &optional parent)\n    (make-instance 'plump:text-node\n                   :parent parent\n                   :text (plump:text element)))\n  (:method ((element plump:comment) &optional parent)\n    (make-instance 'plump:comment\n                   :parent parent\n                   :text (plump:text element)))\n  (:method ((element plump:doctype) &optional parent)\n    (make-instance 'plump:doctype\n                   :parent parent\n                   :doctype (plump:doctype element)))\n  (:method ((element plump:xml-header) &optional parent)\n    (make-instance 'plump:xml-header\n                   :parent parent\n                   :attributes (alex:copy-hash-table (plump:attributes element))))\n  (:method ((element plump:cdata) &optional parent)\n    (make-instance 'plump:cdata\n                   :parent parent\n                   :text (plump:text element)))\n  (:method ((element plump:processing-instruction) &optional parent)\n    (make-instance 'plump:processing-instruction\n                   :parent parent\n                   :text (plump:text element)\n                   :tag-name (plump:tag-name element)))\n  (:documentation \"Produce a full copy of NODE as belonging to the PARENT node.\nFull copy means recursively descending to the children of the NODE too.\"))\n\n(export-always 'parents)\n(defgeneric parents (node)\n  (:method ((node plump:node)) nil)\n  (:method ((node plump:child-node))\n    (let ((parent (plump:parent node)))\n      (cons parent (parents parent))))\n  (:documentation \"Get the recursive parents of the NODE.\nThe closest parent goes first, the furthest one goes last.\"))\n\n(defmethod url :around ((element plump:element))\n  (when-let* ((result (call-next-method))\n              (url (url result)))\n    (if (valid-url-p url)\n        url\n        (quri:merge-uris url (url (current-buffer))))))\n\n(defmethod url ((element plump:element))\n  (when (plump:has-attribute element \"href\")\n    (quri:uri (plump:get-attribute element \"href\"))))\n\n(defmethod url ((img img-element))\n  (when (plump:has-attribute img \"src\")\n    (quri:uri (plump:get-attribute img \"src\"))))\n\n;; REVIEW: Export to :nyxt? We are forced to use it with nyxt/dom: prefix.\n(export-always 'body)\n(defgeneric body (element)\n  (:method ((element plump:element))\n    (when (plump:children element)\n      (plump:text element)))\n  (:method :around (element)\n    (declare (ignorable element))\n    (let ((result (call-next-method)))\n      (when result\n        (sera:collapse-whitespace (sera:trim-whitespace result)))))\n  (:documentation \"Return the textual contents of the ELEMENT and its recursive children.\"))\n\n(defun label-of (element)\n  (let ((label-for (and (plump:has-attribute element \"name\")\n                        (ignore-errors\n                         (elt (clss:select (format nil \"label[for=\\\"~a\\\"]\"\n                                                   (plump:attribute element \"name\"))\n                                (alex:lastcar (parents element)))\n                              0)))))\n    (cond\n      ((label-element-p (plump:parent element)) (body (plump:parent element)))\n      (label-for (body label-for))\n      (t nil))))\n\n(defun fallback-body (element)\n  (when-let ((body (or (plump:get-attribute element \"value\")\n                       (plump:get-attribute element \"placeholder\"))))\n    body))\n\n(defmethod body ((input input-element))\n  (or (label-of input) (fallback-body input)))\n\n(defmethod body ((textarea textarea-element))\n  (or (label-of textarea) (fallback-body textarea)))\n\n(defmethod body ((details details-element))\n  (let ((summary (clss:select \"summary\" details)))\n    (unless (uiop:emptyp summary)\n      (plump:text (elt summary 0)))))\n\n(defmethod body ((select select-element))\n  (or (label-of select)\n      (str:join \", \" (map 'list #'plump:text\n                          (clss:select \"option\" select)))))\n\n(defmethod body ((img img-element))\n  (when (plump:has-attribute img \"alt\")\n    (plump:attribute img \"alt\")))\n\n(export-always 'get-nyxt-id)\n(defmethod get-nyxt-id ((element plump:element))\n  \"Get the nyxt-identifier of the page element matching ELEMENT.\"\n  (plump:attribute element \"nyxt-identifier\"))\n\n(export-always 'click-element)\n(define-parenscript click-element (element)\n  \"Click the ELEMENT (Lisp object) on the page with JS.\"\n  (ps:chain (nyxt/ps:rqs-nyxt-id document (ps:lisp (get-nyxt-id element))) (click)))\n\n(export-always 'focus-select-element)\n(define-parenscript focus-select-element (element)\n  \"Focus the element matching ELEMENT on the page.\"\n  (let ((element (nyxt/ps:rqs-nyxt-id document (ps:lisp (get-nyxt-id element)))))\n    (unless (nyxt/ps:element-in-view-port-p element)\n      (ps:chain element (scroll-into-view)))\n    (ps:chain element (focus))\n    (when (functionp (ps:chain element select))\n      (ps:chain element (select)))))\n\n(export-always 'check-element)\n(define-parenscript check-element (element &key (value t))\n  \"Toggle (to VALUE) the checkbox/radio button matching ELEMENT on the page.\"\n  (let ((element (nyxt/ps:rqs-nyxt-id document (ps:lisp (get-nyxt-id element)))))\n    (unless (nyxt/ps:element-in-view-port-p element)\n      (ps:chain element (scroll-into-view)))\n    (ps:chain element (set-attribute \"checked\" (ps:lisp value)))))\n\n(export-always 'toggle-details-element)\n(define-parenscript toggle-details-element (element)\n  \"Open/close the <details> element matching ELEMENT on the page.\"\n  (ps:let ((element (nyxt/ps:rqs-nyxt-id document (ps:lisp (get-nyxt-id element)))))\n    (unless (nyxt/ps:element-in-view-port-p element)\n      (ps:chain element (scroll-into-view)))\n    (if (ps:chain element (get-attribute \"open\"))\n        (ps:chain element (remove-attribute \"open\"))\n        (ps:chain element (set-attribute \"open\" t)))))\n\n(export-always 'select-option-element)\n(define-parenscript select-option-element (element parent)\n  \"Select one of the <option> elements (ELEMENT) in PARENT <select>.\"\n  (ps:let* ((element (nyxt/ps:rqs-nyxt-id document (ps:lisp (get-nyxt-id element))))\n            (parent-select (nyxt/ps:rqs-nyxt-id document (ps:lisp (get-nyxt-id parent)))))\n    (unless (nyxt/ps:element-in-view-port-p element)\n      (ps:chain element (scroll-into-view)))\n    (if (ps:chain element (get-attribute \"multiple\"))\n        (ps:chain element (set-attribute \"selected\" t))\n        (setf (ps:@ parent-select value) (ps:@ element value)))))\n\n(export-always 'scroll-to-element)\n(define-parenscript scroll-to-element (element)\n  \"Scroll the element matching ELEMENT on the page into view.\"\n  (ps:chain (nyxt/ps:rqs-nyxt-id document (ps:lisp (get-nyxt-id element)))\n            (scroll-into-view)))\n\n(export-always 'set-caret-on-start)\n(define-parenscript set-caret-on-start (element)\n  \"Set the cursor at the start of input element matching ELEMENT.\"\n  (let ((el (nyxt/ps:qs-nyxt-id document (ps:lisp (get-nyxt-id element))))\n        (range (ps:chain document (create-range)))\n        (sel (ps:chain #:window (get-selection))))\n    (ps:chain #:window (focus))\n    (ps:chain range (set-start (ps:@ el child-nodes 0) 0))\n    (ps:chain range (collapse true))\n    (ps:chain sel (remove-all-ranges))\n    (ps:chain sel (add-range range))))\n"
  },
  {
    "path": "source/external-editor.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt)\n\n(defun %edit-with-external-editor (content &key (read-only nil))\n  \"Edit CONTENT using `external-editor-program'.\nCreate a temporary file and return its content.  The editor runs synchronously\nso invoke on a separate thread when possible.\"\n  (with-accessors ((cmd external-editor-program)) *browser*\n    (uiop:with-temporary-file (:directory (files:expand (make-instance 'nyxt-temporary-directory))\n                               :pathname p)\n      (str:to-file p content :if-exists :supersede)\n      (log:debug \"External editor ~s opens ~s\" cmd p)\n      (with-protect (\"Failed editing: ~a. See `external-editor-program' slot.\" :condition)\n        (uiop:run-program `(,@cmd ,(uiop:native-namestring p))))\n      (unless read-only (uiop:read-file-string p)))))\n\n;; BUG: Fails when the input field loses its focus, e.g the DuckDuckGo search\n;; bar.  A possible solution is to keep track of the last focused element for\n;; each buffer.\n(define-parenscript select-input-field ()\n  (let ((active-element (nyxt/ps:active-element document)))\n    (when (nyxt/ps:element-editable-p active-element)\n      (ps:chain active-element (select)))))\n\n(define-command-global edit-with-external-editor ()\n  \"Edit the current input field using `external-editor-program'.\"\n  (run-thread \"external editor\"\n    (select-input-field)\n    (ffi-buffer-paste (current-buffer)\n                      (%edit-with-external-editor (ffi-buffer-copy (current-buffer))))))\n\n;; Should belong to user-files.lisp but the define-command-global macro is\n;; defined later.\n(define-command-global edit-user-file-with-external-editor ()\n  \"Edit the queried user file using `external-editor-program'.\nIf the user file is GPG-encrypted, the editor must be capable of decrypting it.\"\n  (let ((cmd (external-editor-program *browser*))\n        (path (files:expand (prompt1 :prompt \"Edit user file in external editor\"\n                                     :sources 'user-file-source))))\n    (echo \"Issued \\\"~{~a~^ ~}\\\" to edit ~s.\" cmd path)\n    (with-protect (\"Failed editing: ~a. See `external-editor-program' slot.\" :condition)\n      (uiop:run-program `(,@cmd ,(uiop:native-namestring path))))))\n\n(define-command-global view-source-with-external-editor (&optional (buffer (current-buffer)))\n  \"View the current page source using `external-editor-program'.\"\n  (run-thread \"source viewer\"\n    (%edit-with-external-editor (if (web-buffer-p buffer)\n                                    (plump:serialize (document-model buffer) nil)\n                                    (ffi-buffer-get-document buffer))\n                                :read-only t)))\n"
  },
  {
    "path": "source/foreign-interface.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt)\n\n(defmacro define-ffi-generic (name arguments &body options)\n  \"Like `defgeneric' but export NAME and define default dummy method if none is provided.\nIf the `:setter-p' option is non-nil, then a dummy setf method is defined.\"\n  (let* ((methods (sera:filter (sera:eqs :method) options :key #'first))\n         (setter? (assoc-value options :setter-p))\n         (normalized-options (set-difference options methods :key #'first))\n         (normalized-options (setf (assoc-value normalized-options :setter-p) nil)))\n    `(progn\n       (export-always ',name)\n       (prog1\n           (defgeneric ,name (,@arguments)\n             ,@(if methods\n                   methods\n                   `((:method (,@arguments)\n                       (declare (ignore ,@(set-difference arguments lambda-list-keywords))))))\n             ,@normalized-options)\n         ,(when setter?\n            `(defmethod (setf ,name) (value ,@arguments)\n               (declare (ignore value ,@arguments))))))))\n\n(define-ffi-generic ffi-window-delete (window)\n  (:method :around ((window window))\n    (with-slots (windows) *browser*\n      (cond ((or *quitting-nyxt-p*\n                 (> (hash-table-count windows) 1))\n             (hooks:run-hook (window-delete-hook window) window)\n             (remhash (id window) windows)\n             (call-next-method))\n            (t\n             (echo \"Can't delete sole window.\")))))\n  (:documentation \"Delete WINDOW.\"))\n\n(define-ffi-generic ffi-window-fullscreen (window &key &allow-other-keys)\n  (:method :around ((window window) &key (user-event-p t) &allow-other-keys)\n    (setf (slot-value window 'fullscreen-p) t)\n    (when user-event-p (call-next-method)))\n  (:documentation \"Set fullscreen WINDOW state on.\nUSER-EVENT-P helps to distinguish events requested by the user or\nrenderer (e.g. fullscreen a video stream).\"))\n(define-ffi-generic ffi-window-unfullscreen (window &key &allow-other-keys)\n  (:method :around ((window window) &key (user-event-p t) &allow-other-keys)\n    (setf (slot-value window 'fullscreen-p) nil)\n    (when user-event-p (call-next-method)))\n  (:documentation \"Set fullscreen WINDOW state off.\nSee `ffi-window-fullscreen'.\"))\n\n(define-ffi-generic ffi-window-maximize (window &key &allow-other-keys)\n  (:method :around ((window window) &key (user-event-p t) &allow-other-keys)\n    (setf (slot-value window 'maximized-p) t)\n    (when user-event-p (call-next-method)))\n  (:documentation \"Set WINDOW to a maximized state.\nUSER-EVENT-P helps to distinguish events requested by the user or renderer.\"))\n(define-ffi-generic ffi-window-unmaximize (window &key &allow-other-keys)\n  (:method :around ((window window) &key (user-event-p t) &allow-other-keys)\n    (setf (slot-value window 'maximized-p) nil)\n    (when user-event-p (call-next-method)))\n  (:documentation \"Set WINDOW to an unmaximized state.\nSee `ffi-window-maximize'.\"))\n\n(define-ffi-generic ffi-buffer-url (buffer)\n  (:documentation \"Return the URL associated with BUFFER as a `quri:uri'.\nThis is used to set the BUFFER `url' slot.\"))\n(define-ffi-generic ffi-buffer-title (buffer)\n  (:documentation \"Return a string corresponding to the BUFFER's title.\"))\n\n(define-ffi-generic ffi-window-to-foreground (window)\n  (:method ((window t))\n    (setf (slot-value *browser* 'last-active-window) window))\n  (:documentation \"Show WINDOW in the foreground.\nThe specialized method must invoke `call-next-method' last.\"))\n\n(define-ffi-generic ffi-window-title (window)\n  (:setter-p t)\n  (:documentation \"Return a string corresponding to the WINDOW's title.\nSetf-able.\"))\n\n(define-ffi-generic ffi-window-active (browser)\n  (:method :around ((browser t))\n    (setf (slot-value browser 'last-active-window)\n          (call-next-method)))\n  (:method ((browser t))\n    (or (slot-value browser 'last-active-window)\n        (first (window-list))))\n  (:documentation \"Return the focused window.\n\nThe specialized method must fallback on the primary method below, as to account\nfor the case when the renderer reports that none of the windows are focused.\n\nThe `:around' method ensures that `last-active-window' is set.\"))\n\n(define-ffi-generic ffi-window-set-buffer (window buffer &key focus)\n  (:method :around ((window window) (buffer buffer) &key focus &allow-other-keys)\n    (hooks:run-hook (window-set-buffer-hook window) window buffer)\n    (when focus\n      (setf (last-access buffer) (time:now)))\n    (call-next-method)\n    buffer)\n  (:method :after ((window window) (buffer buffer) &key focus &allow-other-keys)\n    (declare (ignore focus))\n    (setf (active-buffer window) buffer))\n  (:documentation \"Return BUFFER and display it in WINDOW as a side effect.\nRun `window-set-buffer-hook' over WINDOW and BUFFER before proceeding.\"))\n\n(define-ffi-generic ffi-focus-buffer (buffer)\n  (:documentation \"Return BUFFER and focus it as a side effect.\"))\n\n(define-ffi-generic ffi-height (object)\n  (:setter-p t)\n  (:documentation \"Return the OBJECT's height in pixels.\nDispatches over `window' and classes inheriting from `buffer'.\nUsually setf-able.\"))\n(define-ffi-generic ffi-width (object)\n  (:setter-p t)\n  (:documentation \"Return the OBJECT's width in pixels.\nDispatches over `window' and classes inheriting from `buffer'.\nUsually setf-able.\"))\n\n(define-ffi-generic ffi-buffer-initialize-foreign-object (buffer)\n  (:documentation \"Create and configure the foreign object for a given buffer.\nCreate the foreign objects necessary for rendering the buffer.\"))\n\n(define-ffi-generic ffi-buffer-delete (buffer)\n  (:method ((buffer buffer))\n    (hooks:run-hook (buffer-delete-hook buffer) buffer)\n    (let ((parent-window (find buffer (window-list) :key 'active-buffer)))\n      (when parent-window\n        (let ((replacement-buffer (get-inactive-buffer)))\n          (ffi-window-set-buffer parent-window replacement-buffer))))\n    (add-to-recent-buffers buffer)\n    (buffer-delete (id buffer))\n    (when (next-method-p)\n      (call-next-method)))\n  (:documentation \"Delete BUFFER.\"))\n\n(define-ffi-generic ffi-buffer-load (buffer url)\n  (:method ((buffer buffer) url)\n    (when-let ((url\n                (ignore-errors\n                 (handler-bind\n                     ((error (lambda (c)\n                               (log:error \"In `buffer-load-hook': ~a\" c))))\n                   (hooks:run-hook (slot-value buffer 'buffer-load-hook)\n                                   (url url))))))\n      (check-type url quri:uri)\n      (cond\n        ((equal \"javascript\" (quri:uri-scheme url))\n         (ffi-buffer-evaluate-javascript\n          buffer (quri:url-decode (quri:uri-path url))))\n        (t\n         (clrhash (lisp-url-callbacks buffer))\n         (call-next-method))))\n    buffer)\n  (:documentation \"Load URL into BUFFER through the renderer.\"))\n\n(define-ffi-generic ffi-buffer-reload (buffer)\n  (:documentation \"Reload BUFFER via the renderer and return it.\"))\n\n(define-ffi-generic ffi-buffer-load-alternate-html (buffer html-content content-url url)\n  (:documentation \"Load HTML-CONTENT for CONTENT-URL into BUFFER through the renderer.\nMeant to display page-loading errors.\"))\n\n(define-ffi-generic ffi-register-custom-scheme (scheme)\n  (:documentation \"Register internal custom SCHEME.\nSee `scheme'.\"))\n\n(define-ffi-generic ffi-buffer-evaluate-javascript (buffer javascript &optional world-name)\n  (:documentation \"Evaluate JAVASCRIPT, encoded as a string, in BUFFER.\"))\n(define-ffi-generic ffi-buffer-evaluate-javascript-async (buffer javascript &optional world-name)\n  (:documentation \"Asynchronous version of `ffi-buffer-evaluate-javascript'.\"))\n\n(define-ffi-generic ffi-buffer-add-user-style (buffer style)\n  (:documentation \"Apply the CSS style to BUFFER.\"))\n(define-ffi-generic ffi-buffer-remove-user-style (buffer style)\n  (:documentation \"Remove the STYLE installed with `ffi-buffer-add-user-style'.\"))\n\n(define-ffi-generic ffi-buffer-add-user-script (buffer user-script)\n  (:documentation \"Install the JAVASCRIPT into the BUFFER web view.\"))\n(define-ffi-generic ffi-buffer-remove-user-script (buffer script)\n  (:documentation \"Remove the SCRIPT installed with `ffi-buffer-add-user-script'.\"))\n\n(define-ffi-generic ffi-buffer-javascript-enabled-p (buffer)\n  (:setter-p t)\n  (:documentation \"Return non-nil when JavaScript is enabled in BUFFER.\nSetf-able.\"))\n(define-ffi-generic ffi-buffer-javascript-markup-enabled-p (buffer)\n  (:setter-p t)\n  (:documentation \"Return non-nil when JavaScript can mutate the BUFFER' contents.\nSetf-able.\"))\n(define-ffi-generic ffi-buffer-smooth-scrolling-enabled-p (buffer)\n  (:setter-p t)\n  (:documentation \"Return non-nil when smooth scrolling is enabled in BUFFER.\nSetf-able.\"))\n(define-ffi-generic ffi-buffer-media-enabled-p (buffer)\n  (:setter-p t)\n  (:documentation \"Return non-nil when video and audio playback are enabled in BUFFER.\nSetf-able.\"))\n(define-ffi-generic ffi-buffer-webgl-enabled-p (buffer)\n  (:setter-p t)\n  (:documentation \"Return non-nil when WebGL is enabled in BUFFER.\nSetf-able.\"))\n(define-ffi-generic ffi-buffer-auto-load-image-enabled-p (buffer)\n  (:setter-p t)\n  (:documentation \"Return non-nil when images are displayed in BUFFER.\nSetf-able.\"))\n(define-ffi-generic ffi-buffer-sound-enabled-p (buffer)\n  (:setter-p t)\n  (:documentation \"Return non-nil when the sound is enabled in BUFFER.\nSetf-able.\"))\n\n(define-ffi-generic ffi-buffer-proxy (buffer)\n  (:setter-p t)\n  (:documentation \"Return the proxy URL as a `quri:uri'.\nReturn the list of ignored hosts (list of strings) as a second value.\n\nSetf-able.  The value is either a PROXY-URL or a pair of (PROXY-URL IGNORE-HOSTS).\nPROXY-URL is a `quri:uri' and IGNORE-HOSTS a list of strings.\"))\n\n(define-ffi-generic ffi-buffer-download (buffer url)\n  (:documentation \"Download URL using the BUFFER web view.\"))\n\n(define-ffi-generic ffi-buffer-zoom-ratio (buffer)\n  (:method ((buffer t))\n    (ps-eval :buffer buffer (ps:chain document body style zoom)))\n  (:setter-p t)\n  (:documentation \"Return the zoom level of the document.\nSetf-able.\"))\n(defmethod (setf ffi-buffer-zoom-ratio) (value (buffer t))\n  \"Use JavaScript, if the renderer does not allow zooming natively.\"\n  (ps-eval :buffer buffer\n    (ps:let ((style (ps:chain document body style)))\n      (setf (ps:@ style zoom)\n            (ps:lisp value)))))\n(defmethod (setf ffi-buffer-zoom-ratio) :after (value (buffer t))\n  (setf (slot-value buffer 'zoom-ratio) value))\n\n(define-ffi-generic ffi-buffer-get-document (buffer)\n  (:method ((buffer t))\n    (ps-labels :buffer buffer\n      ((get-html\n        (start end)\n        (ps:chain document document-element |innerHTML| (slice (ps:lisp start)\n                                                               (ps:lisp end))))\n       (get-html-length\n        ()\n        (ps:chain document document-element |innerHTML| length)))\n      (let ((slice-size 10000))\n        (reduce #'str:concat\n                (loop for i from 0 to (truncate (get-html-length)) by slice-size\n                      collect (get-html i (+ i slice-size)))))))\n  (:documentation \"Return the BUFFER raw HTML as a string.\"))\n\n(define-ffi-generic ffi-within-renderer-thread (thunk)\n  (:method ((thunk t))\n    (funcall thunk))\n  (:documentation \"Run THUNK (a lambda of no argument) in the renderer's thread.\nIt is particularly useful for renderer procedures required to be executed in\nspecific threads.\"))\n\n(define-ffi-generic ffi-kill-browser (browser)\n  (:documentation \"Terminate the renderer process.\"))\n\n(define-ffi-generic ffi-initialize (browser urls startup-timestamp)\n  (:method ((browser t) urls startup-timestamp)\n    (finalize-startup browser urls startup-timestamp))\n  (:documentation \"Renderer-specific initialization.\nA specialization of this method must call `call-next-method' to conclude the\nstartup routine.\"))\n\n(define-ffi-generic ffi-inspector-show (buffer)\n  (:documentation \"Show the renderer built-in inspector.\"))\n\n(define-ffi-generic ffi-print-status (status html-body)\n  (:method ((status-buffer t) html-body)\n    (html-write (spinneret:with-html-string\n                  (:head (:nstyle (style status-buffer)))\n                  (:body (:raw html-body)))\n                status-buffer))\n  (:documentation \"Display status buffer according to HTML-BODY.\nThe `style' of the `status-buffer' is honored.\"))\n\n(define-ffi-generic ffi-print-message (message html-body)\n  (:method ((message-buffer t) html-body)\n    (html-write (spinneret:with-html-string\n                  (:head (:nstyle (style message-buffer)))\n                  (:body (:raw html-body)))\n                message-buffer))\n  (:documentation \"Print HTML-BODY in the WINDOW's message buffer.\nThe `style' of the `message-buffer' is honored.\"))\n\n(define-ffi-generic ffi-buffer-cookie-policy (buffer)\n  (:setter-p t)\n  (:documentation \"Return the cookie policy.\nSetf-able.  Valid values are determined by the `cookie-policy' type.\"))\n\n(define-ffi-generic ffi-focused-p (buffer)\n  (:documentation \"Return non-nil when BUFFER is focused.\"))\n\n(define-ffi-generic ffi-buffer-copy (buffer &optional text)\n  (:method :around ((buffer t) &optional text)\n    (declare (ignore text))\n    ;; Save the top of clipboard before it's overridden.\n    (ring-insert-clipboard (clipboard-ring *browser*))\n    (sera:lret ((result (call-next-method)))\n      (ring-insert-clipboard (clipboard-ring *browser*))))\n  (:method ((buffer t) &optional (text nil text-provided-p))\n    (ps-labels :buffer buffer ((copy () (ps:chain window (get-selection) (to-string))))\n      ;; On some systems like Xorg, clipboard pasting happens just-in-time.  So if we\n      ;; copy something from the context menu 'Copy' action, upon pasting we will\n      ;; retrieve the text from the GTK thread.  This is prone to create\n      ;; dead-locks (e.g. when executing a Parenscript that acts upon the clipboard).\n      ;;\n      ;; To avoid this, we can 'flush' the clipboard to ensure that the copied text\n      ;; is present the clipboard and need not be retrieved from the GTK thread.\n      ;; TODO: Do we still need to flush now that we have multiple threads?\n      ;; (trivial-clipboard:text (trivial-clipboard:text))\n\n      (sera:lret ((input (if text-provided-p text (copy))))\n        (copy-to-clipboard input)\n        (echo \"~s copied to clipboard.\" input))))\n  (:documentation \"Copy selected text in BUFFER to the system clipboard.\nIf TEXT is provided, add it to system clipboard instead of selected text.\nShould return the copied text or NIL, if something goes wrong.\"))\n\n(define-ffi-generic ffi-buffer-paste (buffer &optional text)\n  ;; While it may sound counterintuitive, it helps to keep track of the system\n  ;; clipboard, both in Nyxt->OS and OS->Nyxt directions.\n  (:method :around ((buffer t) &optional text)\n    (declare (ignore text))\n    ;; Save the top of clipboard before it's overridden.\n    (ring-insert-clipboard (clipboard-ring *browser*))\n    (sera:lret ((result (call-next-method)))\n      (ring-insert-clipboard (clipboard-ring *browser*))))\n  (:method ((buffer t) &optional (text nil text-provided-p))\n    (ps-labels :buffer buffer\n      ((paste\n        (&optional (input-text (ring-insert-clipboard (clipboard-ring *browser*))))\n        (let ((active-element (nyxt/ps:active-element document))\n              (tag (ps:@ (nyxt/ps:active-element document) tag-name))\n              (text-to-paste (or (ps:lisp input-text)\n                                 (ps:chain navigator clipboard (read-text)))))\n          (when (nyxt/ps:element-editable-p active-element)\n            (nyxt/ps:insert-at active-element text-to-paste))\n          text-to-paste)))\n      (if text-provided-p\n          (paste text)\n          (paste))))\n  (:documentation \"Paste the last clipboard entry into BUFFER.\nIf TEXT is provided, paste it instead.\"))\n\n(define-ffi-generic ffi-buffer-cut (buffer)\n  (:method :around ((buffer t))\n    ;; Save the top of clipboard before it's overridden.\n    (ring-insert-clipboard (clipboard-ring *browser*))\n    (sera:lret ((result (call-next-method)))\n      (ring-insert-clipboard (clipboard-ring *browser*))))\n  (:method ((buffer t))\n    (ps-labels :buffer buffer\n      ((cut\n        ()\n        (let ((active-element (nyxt/ps:active-element document)))\n          (when (nyxt/ps:element-editable-p active-element)\n            (let ((selection-text (ps:chain window (get-selection) (to-string))))\n              (nyxt/ps:insert-at active-element \"\")\n              selection-text)))))\n      (sera:lret ((input (cut)))\n        (when input\n          (copy-to-clipboard input)\n          (echo \"Text cut: ~s\" input)))))\n  (:documentation \"Cut selected text in BUFFER to the system clipboard.\nReturn the text cut.\"))\n\n(define-ffi-generic ffi-buffer-select-all (buffer)\n  (:method ((buffer t))\n    (ps-eval :async t :buffer buffer\n      (let ((active-element (nyxt/ps:active-element document)))\n        (when (nyxt/ps:element-editable-p active-element)\n          (ps:chain active-element (set-selection-range 0 (ps:@ active-element value length)))))))\n  (:documentation \"Select all text in BUFFER web view.\"))\n\n(define-ffi-generic ffi-buffer-undo (buffer)\n  (:documentation \"Undo the last text edit performed in BUFFER's web view.\"))\n\n(define-ffi-generic ffi-buffer-redo (buffer)\n  (:documentation \"Redo the last undone text edit performed in BUFFER's web view.\"))\n\n(define-ffi-generic ffi-buffer-navigate-backwards (buffer)\n  (:method ((buffer t))\n    (ps-eval :async t :buffer buffer\n      (ps:chain history (back))))\n  (:documentation \"Navigate backwards in the history.\"))\n\n(define-ffi-generic ffi-buffer-navigate-forwards (buffer)\n  (:method ((buffer t))\n    (ps-eval :async t :buffer buffer\n      (ps:chain history (forward))))\n  (:documentation \"Navigate forwards in the history.\"))\n\n;; TODO: Move to alists for arbitrary number of params?\n(defvar *context-menu-commands* (make-hash-table :test #'equal)\n  \"A hash table from labels to context menu commands.\nOnce a context menu appears, those commands will be added to it as actions with\nthe labels they have as hash keys.\")\n\n;; TODO: Add TEST arg to decide on whether to display?\n(define-ffi-generic ffi-add-context-menu-command (command label)\n  (:method ((command command) (label string))\n    (setf (gethash label *context-menu-commands*)\n          command))\n  (:method ((command list) (label string))\n    (flet ((thing->function (thing)\n             (typecase thing\n               (symbol (symbol-function thing))\n               (function thing))))\n      (setf (gethash label *context-menu-commands*)\n            (mapcar (lambda (pair)\n                      ;; Convert to an undotted alist.\n                      (match pair\n                        ((cons command (list label))\n                         (list (thing->function command) label))\n                        ((cons command label)\n                         (list (thing->function command) label))))\n                    command))))\n  (:method ((command function) (label string))\n    (setf (gethash label *context-menu-commands*)\n          command))\n  (:method ((command symbol) (label string))\n    (ffi-add-context-menu-command (symbol-function command) label))\n  (:documentation \"Add COMMAND as accessible in context menus with LABEL displayed for it.\nCOMMAND can be a:\n- `command',\n- `function',\n- symbol naming either a command or function,\n- or an alist (dotted or undotted) of COMMAND (any of above types, but not list)\n  to LABEL pairs.\n\nIn case COMMAND is an alist, every command in this alist is bound to its own\nlabel, and all of those are available under LABEL-named submenu.\n\nExample:\n\n\\(ffi-add-context-menu-command\n (list (list 'reload-current-buffer \\\"Reload it\\\")\n       (list (lambda () (delete-buffer :buffers (current-buffer))) \\\"Delete it\\\"))\n \\\"Buffer actions\\\")\"))\n\n\n;;; Signals\n\n(define-ffi-generic on-signal-notify-uri (object url)\n  (:method ((buffer buffer) no-url)\n    (declare (ignore no-url))\n    ;; Need to run the mode-specific actions first so that modes can modify the\n    ;; behavior of buffer.\n    (dolist (mode (enabled-modes buffer))\n      (on-signal-notify-uri mode (url buffer)))\n    (let ((view-url (ffi-buffer-url buffer)))\n      (unless (or (load-failed-p buffer)\n                  (url-empty-p view-url))\n        ;; When a buffer fails to load and `ffi-buffer-url' returns an empty\n        ;; URL, we don't set (url buffer) to keep access to the old value.\n        (setf (url buffer) (ffi-buffer-url buffer))))\n    (url buffer))\n  (:method ((mode mode) url)\n    url)\n  (:documentation \"Invoked when URL changes in OBJECT.\nDispatches on buffers and modes.\"))\n\n(define-ffi-generic on-signal-notify-title (object title)\n  (:method ((buffer buffer) no-title)\n    (declare (ignore no-title))\n    (setf (title buffer) (ffi-buffer-title buffer))\n    (dolist (mode (enabled-modes buffer))\n      (on-signal-notify-title mode (url buffer)))\n    (title buffer))\n  (:method ((mode mode) title)\n    (on-signal-notify-uri mode (url (buffer mode)))\n    title)\n  (:documentation \"Invoked when page TITLE is set in OBJECT.\nDispatches on buffers and modes.\"))\n\n;; See https://webkitgtk.org/reference/webkit2gtk/stable/WebKitWebView.html#WebKitLoadEvent\n(define-ffi-generic on-signal-load-started (object url)\n  (:method ((buffer buffer) url)\n    (dolist (mode (enabled-modes buffer))\n      (on-signal-load-started mode url)))\n  (:method ((mode mode) url)\n    url)\n  (:documentation \"Invoked when URL starts loading in OBJECT.\nDispatches on buffers and modes.\"))\n\n(define-ffi-generic on-signal-load-redirected (object url)\n  (:method ((buffer buffer) url)\n    (dolist (mode (enabled-modes buffer))\n      (on-signal-load-redirected mode url)))\n  (:method ((mode mode) url)\n    url)\n  (:documentation \"Invoked when the request gets redirected to URL in OBJECT.\nDispatches on buffers and modes.\"))\n\n(define-ffi-generic on-signal-load-canceled (object url)\n  (:method ((buffer buffer) url)\n    (dolist (mode (enabled-modes buffer))\n      (on-signal-load-canceled mode url)))\n  (:method ((mode mode) url)\n    url)\n  (:documentation \"Invoked when URL loading is canceled in OBJECT.\nDispatches on buffers and modes.\"))\n\n(define-ffi-generic on-signal-load-committed (object url)\n  (:method ((buffer buffer) url)\n    (dolist (mode (enabled-modes buffer))\n      (on-signal-load-committed mode url)))\n  (:method ((mode mode) url)\n    url)\n  (:documentation \"Invoked when URL loading is approved in OBJECT.\nDispatches on buffers and modes.\"))\n\n(define-ffi-generic on-signal-load-finished (object url title)\n  (:method ((buffer buffer) url title)\n    (if (equal (quri:uri-scheme url) \"nyxt\")\n        (alex:if-let ((internal-page (find-url-internal-page url)))\n          (progn\n            (enable-page-mode buffer (page-mode internal-page))\n            (html-write\n             (apply (form internal-page)\n                    (append\n                     (query-params->arglist (quri:uri-query-params url))\n                     (list :%buffer% buffer)))\n             buffer))\n          (warn \"No internal page corresponds to URL ~a\" url))\n        (disable-page-mode buffer))\n    (update-document-model :buffer buffer)\n    (dolist (mode (enabled-modes buffer))\n      (on-signal-load-finished mode url title))\n    (run-thread \"buffer-loaded-hook\"\n      (hooks:run-hook (buffer-loaded-hook buffer) buffer)))\n  (:method ((mode mode) url title)\n    url)\n  (:documentation \"Invoked when done loading URL in OBJECT.\nDispatches on buffers and modes.\"))\n\n(define-ffi-generic on-signal-load-failed (object url)\n  (:method ((buffer buffer) url)\n    (dolist (mode (enabled-modes buffer))\n      (on-signal-load-failed mode url)))\n  (:method ((mode mode) url)\n    url)\n  (:documentation \"Invoked when URL loading has failed in OBJECT.\nDispatches on buffers and modes.\"))\n\n(define-ffi-generic on-signal-button-press (object button-key)\n  (:method ((buffer buffer) button-key)\n    (dolist (mode (enabled-modes buffer))\n      (on-signal-button-press mode button-key)))\n  (:method ((mode mode) button-key)\n    (declare (ignorable button-key))\n    nil)\n  (:documentation \"Invoked on BUTTON-KEY press.\nDispatches on buffers and modes.\"))\n\n(define-ffi-generic on-signal-key-press (object key)\n  (:method ((buffer buffer) key)\n    (dolist (mode (enabled-modes buffer))\n      (on-signal-key-press mode key)))\n  (:method ((mode mode) key)\n    (declare (ignorable key))\n    nil)\n  (:documentation \"Invoked on KEY press.\nDispatches on buffers and modes.\"))\n"
  },
  {
    "path": "source/global.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt)\n;; Packagers are welcome to customize the `defparameter's to suit the host system.\n\n(export-always '*options*)\n(defvar *options* '()\n  \"The list of command line options.\")\n\n(defvar *run-from-repl-p* t\n  \"If non-nil, don't terminate the Lisp process when quitting the browser.\nThis is useful when the browser is run from a REPL so that quitting does not\nclose the connection.\")\n\n(defvar *restart-on-error* nil\n  \"Control variable to enable accurate error reporting during startup.\nImplementation detail.\nFor user-facing controls, see `*run-from-repl-p*'.\")\n\n(export-always '*open-program*)\n(declaim (type (or string null) *open-program*))\n(defvar *open-program*\n  #+darwin \"open\"\n  #+(and (or linux bsd) (not darwin)) \"xdg-open\"\n  #-(or linux bsd darwin) nil\n  \"The program to open unsupported files with.\")\n\n(export-always '*headless-p*)\n(defvar *headless-p* nil\n  \"If non-nil, don't display anything.\nThis is convenient for testing purposes or to drive Nyxt programmatically.\")\n\n(export-always '*quitting-nyxt-p*)\n(defvar *quitting-nyxt-p* nil\n  \"When non-nil, Nyxt is quitting.\")\n\n(export-always '*browser*)\n(defvar *browser* nil\n  \"The entry-point object to a complete instance of Nyxt.\nIt can be initialized with\n\n  (setf *browser* (make-instance 'browser))\n\nIt's possible to run multiple interfaces of Nyxt at the same time.  You can\nlet-bind *browser* to temporarily switch interface.\")\n\n\n(declaim (type (maybe renderer) *renderer*))\n(defparameter *renderer* nil\n  ;; TODO: Switching renderer does not seem to work anymore.\n  ;; Maybe issue at the library level?\n  \"The renderer used by Nyxt.\nIt can be changed between two runs of Nyxt when run from a Lisp REPL.\nExample:\n\n  (nyxt:quit)\n  (setf nyxt::*renderer* (make-instance 'nyxt/renderer/gtk:gtk-renderer))\n  (nyxt:start)\")\n\n(export-always '+version+)\n(alex:define-constant +version+\n    (or (uiop:getenv \"NYXT_VERSION\")\n        (asdf/component:component-version (asdf:find-system :nyxt)))\n  :test #'equal\n  :documentation \"Nyxt version.\nCan be overridden via NYXT_VERSION environment variable.\")\n\n(defun parse-version (version)\n  \"Helper to parse VERSION as a string.\n\nReturn NIL on error.\nReturn major version as an integer on pre-releases.\nOtherwise, return 3 values:\n- major version as an integer,\n- minor version as an integer,\n- patch version as an integer.\"\n  (ignore-errors\n   (if (search \"pre-release\" version)\n       (first (sera:words version))\n       (destructuring-bind (&optional major minor patch) (uiop:parse-version version)\n         (values major minor patch)))))\n\n(defun version ()\n  \"Get the version of Nyxt parsed as multiple values.\nSee `parse-version' for details on the returned values.\"\n  (parse-version +version+))\n\n(multiple-value-bind (major minor patch) (version)\n  (flet ((push-feature (string)\n           (pushnew (intern (uiop:strcat \"NYXT-\"\n                                         (string-upcase (princ-to-string string)))\n                            \"KEYWORD\")\n                    *features*)))\n    (when +version+ (push-feature +version+))\n    (when major (push-feature major))\n    (when minor (push-feature (format nil \"~a.~a\" major minor)))\n    (when patch (push-feature (format nil \"~a.~a.~a\" major minor patch)))))\n\n(export-always '*static-data*)\n(defvar *static-data* (make-hash-table :test 'equal)\n  \"Static data for usage in Nyxt.\")\n\n(defun load-assets (subdirectory read-function)\n  (mapcar (lambda (i)\n            (setf (gethash (file-namestring i) *static-data*)\n                  (funcall read-function i)))\n          (uiop:directory-files (asdf:system-relative-pathname :nyxt (format nil \"assets/~a/\" subdirectory)))))\n\n(load-assets \"fonts\" #'alex:read-file-into-byte-vector)\n(load-assets \"glyphs\" #'alex:read-file-into-string)\n\n;; Load assets needed for `nyxt.desktop' creation.\n#+(and unix (not darwin))\n(load-assets \"icons\" #'alex:read-file-into-byte-vector)\n#+(and unix (not darwin))\n(setf (gethash \"nyxt.desktop\" *static-data*)\n      (alex:read-file-into-string\n       (asdf:system-relative-pathname :nyxt \"assets/nyxt.appimage.desktop\")))\n\n(defun appimage-p ()\n  \"Is the Lisp image running within the context of an AppImage?\"\n  (uiop:getenv \"APPIMAGE_PATH\"))\n"
  },
  {
    "path": "source/help.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt)\n\n;; Moved here so all the `nyxt-packages' are defined by the moment it's set.\n(setf sym:*default-packages* (append '(:nyxt-user) (nyxt-packages)))\n\n(defmacro command-docstring-first-sentence (fn &key (sentence-case-p nil))\n  \"Print FN first docstring sentence in HTML.\"\n  `(if (fboundp ,fn)\n       (spinneret:with-html\n         (:span\n          (or ,(if sentence-case-p\n                   `(sera:ensure-suffix (str:sentence-case (first (ppcre:split \"\\\\.\\\\s\" (documentation ,fn 'function)))) \".\")\n                   `(sera:ensure-suffix (first (ppcre:split \"\\\\.\\\\s\" (documentation ,fn 'function))) \".\"))\n              (error \"Undocumented function ~a.\" ,fn))))\n       (error \"~a is not a function.\" ,fn)))\n\n(defmacro command-information (fn)\n  \"Print FN keybinding and first docstring sentence in HTML.\"\n  `(spinneret:with-html (:li (:nxref :command ,fn) \": \" (command-docstring-first-sentence ,fn))))\n\n(defun list-command-information (fns)\n  \"Print information over a list of commands in HTML.\"\n  (dolist (i fns)\n    (command-information i)))\n\n(defun configure-slot (slot class &key\n                                    (type (getf (mopu:slot-properties (find-class class) slot)\n                                                :type))\n                                    (sources 'prompter:raw-source)\n                                    (input \"\")\n                                    (postprocess #'read-from-string))\n  \"Set value of CLASS' SLOT in `*auto-config-file*'.\nPrompt for a new value with prompt SOURCES and type-check\n it against the SLOT's TYPE, if any. CLASS is a class symbol.\"\n  (sera:nlet lp ()\n    (let* ((input (prompt\n                   :prompt (format nil \"Configure slot value ~a\" slot)\n                   :input input\n                   :sources sources))\n           (input (if (serapeum:single input)\n                      (funcall postprocess (first input))\n                      (funcall postprocess input))))\n      (cond\n        ((and type (not (typep input type)))\n         (echo-warning \"Type mismatch for ~a: got ~a, expected ~a.\"\n                       slot (type-of input) type)\n         (lp))\n        (t\n         (auto-configure :class-name class :slot slot :slot-value input)\n         (echo \"Update slot ~s to ~s. You might need to restart to experience the change.\" slot input))))))\n\n(define-internal-page-command-global common-settings (&key (section 'keybindings))\n    (buffer \"*Settings*\" 'nyxt/mode/help:help-mode)\n  \"Display an interface to tweak frequently sought-after user options.\nThe changes are saved to `*auto-config-file*', and persist from one Nyxt session\nto the next.\"\n  (spinneret:with-html-string\n    (:nstyle\n      (theme:themed-css (theme *browser*)\n        '(\"body,h3\"\n          :margin 0)\n        '(\".radio-div,.checkbox-div\"\n          :margin-top \"1em\")\n        '(\".radio-label,.checkbox-input\"\n          :display block\n          :padding-bottom \"0.5em\")\n        '(\".radio-input,.checkbox-input\"\n          :display inline-block\n          :margin-right \"0.5em\"\n          :margin-left \"3em\")\n        '(\"select.button,.button\"\n          :display block\n          :margin \"1em 0 0.5em 2.5em\")\n        '(.section\n          :margin 0\n          :padding \"2em 0 0.5em 1.5em\")\n        '(.row\n          :display \"flex\"\n          :margin-top \"1em\")\n        `(.tabs\n          :flex \"0 0 180px\"\n          :background-color ,theme:background-color+)\n        `(.content\n          :flex \"85%\"\n          :background-color ,theme:background-color-)\n        '(.left\n          :flex \"0 0 256px\")\n        `(.right\n          :color ,theme:primary-color\n          :padding-top \"1em\"\n          :padding-left \"4em\"\n          :max-width \"50ch\")\n        '(p\n          :margin 0\n          :padding-bottom \"0.5em\")\n        `(.tab-button\n          :display \"block\"\n          :text-decoration \"none\"\n          :background-color ,theme:background-color+\n          :color ,theme:action-color-\n          :padding \"1.5em 1em\"\n          :width \"100%\"\n          :border \"none\"\n          :outline \"none\"\n          :text-align \"left\"\n          :cursor \"pointer\")\n        `((:and .tab-button :hover)\n          :background-color ,theme:background-color-)\n        `(.tab-button.active\n          :background-color ,theme:background-color-\n          :color ,theme:on-background-color)))\n    (:div.row\n     :style \"min-height: 100%; margin: 0\"\n     (:div.tabs\n      (:a.tab-button\n       :class (when (equal section 'keybindings) \"active\")\n       :href (nyxt-url 'common-settings :section 'keybindings)\n       \"Keyboard shortcuts\")\n      (:a.tab-button\n       :class (when (equal section 'theme-and-style) \"active\")\n       :href (nyxt-url 'common-settings :section 'theme-and-style)\n       \"Theme & Style\")\n      (:a.tab-button\n       :class (when (equal section 'buffer-defaults) \"active\")\n       :href (nyxt-url 'common-settings :section 'buffer-defaults)\n       \"Buffer settings\"))\n     (:div.content\n      (:div.section\n       (:h2 \"Settings applied on restart.\"))\n      (case section\n        (keybindings\n         (:div.section\n          (:h3 \"Keybindings\")\n          (:div.row\n           (:div.left\n            (:nradio\n              :name \"keyscheme\"\n              :vertical t\n              :checked (cond ((find \"nyxt/mode/vi\" (default-modes (current-buffer))\n                                    :key #'uiop:symbol-package-name :test #'string-equal)\n                              'vi)\n                             ((find \"nyxt/mode/emacs\" (default-modes (current-buffer))\n                                    :key #'uiop:symbol-package-name :test #'string-equal)\n                              'emacs)\n                             (t 'cua))\n              :buffer buffer\n              '(cua \"CUA (default)\"\n                (nyxt::auto-configure\n                 :form '(define-configuration (input-buffer)\n                         ((default-modes (remove-if (lambda (m)\n                                                      (find (symbol-name m)\n                                                            '(\"EMACS-MODE\" \"VI-NORMAL-MODE\" \"VI-INSERT-MODE\")\n                                                            :test #'string=))\n                                          %slot-value%))))))\n              '(emacs \"Emacs\"\n                (nyxt::auto-configure\n                 :form '(define-configuration (input-buffer)\n                         ((default-modes (pushnew 'nyxt/mode/emacs:emacs-mode %slot-value%))))))\n              '(vi \"vi\"\n                (progn\n                  (nyxt::auto-configure\n                   :form '(define-configuration (input-buffer)\n                           ((default-modes (pushnew 'nyxt/mode/vi:vi-normal-mode %slot-value%)))))\n                  (nyxt::auto-configure\n                   :form '(define-configuration (prompt-buffer)\n                           ((default-modes (pushnew 'nyxt/mode/vi:vi-insert-mode %slot-value%)))))))))\n           (:div.right\n            (:p \"Set the default keybindings (requires restarting Nyxt).\")\n            (:p \"Keybindings can also be enabled on a per-buffer basis by\ninvoking the \" (:nxref :command 'toggle-modes) \"command.\")))))\n        (theme-and-style\n         (:div.section\n          (:h3 \"Browser interface\")\n          (:div.row\n           (:div.left\n            (:nradio\n              :name \"theme\"\n              :checked (if (eq (theme *browser*) theme:+light-theme+)\n                           'theme:+light-theme+\n                           'theme:+dark-theme+)\n              :vertical t\n              :buffer buffer\n              '(theme:+light-theme+ \"Light theme\"\n                (nyxt::auto-configure :form '(define-configuration browser\n                                              ((theme theme:+light-theme+)))))\n              '(theme:+dark-theme+ \"Dark theme\"\n                (nyxt::auto-configure :form '(define-configuration browser\n                                              ((theme theme:+dark-theme+)))))))))\n         (:div.section\n          (:h3 \"Webpage theme\")\n          (:div.row\n           (:div.left\n            (:nradio\n              :name \"darken\"\n              :checked (if (find 'nyxt/mode/style:dark-mode (default-modes (current-buffer)))\n                           'dark\n                           'auto)\n              :vertical t\n              :buffer buffer\n              '(auto \"Default\"\n                (nyxt::auto-configure\n                 :form '(define-configuration (web-buffer)\n                         ((default-modes (remove-if (lambda (m)\n                                                      (string= (symbol-name m) \"DARK-MODE\"))\n                                          %slot-value%))))))\n              '(dark \"Darkened\"\n                (nyxt::auto-configure\n                 :form '(define-configuration (web-buffer)\n                         ((default-modes (pushnew 'nyxt/mode/style:dark-mode %slot-value%))))))))))\n         (:div.section\n          (:h3 \"Default zoom\")\n          (:div.row\n           (:div.left\n            (:nselect\n              :id \"default-zoom-ratio\"\n              :default (format nil \"~a%\" (* 100 (zoom-ratio-default (current-buffer))))\n              (loop for number in '(30 50 67 80 90 100\n                                    110 120 133 150 170\n                                    200 240 300 400 500)\n                    collect `((,number ,(format nil \"~a%\" number))\n                              (nyxt::auto-configure\n                               :class-name 'document-buffer\n                               :slot 'zoom-ratio-default\n                               :slot-value ,(/ number 100.0)))))))))\n        (buffer-defaults\n         (:div.section\n          (:h3 \"Homepage\")\n          (:div.row\n           (:div.left\n            (:nbutton :text \"Set default URL\"\n              '(nyxt::configure-slot 'default-new-buffer-url 'browser\n                :sources (list\n                          (make-instance\n                           'prompter:raw-source\n                           :name \"New URL\")\n                          (make-instance\n                           'global-history-source\n                           :enable-marks-p nil\n                           :actions-on-return #'identity))\n                :input (render-url (default-new-buffer-url *browser*))\n                :postprocess (lambda (url-or-history-entry)\n                               (render-url (url url-or-history-entry))))))\n           (:div.right\n            (:p \"By default, it is set to Nyxt's home screen.\"))))\n         (:div.section\n          (:h3 \"Modes\")\n          (:div.row\n           (:div.left\n            (:nbutton :text \"Set default modes\"\n              '(nyxt::configure-slot 'default-modes 'buffer\n                :sources (make-instance\n                          'mode-source\n                          :marks (default-modes (current-buffer)))\n                :postprocess (lambda (modes)\n                               `(quote ,modes))\n                :type 'cons)))\n           (:div.right\n            (:p \"Specify default modes for new buffers.\")\n            (:p \"Modes can also be set interactively by command \"\n                (:nxref :command 'toggle-modes)\n                \" ,or by specific mode togglers such as \"\n                (:nxref :command 'nyxt/mode/no-sound:no-sound-mode) \".\"))))))))))\n\n(defun tls-help (buffer url)\n  \"Helper function invoked upon TLS certificate errors.\"\n  (setf (status buffer) :failed)\n  (html-set\n   (spinneret:with-html-string\n     (:h1 (format nil \"TLS Certificate Error: ~a\" (render-url url)))\n     (:p \"The address you are trying to visit has an invalid\ncertificate. By default Nyxt refuses to establish a secure connection\nto a host with an erroneous certificate (e.g. self-signed ones). This\ncould mean that the address you are attempting the access is\ncompromised.\")\n     (:p \"If you trust the address nonetheless, you can add an exception\nfor the current hostname with the \"\n         (:code \"add-domain-to-certificate-exceptions\")\n         \" command.  The \"\n         (:code \"certificate-exception-mode\")\n         \" must be active for the current buffer (which is the\ndefault).\")\n     (:p \"To persist hostname exceptions in your initialization\nfile, see the \"\n         (:code \"add-domain-to-certificate-exceptions\")\n         \" documentation.\"))\n   buffer))\n\n(define-command nyxt-version ()\n  \"Display the version of Nyxt in the `message-buffer'.\nThe value is saved to clipboard.\"\n  (trivial-clipboard:text +version+)\n  (echo \"Version ~a\" +version+))\n\n(define-internal-page-command-global new ()\n    (buffer \"*New buffer*\")\n  \"Display a page suitable as `default-new-buffer-url'.\"\n  (spinneret:with-html-string\n    (:nstyle\n      `(body\n        :min-height \"100vh\"\n        :padding \"0\"\n        :margin \"0\")\n      `(.container\n        :padding \"32px\"\n        :display \"grid\"\n        :grid-template-columns \"1fr 2fr\"\n        :gap \"2px\"\n        :height \"100%\")\n      `(.button\n        :background-color ,theme:secondary-color+\n        :padding \"7px\"\n        :border \"none\"\n        :color ,theme:on-secondary-color\n        :min-width \"100px\")\n      `(\"nav .button\"\n        :display \"block\"\n        :text-align \"left\"\n        :font-size \"small\")\n      `(\"#quick-access\"\n        :margin-top \"64px\")\n      `(.copyright\n        :position \"absolute\"\n        :bottom \"12px\"\n        :right \"48px\")\n      `(.program-name\n        :color ,theme:action-color\n        :font-size \"24px\"\n        :font-weight \"bold\")\n      `(\"#start-container\"\n        :display \"flex\"\n        :align-items \"center\")\n      `(.start\n        :display \"flex\"\n        :flex-direction \"row\")\n      `(.logo\n        :color ,(if (theme:dark-p theme:theme)\n                    theme:action-color\n                    theme:on-background-color)\n        :width \"100px\"\n        :height \"100px\"\n        :padding-top \"3px\"\n        :margin-right \"12px\")\n      `(\".logo svg\"\n        :border-radius \"4px\")\n      `(.set-url\n        :min-width \"180px\"\n        :height \"40px\"\n        :line-height \"30px\"\n        :color ,theme:on-primary-color\n        :background-color ,theme:action-color\n        :border \"none\"\n        :border-width \"2px\"\n        :margin-bottom \"16px\")\n      `(.execute-command\n        :min-width \"180px\"\n        :line-height \"12px\"\n        :height \"40px\"\n        :border \"none\"\n        :background-color ,theme:primary-color\n        :border-color ,theme:primary-color\n        :color ,theme:on-primary-color)\n      `(.binding\n        :margin-left \"12px\"\n        :font-weight \"bold\"\n        :color ,theme:secondary-color))\n    (:div\n     :class \"container\"\n     (:nav\n      (:div\n       (when (appimage-p)\n         (:nbutton\n           :text \"Install Desktop Shortcut\"\n           :title \"Install a `.desktop` entry so that Nyxt can be ran from your launcher.\"\n           '(add-desktop-entry)))\n       (:nbutton\n         :text \"Quick-Start\"\n         :title \"Display a short tutorial.\"\n         '(quick-start))\n       (:nbutton\n         :text \"Hotkeys\"\n         :title \"List all keyboard shortcuts for the current buffer.\"\n         '(make-buffer-focus :url (nyxt-url 'describe-bindings)))\n       (:nbutton\n         :text \"Manual\"\n         :title \"Detailed Nyxt documentation including configuration guides.\"\n         '(make-buffer-focus :url (nyxt-url 'manual)))\n       (:nbutton\n         :text \"Settings\"\n         :title \"Set keyboard shortcuts (CUA/Emacs/vi), homepage URL or zoom.\"\n         '(make-buffer-focus :url (nyxt-url 'common-settings))))\n      (:div\n       :id \"quick-access\"\n       (:nbutton\n         :text \"Bookmarks\"\n         :title \"View all bookmarks.\"\n         '(make-buffer-focus :url (nyxt-url 'nyxt/mode/bookmark:list-bookmarks)))\n       (:nbutton\n         :text \"Downloads\"\n         :title \"View downloads for the current session.\"\n         '(make-buffer-focus :url (nyxt-url 'nyxt/mode/download:list-downloads)))\n       (:nbutton\n         :text \"History\"\n         :title \"List history.\"\n         '(make-buffer-focus :url (nyxt-url 'nyxt/mode/history:list-history)))\n       (:nbutton\n         :text \"Annotations\"\n         :title \"Show annotations for all pages.\"\n         '(make-buffer-focus :url (nyxt-url 'nyxt/mode/annotate:show-annotations)))\n       (:nbutton\n         :text \"Buffers\"\n         :title \"List all buffers.\"\n         '(make-buffer-focus :url (nyxt-url 'nyxt/mode/buffer-listing:list-buffers)))))\n     (:div :id \"start-container\"\n           (:div :class \"start\"\n                 (:div :class \"logo\" (:raw (glyph-logo *browser*)))\n                 (:div\n                  (:div (:nbutton :class \"set-url\" :text \"Set-URL\"\n                          '(set-url))\n                        (:span :class \"binding\"\n                               (format nil \"(~a)\" (nyxt::binding-keys 'set-url))))\n                  (:div (:nbutton :class \"execute-command\" :text \"Execute-Command\"\n                          '(execute-command))\n                        (:span :class \"binding\"\n                               (format nil \"(~a)\" (nyxt::binding-keys 'execute-command)))))))\n     (:p :class \"copyright\"\n         (:span :class \"program-name\" \"Nyxt\")\n         (format nil \" ~a (~a)\" +version+ (name *renderer*))\n         (:br)\n         (format nil \"Atlas Engineer, 2018-~a\" (time:timestamp-year (time:now)))))))\n\n(define-internal-page-command-global manual ()\n    (buffer \"*Manual*\" 'nyxt/mode/help:help-mode)\n  \"Display Nyxt manual.\"\n  (spinneret:with-html-string\n    (:nstyle '(body :max-width \"80ch\"))\n    (:ntoc\n      (tutorial-content)\n      (manual-content))))\n\n(define-internal-page-command-global tutorial ()\n    (buffer \"*Tutorial*\" 'nyxt/mode/help:help-mode)\n  \"Display Nyxt tutorial.\"\n  (spinneret:with-html-string\n    (:nstyle '(body :max-width \"80ch\"))\n    (:h1 \"Nyxt tutorial\")\n    (:p \"The following tutorial introduces core concepts and\nbasic usage.  For more details, especially regarding configuration, see\nthe \" (:code (:a.link :href (nyxt-url 'manual) \"manual\")) \".\")\n    (tutorial-content)))\n\n(define-internal-page-command-global show-system-information ()\n    (buffer \"*System information*\")\n  \"Display information about the currently running Nyxt system.\n\nIt is of particular interest when reporting bugs.  The content is saved to\nclipboard.\"\n  (let* ((*print-length* nil)\n         (nyxt-information (system-information)))\n    (prog1\n        (spinneret:with-html-string\n          (:h1 \"System information\")\n          (:pre nyxt-information))\n      (copy-to-clipboard nyxt-information)\n      (log:info nyxt-information)\n      (echo \"System information saved to clipboard.\"))))\n\n(define-command-global report-bug ()\n  \"Report a bug on Nyxt's issue tracker.\"\n  (make-buffer-focus :url (quri:uri \"https://github.com/atlas-engineer/nyxt/issues/new?&template=bug_report.md\")))\n\n(define-internal-page-command-global list-extensions\n    (&key (endpoint \"https://nyxt-browser.com/api/extensions\"))\n    (buffer \"*Nyxt extensions*\" 'nyxt/mode/help:help-mode)\n  \"List available extensions for Nyxt.\nThe `:download-link' is overloaded as a reference URL for external extensions as\nthey are not served on the Nyxt website.\"\n  (flet ((extension->html (extension)\n           (spinneret:with-html\n             (:dl\n              (:dt \"Name\")\n              (:dd (assoc-value extension :name))\n              (:dt \"Description\")\n              (:dd (assoc-value extension :description))\n              (:dt \"Link\")\n              (let ((link (if (assoc-value extension :internal-p)\n                              (format nil \"https://nyxt-browser.com~a\"\n                                      (assoc-value extension :link))\n                              (assoc-value extension :download-link))))\n                (:dd (:a :href link link)))))))\n    (spinneret:with-html-string\n      (:h1 \"Nyxt extensions\")\n      (loop for extension in (cl-json:decode-json-from-string (dex:get endpoint))\n            collect (:div (extension->html extension)\n                          (:hr))))))\n"
  },
  {
    "path": "source/history.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt)\n\n(define-class history-file (files:data-file nyxt-lisp-file)\n  ((files:base-path #p\"history/default\")\n   (files:name \"history\"))\n  (:export-class-name-p t))\n\n(define-class history-entry ()\n  ((url\n    (quri:uri \"\")\n    :writer nil\n    :type (or quri:uri string))\n   (title \"\"))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:documentation \"Entry for the global history.\"))\n\n(defmethod (setf url) (value (he history-entry))\n  (setf (slot-value he 'url) (url value)))\n\n(defmethod prompter:object-attributes\n    ((entry history-entry) (source prompter:source))\n  (declare (ignore source))\n  `((\"Title\" ,(title entry) (:width 3))\n    (\"URL\" ,(render-url (url entry)) (:width 2))))\n\n(export-always 'equals)\n(defmethod equals ((e1 history-entry) (e2 history-entry))\n  (quri:uri= (url e1) (url e2)))\n\n(defmethod url ((he history-entry))\n  \"This accessor ensures we always return a `quri:uri'.\nThis is useful in cases the URL is originally stored as a string (for instance\nwhen deserializing a `history-entry').\"\n  (unless (quri:uri-p (slot-value he 'url))\n    (setf (slot-value he 'url) (url (slot-value he 'url))))\n  (slot-value he 'url))\n\n(defmethod serialize-history-entry ((entry history-entry) stream)\n  (unless (url-empty-p (url entry))\n    (flet ((write-slot (slot)\n             (let ((entry-slot (funcall slot entry)))\n               (unless (str:emptyp entry-slot)\n                 (format t \" :~a ~s\"\n                         (str:downcase slot)\n                         entry-slot)))))\n      (let ((*standard-output* stream))\n        (write-string \"(\")\n        (write-string \":url \")\n        (format t \"~s\" (render-url (url entry)))\n        (write-slot 'title)\n        (write-string \")\")))))\n\n(defmethod files:serialize\n    ((profile nyxt-profile) (file history-file) stream &key)\n  (let ((*package* (find-package :nyxt))\n        (*print-length* nil))\n    (write-string \"(\" stream)\n    (loop for entry across (history-vector *browser*)\n          do (write-string +newline+ stream)\n             (serialize-history-entry entry stream))\n    (write-string +newline+ stream)\n    (write-string \")\" stream)))\n\n(defmethod files:deserialize\n    ((profile nyxt-profile) (path history-file) raw-content &key)\n  (let ((*package* (find-package :nyxt))\n        (entries (safe-read raw-content)))\n    (mapcar (lambda (entry)\n              (when (getf entry :url)\n                (setf (getf entry :url)\n                      (quri:uri (getf entry :url))))\n              (apply #'make-instance 'history-entry entry))\n            entries)))\n"
  },
  {
    "path": "source/input.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt)\n\n(-> binding-keys (sym:function-symbol &key (:modes list)) *)\n(defun binding-keys\n    (fn &key (modes (if (current-buffer)\n                        (enabled-modes (current-buffer))\n                        (mapcar #'make-instance (default-modes nil)))))\n  ;; We can't use `(modes (make-instance 'buffer))' because modes are only\n  ;; instantiated after the buffer web view, which is not possible if there is\n  ;; no *browser*.\n  (let* ((current-buffer (current-buffer))\n         (buffer (or (current-buffer)\n                     (make-instance 'input-buffer)))\n         (keymaps (delete nil (mapcar #'keymap modes))))\n    (unwind-protect\n         (or (first (keymaps:pretty-binding-keys\n                     fn keymaps :print-style (keymaps:name (keyscheme buffer))))\n             \"unbound\")\n      (unless current-buffer\n        (ffi-buffer-delete buffer)))))\n\n(export-always 'current-keymaps)\n(defun current-keymaps\n    (&optional (buffer (let ((prompt-buffer (current-prompt-buffer)))\n                         (if (and prompt-buffer (ffi-focused-p prompt-buffer))\n                             prompt-buffer\n                             (current-buffer)))))\n  \"Return the list of `keymap' for the current buffer, ordered by priority.\nIf non-empty, return the result of BUFFER's `current-keymaps-hook' instead.\"\n  (let ((keymaps\n          (when (input-buffer-p buffer)\n            (delete nil (mapcar #'keymap (enabled-modes buffer))))))\n    (if (and (input-buffer-p buffer) (current-keymaps-hook buffer))\n        (hooks:run-hook (current-keymaps-hook buffer) keymaps buffer)\n        keymaps)))\n\n(defun all-keymaps (&optional (window (current-window)))\n  \"Return all keymaps for WINDOW, including the buffer keymaps and the\nprompt buffer keymaps.\"\n  (when-let ((buffer (active-buffer window)))\n    (delete nil\n            (mapcar #'keymap\n                    (append (enabled-modes buffer)\n                            (ignore-errors (enabled-modes\n                                            (current-prompt-buffer))))))))\n\n(-> pointer-event-p (keymaps:key) boolean)\n(defun pointer-event-p (key)\n  \"Return non-nil if KEY is a pointer event.\"\n  (coerce (str:starts-with? \"button\" (keymaps:key-value key))\n          'boolean))\n\n(defun keyspecs-without-keycode (keys)\n  (keymaps:keys->keyspecs\n   (mapcar (lambda (key) (keymaps:copy-key key :code 0))\n           keys)))\n\n(export-always 'keyspecs-with-keycode)\n(defun keyspecs-with-keycode (keys)\n  \"Like `keymaps:keys->keyspecs' but display keycodes as well.\"\n  (let ((no-code-specs (keyspecs-without-keycode keys)))\n    (if (find-if (complement #'zerop) keys :key #'keymaps:key-code)\n        (format nil \"~s [~a]\" no-code-specs (keymaps:keys->keyspecs keys))\n        (format nil \"~s\" no-code-specs))))\n\n(-> dispatch-command ((or sym:function-symbol function)) *)\n(export-always 'dispatch-command)\n(defun dispatch-command (function)\n  \"Run FUNCTION asynchronously.\"\n  (echo-dismiss)\n  (let ((ignored-commands '(execute-command\n                            execute-predicted-command\n                            next-suggestion\n                            previous-suggestion\n                            next-source\n                            previous-source))\n        (function-function (typecase function\n                             (symbol (symbol-function function))\n                             (function function))))\n    (unless (find (name function-function)\n                  ignored-commands\n                  :test (lambda (x y) (search (symbol-name x) (symbol-name y))))\n      (analysis:add-record (command-model *browser*)\n                           (list (last-command *browser*) function))\n      (setf (last-command *browser*) function-function))\n    (run-async function)))\n\n(export-always 'dispatch-input-event)\n(defun dispatch-input-event (event buffer)\n  \"Dispatch keys in BUFFER `key-stack'.\nReturn nil to forward to renderer or non-nil otherwise.\"\n  (with-accessors ((key-stack key-stack)) buffer\n    (labels ((keyspecs (key &optional translated-key)\n               (if translated-key\n                   (let ((specs (keyspecs key))\n                         (translated-specs (keyspecs translated-key)))\n                     (if (string= specs translated-specs)\n                         (format nil \"~a\" specs)\n                         (format nil \"~a (translated from ~a)\"\n                                 translated-specs\n                                 specs)))\n                   (keyspecs-with-keycode key))))\n      (when (input-buffer-p buffer)\n        (setf (last-event buffer) event))\n      (when (prompt-buffer-p buffer)\n        (run-thread \"update-prompt-buffer\"\n          (update-prompt-input\n           buffer\n           (ps-eval :buffer buffer\n             (ps:chain (nyxt/ps:qs document \"#input\") value)))))\n      (multiple-value-bind (bound-function matching-keymap translated-key)\n          (the keyscheme:nyxt-keymap-value\n               (keymaps:lookup-key key-stack (current-keymaps buffer)))\n        (cond\n          ((and (input-buffer-p buffer)\n                (forward-input-events-p buffer)\n                (and matching-keymap\n                     (not (str:starts-with-p \"passthrough-mode\"\n                                             (keymaps:name matching-keymap)))\n                     (not (str:starts-with-p \"vi-insert-mode\"\n                                             (keymaps:name matching-keymap)))))\n           (log:debug \"Forward key ~s.\" (keyspecs key-stack))\n           (setf key-stack nil)\n           nil)\n          ((keymaps:keymap-p bound-function)\n           (log:debug \"Prefix binding ~a.\" (keyspecs key-stack translated-key))\n           t)\n          ((typep bound-function '(and (not null) (or symbol command)))\n           (let ((command\n                   (typecase bound-function\n                     (symbol (symbol-function\n                              (resolve-user-symbol bound-function :command)))\n                     (command bound-function))))\n             (log:debug \"Found key binding ~a to ~a.\"\n                        (keyspecs key-stack translated-key) bound-function)\n             (setf (last-key buffer) (first key-stack))\n             (run-thread \"run-command\"\n               (unwind-protect (funcall (command-dispatcher *browser*) command)\n                 (setf key-stack nil)))\n             t))\n          (t\n           (log:debug \"Fallback forward key ~s.\" (keyspecs key-stack))\n           (setf key-stack nil)\n           nil))))))\n"
  },
  {
    "path": "source/inspector.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt)\n\n(defvar *inspected-values*\n  (tg:make-weak-hash-table :test 'equal :weakness :value)\n  \"All the values Nyxt inspected (with `describe-value', for example).\")\n\n(export-always 'sequence-p)\n(defun sequence-p (object)\n  \"Return true if OBJECT is a sequence that's not a string.\"\n  (typep object '(and sequence (not string))))\n\n(export-always 'scalar-p)\n(defun scalar-p (object)\n  ;; REVIEW: List direct T subclasses instead?\n  \"Return true if OBJECT is of one of the following types:\n- symbol,\n- character,\n- string,\n- non-complex number.\"\n  (typep object '(or symbol character string real)))\n\n(export-always 'inspected-value)\n(defmethod inspected-value (id)\n  \"Get the inspected value corresponding to ID.\"\n  (gethash id *inspected-values*))\n\n(defmethod (setf inspected-value) (new-value id)\n  \"Set the ID-indexed inspected value.\"\n  (setf (gethash id *inspected-values*) new-value))\n\n(defun ensure-inspected-id (value)\n  \"In case VALUE was already inspected, return its ID.\nIf it wasn't, add it to inspected values and return its new ID.\"\n  (maphash\n   (lambda (id object)\n     (when (equal value object)\n       (return-from ensure-inspected-id id)))\n   *inspected-values*)\n  (sera:lret ((id (new-id)))\n    (setf (inspected-value id) value)))\n\n\n(export-always '*inspector-print-length*)\n(defvar *inspector-print-length* 20\n  \"The size of the structure after which to collapse this structure into a link.\n\nCan cause a renderer to choke when set to a high value. Use with caution!\")\n\n(defun escaped-literal-print (value)\n  \"Print the constant/literal/`scalar-p' VALUE to HTML-escaped string.\"\n  (spinneret:with-html-string\n    (:code (:raw (spinneret:escape-string\n                  (let ((*print-lines* 2)\n                        (*print-length* *inspector-print-length*))\n                    (prini-to-string value)))))))\n\n(defun link-to (object)\n  \"Convert the OBJECT to a `describe-value' link inspecting it.\nIn case it's `scalar-p', simply print it.\"\n  (if (scalar-p object)\n      (spinneret:with-html-string\n        (:raw (escaped-literal-print object)))\n      (spinneret:with-html-string\n        (:a :href (nyxt-url 'describe-value :id (ensure-inspected-id object))\n            (:raw (escaped-literal-print object))))))\n\n(defun compact-listing (sequence &key table-p)\n  \"Print the SEQUENCE head as table with a link to the rest of the sequence.\"\n  (let ((length (min (length sequence) *inspector-print-length*)))\n    (spinneret:with-html-string\n      (cond\n        (table-p\n         (:table\n          (:tbody\n           (:tr\n            (dotimes (i length)\n              (:td (:raw (value->html (elt sequence i) t))))\n            (when (> (length sequence) *inspector-print-length*)\n              (:td \"More: \" (:raw (link-to sequence))))))))))))\n\n(export-always 'value->html)\n(defgeneric value->html (value &optional compact-p)\n  (:method :around (value &optional compact-p)\n    (let ((spinneret:*html-style* :tree))\n      (call-next-method value compact-p)))\n  (:method (value &optional compact-p)\n    (declare (ignore compact-p))\n    (escaped-literal-print value))\n  (:method ((value null) &optional compact-p)\n    (declare (ignore compact-p))\n    (escaped-literal-print value))\n  (:method ((value string) &optional compact-p)\n    (declare (ignore compact-p))\n    (escaped-literal-print value))\n  (:documentation \"Produce HTML showing the structure of the VALUE.\nIf it's COMPACT-P, compress the output.\n\nSpecialize this generic function if you want to have a different markup for Lisp\nvalues in help buffers, REPL and elsewhere.\"))\n\n(defmethod value->html ((value function) &optional compact-p)\n  (declare (ignore compact-p))\n  (spinneret:with-html-string (:raw (link-to value))))\n\n(defmethod value->html ((value list) &optional compact-p)\n  (spinneret:with-html-string\n    (:div\n     :style \"overflow-x: auto\"\n     (cond\n       ((not (null (cdr (last value))))\n        (:raw (escaped-literal-print value)))\n       (compact-p\n        (:raw (compact-listing value :table-p t)))\n       ((types:association-list-p value)\n        (:table\n         (unless compact-p\n           (:caption \"Association list\"))\n         (:thead\n          (dolist (e value)\n            (:th (:raw (value->html (car e) t)))))\n         (:tbody\n          (:tr\n           (dolist (e value)\n             (:td (:raw (value->html (rest e) t))))))))\n       ((and (types:property-list-p value)\n             ;; Stricter understanding of property lists:\n             ;; -- Even length.\n             ;; -- Keys are strictly keywords.\n             ;; -- At least one value should be a non-keyword.\n             (evenp (length value))\n             (loop with all-values-keywords? = t\n                   for (key val) on value by #'cddr\n                   unless (keywordp key)\n                     do (return nil)\n                   unless (keywordp val)\n                     do (setf all-values-keywords? nil)\n                   finally (return (not all-values-keywords?))))\n        (:table\n         (unless compact-p\n           (:caption \"Property list\"))\n         (:thead (loop for key in value by #'cddr\n                       collect (:th (:raw (escaped-literal-print key)))))\n         (:tbody\n          (:tr\n           (loop for val in (rest value) by #'cddr\n                 collect (:td (:raw (value->html val t))))))))\n       ((and (types:proper-list-p value)\n             (not (alex:circular-list-p value))\n             (not (alex:circular-tree-p value)))\n        (:ul\n         (dotimes (i (length value))\n           (:li (:raw (value->html (elt value i) t))))))\n       (t (:raw (escaped-literal-print value)))))))\n\n(defmethod value->html ((value array) &optional compact-p)\n  (spinneret:with-html-string\n    (cond\n      ((uiop:emptyp value)\n       (:raw (call-next-method)))\n      (compact-p\n       (:raw (compact-listing value :table-p t)))\n      (t (:div\n          :style \"overflow-x: auto\"\n          (case (length (array-dimensions value))\n            (1 (:table\n                (unless compact-p\n                  (:caption \"Array\")\n                  (:thead\n                   (:th :colspan (alex:lastcar (array-dimensions value))\n                        \"Elements (\" (princ-to-string (array-dimension value 0)) \")\")))\n                (:tbody\n                 (:tr\n                  (loop for e across value\n                        collect (:td (:raw (value->html e t))))))))\n            (2 (:table\n                (:tbody\n                 (loop with height = (array-dimension value 0)\n                       and width = (array-dimension value 1)\n                       for y below height\n                       collect (:tr (loop for x below width\n                                          collect (:td (:raw (value->html (aref value y x) t)))))))))\n            (otherwise (:raw (call-next-method)))))))))\n\n(defmethod value->html ((value sequence) &optional compact-p)\n  (spinneret:with-html-string\n    (cond\n      ((uiop:emptyp value)\n       (:raw (escaped-literal-print value)))\n      (compact-p\n       (:raw (compact-listing value :table-p compact-p)))\n      (t (:ul\n          (dotimes (i (length value))\n            (:li (:raw (value->html (elt value i) t)))))))))\n\n(defmethod value->html ((value hash-table) &optional compact-p)\n  (spinneret:with-html-string\n    (:div\n     :style \"overflow-x: auto\"\n     (let ((keys (alex:hash-table-keys value)))\n       (cond\n         ((uiop:emptyp keys)\n          (:raw (call-next-method)))\n         ((and compact-p (> (hash-table-count value) *inspector-print-length*))\n          (:raw (link-to value)))\n         (t (:table\n             (unless compact-p\n               (:caption \"Hash-table\"))\n             (:thead (dolist (key keys)\n                       (:th (:raw (escaped-literal-print key)))))\n             (:tbody\n              (:tr\n               (dolist (key keys)\n                 (:td (:raw (value->html (gethash key value) t)))))))))))))\n\n(defmethod value->html ((value pathname) &optional compact-p)\n  (let* ((namestring (uiop:native-namestring value))\n         (mime (mimes:mime namestring)))\n    (spinneret:with-html-string\n      (if compact-p\n          (:raw (link-to value))\n          (:a :href (quri.uri.file:make-uri-file :path namestring)\n              :title (if (uiop:directory-pathname-p value)\n                         \"directory\"\n                         mime)\n              (cond\n                ((and (uiop:directory-pathname-p value)\n                      (not compact-p))\n                 ;; REVIEW: This should use\n                 ;; `nyxt/mode/file-manager:directory-elements' (not accessible\n                 ;; at the time this is loaded) or an NFiles equivalent (should\n                 ;; we abstract most of File Manager to Nfiles?)\n                 (dolist (element (append (uiop:subdirectories value)\n                                          (uiop:directory-files value)))\n                   (:li (:raw (value->html element t)))))\n                ((and (str:starts-with-p \"image/\" mime)\n                      (not compact-p))\n                 (:figure\n                  (:figcaption namestring)\n                  (:img :src (quri.uri.file:make-uri-file :path namestring)\n                        :alt namestring)))\n                ((and (str:starts-with-p \"audio/\" mime)\n                      (not compact-p))\n                 (:figure\n                  (:figcaption namestring)\n                  (:audio :src (quri.uri.file:make-uri-file :path namestring)\n                          :controls t)))\n                ((and (str:starts-with-p \"video/\" mime)\n                      (not compact-p))\n                 (:figure\n                  (:figcaption namestring)\n                  (:video :src (quri.uri.file:make-uri-file :path namestring)\n                          :controls t)))\n                (t namestring)))))))\n\n(defun print-complex-object (value compact-p)\n  (if compact-p\n      (link-to value)\n      (spinneret:with-html-string\n        (if-let ((slot-names (mapcar #'closer-mop:slot-definition-name\n                                     (closer-mop:class-slots (class-of value)))))\n          (:dl\n           (dolist (slot-name slot-names)\n             (:dt (prini-to-string slot-name))\n             (:dd (:raw (value->html (slot-value value slot-name) t)))))\n          (:raw (escaped-literal-print value))))))\n\n(defmethod value->html ((value standard-object) &optional compact-p)\n  (print-complex-object value compact-p))\n\n(defmethod value->html ((value structure-object) &optional compact-p)\n  (print-complex-object value compact-p))\n"
  },
  {
    "path": "source/keyscheme.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n;; `uiop:define-package' instead of `nyxt:define-package' since it does not\n;; depend on Nyxt.\n(uiop:define-package :nyxt/keyscheme\n  (:use :cl)\n  (:import-from :serapeum #:export-always #:->)\n  (:documentation \"Nyxt type specialization for `keymap' bound values.\"))\n(in-package :nyxt) ; In case the package is locked.\n(trivial-package-local-nicknames:add-package-local-nickname :keyscheme :nyxt/keyscheme :nyxt)\n(in-package :nyxt/keyscheme)\n\n(export-always 'nyxt-keymap-value)\n(deftype nyxt-keymap-value ()\n  \"A Nyxt-specific type suitable for `nkeymaps:bound-type'.\"\n  '(or nkeymaps:keymap t))\n\n(export-always 'make-keyscheme)\n(defun make-keyscheme (name &rest parents)\n  \"Return a new `nkeymaps:keyscheme' object of type `nyxt-keymap-value'.\nThe keyscheme inherits from the optional PARENTS, ordered by priority.\n\nExample:\n\n  (defvar cua-child (make-keyscheme \\\"cua-child\\\" cua))\n\nThe above example defines a keyscheme called `cua-child', which inherits from\nthe existing keyscheme `cua'.\"\n  (the (values nkeymaps:keyscheme &optional)\n       (make-instance 'nkeymaps:keyscheme\n                      :name name\n                      :parents parents\n                      :bound-type 'nyxt-keymap-value)))\n\n(export-always 'default)\n(defvar default (make-keyscheme \"default\")\n  \"The root keyscheme from which all the other Nyxt keyschemes (command ones, at least) inherit.\")\n(export-always 'cua)\n(defvar cua (make-keyscheme \"cua\" default)\n  \"CUA (Common User Access) keyscheme with conventional bindings typical to major browsers.\")\n(export-always 'emacs)\n(defvar emacs (make-keyscheme \"emacs\" default)\n  \"Keyscheme inspired by Emacs text editor.\")\n(export-always 'vi-normal)\n(defvar vi-normal (make-keyscheme \"vi-normal\" default)\n  \"Keyscheme inspired by the command mode in VI-family text editors.\")\n(export-always 'vi-insert)\n(defvar vi-insert (make-keyscheme \"vi-insert\")\n  \"Keyscheme inspired by the insert mode in VI-family editors.\")\n"
  },
  {
    "path": "source/manual.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt)\n\n(defun manual-html ()\n  (spinneret:with-html-string\n    (:ntoc\n      (tutorial-content)\n      (manual-content))))\n\n(defun manual-content ()\n  (spinneret:with-html\n    (let ((auto-config-file (namestring (files:expand *auto-config-file*)))\n          (config-file (namestring (files:expand *config-file*)))\n          (gtk-extensions-directory (namestring (uiop:merge-pathnames* \"nyxt/\" nasdf:*libdir*))))\n      (:nsection :title \"Configuration\"\n        (:p \"Nyxt is written in the Common Lisp programming language which offers a\ngreat perk: everything in the browser can be customized by the user, even while\nit's running!\")\n        (:p \"To get started with Common Lisp, we recommend checking out\n    our web page: \"\n            (:a :href \"https://nyxt-browser.com/learn-lisp\" \"Learn Lisp\")\n            \". It contains numerous pointers to other resources, including\n        free books both for beginners and seasoned programmers.\")\n        (unless (str:empty? auto-config-file)\n          (:p \"Settings created by Nyxt are stored in \" (:code auto-config-file) \".\"))\n        (unless (str:empty? config-file)\n          (:p \"Any settings can be overridden manually by \" (:code config-file) \".\"))\n        (:p \"The following section assumes knowledge of basic Common Lisp or a\nsimilar programming language.\")\n        (:p \"The user needs to manually create the Nyxt configuration file, and the parent folders if necessary.\"\n            (when (and (current-buffer) ; In case manual is dumped.\n                       (not (files:nil-pathname-p config-file))\n                       (not (uiop:file-exists-p config-file)))\n              (:p \"You can also press the button below to create it.\"\n                  (:p (:a :class \"button\"\n                          :onclick (ps:ps\n                                     (nyxt/ps:lisp-eval\n                                      (:title \"create-config-file\")\n                                      (ensure-directories-exist config-file)\n                                      (files:ensure-file-exists config-file)\n                                      (echo \"Configuration file created at ~s.\" config-file)))\n                          \"Create configuration file\")))))\n        (:p \"Example:\")\n        (:ncode\n          '(define-configuration web-buffer\n            ((default-modes (pushnew 'nyxt/mode/no-sound:no-sound-mode %slot-value%)))))\n        (:p \"The above turns on the 'no-sound-mode' (disables sound) by default for\nevery buffer.\")\n        (:p \"The \" (:nxref :macro 'define-configuration) \" macro can be used to customize\nthe slots of classes like the browser, buffers, windows, etc.\")\n        (:p \"To find out about all modes known to Nyxt,\nrun \" (:nxref :command 'describe-command) \" and type 'mode'.\"))\n\n      (:nsection :title \"Different types of buffers\"\n        (:p \"There are multiple buffer classes, such as \"\n            (:nxref :class-name 'document-buffer) \" (for structured documents) and \"\n            (:nxref :class-name 'input-buffer) \" (for buffers that can receive user input).  A \"\n            (:nxref :class-name 'web-buffer) \" class is used for web pages, \" (:nxref :class-name 'prompt-buffer)\n            \" for, well, the prompt buffer.  Some buffer classes may inherit from multiple other classes.\nFor instance \" (:nxref :class-name 'web-buffer) \" and \" (:nxref :class-name 'prompt-buffer)\n            \" both inherit from \" (:nxref :class-name 'input-buffer) \".\")\n        (:p \"You can configure one of the parent \" (:nxref :class-name 'buffer) \" classes slots and the new\nvalues will automatically cascade down as a new default for all child classes-\nunless this slot is specialized by these child classes.\"))\n\n      (:nsection :title \"Keybinding configuration\"\n        (:p \"Nyxt supports multiple \" (:i \"bindings schemes\") \" such as CUA (the\n    default), Emacs or vi.  Changing scheme is as simple as setting the\n    corresponding mode as default, e.g. \"\n            (:nxref :class-name 'nyxt/mode/emacs:emacs-mode) \".  To make the change persistent across sessions,\nadd the following to your configuration:\")\n        (:ul\n         (:li \"vi bindings:\"\n              (:ncode\n                '(define-configuration input-buffer\n                  ((default-modes (pushnew 'nyxt/mode/vi:vi-normal-mode %slot-value%))))))\n         (:li \"Emacs bindings:\"\n              (:ncode\n                '(define-configuration input-buffer\n                  ((default-modes (pushnew 'nyxt/mode/emacs:emacs-mode %slot-value%)))))))\n        (:p \"You can create new scheme names with \" (:nxref :function 'nkeymaps:make-keyscheme)\n            \".  Also see the \"\n            (:nxref :function 'keymaps:define-keyscheme-map \"define-keyscheme-map macro\") \".\")\n        (:p \"To extend the bindings of a specific mode, you can configure the mode with \"\n            (:nxref :macro 'define-configuration) \" and extend its \"\n            (:nxref :slot 'keyscheme-map :class-name 'mode) \" with \"\n            (:nxref :function 'keymaps:define-keyscheme-map) \". For example:\")\n        (:ncode\n          '(define-configuration base-mode\n            \"Note the :import part of the `define-keyscheme-map'.\nIt re-uses the other keymap (in this case, the one that was slot value before\nthe configuration) and merely adds/modifies it.\"\n            ((keyscheme-map\n              (define-keyscheme-map\n               \"my-base\" (list :import %slot-value%)\n               nyxt/keyscheme:vi-normal\n               (list \"g b\" (lambda-command switch-buffer* ()\n                             (switch-buffer :current-is-last-p t))))))))\n        (:p \"The \" (:nxref  :command 'nothing) \" command is useful to override bindings to do\nnothing. Note that it's possible to bind any command, including those of\ndisabled modes that are not listed in \" (:nxref :command 'execute-command)\n\". Binding to \" (:nxref :command 'nothing)\n\" and binding to NIL means different things (see the documentation of \"\n(:nxref :function 'keymaps:define-key) \" for details):\")\n        (:dl\n         (:dt (:nxref  :command 'nothing))\n         (:dd \"Binds the key to a command that does nothing. Still discovers the key and\nrecognizes it as pressed.\")\n         (:dt \"NIL\")\n         (:dd \"Un-binds the key, removing all the bindings that it had in a given\nmode/keyscheme-map. If you press the un-bound key, the bindings that used to be\nthere will not be found anymore, and the key will be forwarded to the renderer.\")\n         (:dt \"Any other symbol/command\")\n         (:dd \"Replaces the command that was there before, with the new one. When the key is\npressed, the new command will fire instead of the old one.\"))\n        (:p \"In addition, a more flexible approach is to create your own mode with\nyour custom keybindings.  When this mode is added first to the buffer mode list,\nits keybindings have priorities over the other modes.\nNote that this kind of global keymaps also have priority over regular character\ninsertion, so you should probably not bind anything without modifiers in such a\nkeymap.\")\n        (:ncode\n          '(defvar *my-keymap* (keymaps:make-keymap \"my-map\"))\n          '(define-key *my-keymap*\n            \"C-f\" 'nyxt/mode/history:history-forwards\n            \"C-b\" 'nyxt/mode/history:history-backwards)\n\n          '(define-mode my-mode ()\n            \"Dummy mode for the custom key bindings in `*my-keymap*'.\"\n            ((keyscheme-map (keymaps:make-keyscheme-map\n                             nyxt/keyscheme:cua *my-keymap*\n                             nyxt/keyscheme:emacs *my-keymap*\n                             nyxt/keyscheme:vi-normal *my-keymap*))))\n\n          '(define-configuration web-buffer\n            \"Enable this mode by default.\"\n            ((default-modes (pushnew 'my-mode %slot-value%)))))\n        (:p \"Bindings are subject to various translations as per \"\n            (:nxref :variable 'nkeymaps:*translator*) \". \"\n            \"By default if it fails to find a binding it tries again with inverted\nshifts.  For instance if \" (:code \"C-x C-F\") \" fails to match anything \" (:code \"C-x C-f\")\n            \" is tried.\"\n            \"See the default value of \" (:nxref :variable 'nkeymaps:*translator*) \" to learn how to\n         customize it or set it to \" (:code \"nil\") \" to disable all forms of\n         translation.\"))\n\n      (:nsection :title \"Search engines\"\n        (:p \"The following search engines are defined, where the default one is\n        the first: \" (:ncode (getf (mopu:slot-properties 'browser 'search-engines)\n                                   :initform)))\n        (:p \"The \" (:code \":shortcut\") \" parameter above impacts the behavior of\n        commands such as \" (:nxref :command 'set-url) \". For example, typing \"\n        (:code \"foo\") \" or \" (:code \"ddg foo\") \" both results in querying\n        DuckDuckGo for \" (:code \"foo\") \" (meaning that the shortcut may be\n        omitted when using the default search engine). As you might have\n        guessed, \" (:code \"wiki foo\") \" queries Wikipedia instead.\")\n\n        (:p \"The example below exemplifies how to define additional search engines:\")\n        (:ncode\n          '(defvar *my-search-engines*\n            (list (make-instance 'search-engine\n                   :name \"Google\"\n                   :shortcut \"goo\"\n                   :control-url \"https://duckduckgo.com/?q=~a\")\n             (make-instance 'search-engine\n              :name \"MDN\"\n              :shortcut \"mdn\"\n              :control-url \"https://developer.mozilla.org/en-US/search?q=~a\")))\n\n          '(define-configuration browser\n            ((search-engines (append %slot-default% *my-search-engines*)))))\n        (:p \"Note that the default search engine is determined by \"\n            (:nxref :function 'default-search-engine)\n            \" (by default, the first element of \"\n            (:nxref :slot 'search-engines :class-name 'browser)\n            \").  Therefore, the order of arguments passed to \"\n            (:code \"append\") \" in the code snippet above is key.\")\n        (:p \"For more information on the topic see \"\n            (:nxref :class-name 'search-engine) \".\"))\n\n      (:nsection :title \"History\"\n        (:p \"Nyxt history model is a vector whose elements are URLs.\")\n        (:p \"History can be navigated with the arrow keys in the status buffer, or with\ncommands like \" (:nxref :command 'nyxt/mode/history:history-backwards) \" and \"\n(:nxref :command 'nyxt/mode/history:history-forwards)\n\" (which the arrows are bound to).\"))\n      (:nsection :title \"Downloads\"\n        (:p \"See the \" (:nxref :command 'nyxt/mode/download:list-downloads) \" command and the \"\n            (:nxref :slot 'download-path :class-name 'buffer) \" buffer slot documentation.\"))\n\n      (:nsection :title \"URL-dispatchers\"\n        (:p \"You can configure which actions to take depending on the URL to be\nloaded.  For instance, you can configure which Torrent program to start to load\nmagnet links.  See the \" (:nxref :function 'url-dispatching-handler) \" function\ndocumentation.\"))\n\n      (:nsection\n        :title \"Custom commands\"\n        :open-p nil\n        (:p \"Creating your own invocable commands is similar to creating a Common\nLisp function, except the form is \" (:code \"define-command\") \" instead of \"\n(:code \"defun\") \". If you want this command to be invocable outside of\n        the context of a mode, use \" (:code \"define-command-global\") \".\")\n        (:p \"Example:\")\n        (:ncode\n          '(define-command-global my-bookmark-url ()\n            \"Query which URL to bookmark.\"\n            (let ((url (prompt\n                        :prompt \"Bookmark URL\"\n                        :sources 'prompter:raw-source)))\n              (nyxt/mode/bookmark:persist-bookmark url))))\n        (:p \"See the \" (:nxref :class-name 'prompt-buffer) \" class documentation for how\nto write custom prompt buffers.\")\n        (:p \"You can also create your own context menu entries binding those to Lisp commands, using \"\n            (:nxref :function 'ffi-add-context-menu-command) \" function. You can bind the \"\n            (:code \"bookmark-url\") \" like this:\")\n        (:ncode '(ffi-add-context-menu-command 'my-bookmark-url \"Bookmark URL\"))\n        (:p \"Currently, context menu commands don't have access to the renderer objects (and\nshouldn't hope to). Commands you bind to context menu actions should deduce most\nof the information from their surroundings, using JavaScript and Lisp functions\nNyxt provides. For example, one can use the \"\n            (:nxref :slot 'url-at-point :class-name 'buffer)\n            \" to get thep URL currently under pointer.\")\n        (:p \"With this, one can improve the bookmarking using \"\n            (:nxref :slot 'url-at-point :class-name 'buffer) \":\")\n        (:ncode\n          '(ffi-add-context-menu-command\n            (lambda ()\n              (nyxt/mode/bookmark:persist-bookmark (url-at-point (current-buffer))))\n            \"Bookmark Link\")))\n\n      (:nsection :title \"Custom URL schemes\"\n        (:p \"Nyxt can register custom schemes that run a handler on URL load.\")\n        (:p \"The example below defines a scheme \" (:code \"hello\") \" that replies\n            accordingly when loading URLs \" (:code \"hello:world\") \" and \"\n            (:code \"hello:mars\") \".\")\n        (:ncode\n          '(define-internal-scheme \"hello\"\n            (lambda (url)\n              (if (string= (quri:uri-path (url url)) \"world\")\n                  (spinneret:with-html-string (:p \"Hello, World!\"))\n                  (spinneret:with-html-string (:p \"Please instruct me on how to greet you!\"))))))\n        (:p \"Note that scheme privileges, such as enabling the Fetch API or\nenabling CORS requests are renderer-specific.\")\n\n        (:nsection :title \"nyxt: URLs and internal pages\"\n          (:p \"You can create pages out of Lisp commands, and make arbitrary computations for\nthe content of those. More so: these pages can invoke Lisp commands on demand,\nbe it on button click or on some page event. The macros and functions to look at are:\")\n          (:ul\n           (:li (:nxref :macro 'define-internal-page) \" to create new pages.\")\n           (:li (:nxref :function 'buffer-load-internal-page-focus)\n                \" to either get or create the buffer for the page.\")\n           (:li (:nxref :function 'nyxt-url) \" to reference the internal pages by their name.\")\n           (:li (:nxref :macro 'define-internal-page-command)\n                \" to generate a mode-specific command loading the internal page.\")\n           (:li (:nxref :macro 'define-internal-page-command-global)\n                \" to generate a global command loading the internal page.\"))\n          (:p \"Using the facilities Nyxt provides, you can make a random number generator\npage:\")\n          (:ncode\n            '(define-internal-page-command-global random-number (&key (max 1000000))\n              (buffer \"*Random*\")\n              \"Generates a random number on every reload.\"\n              (spinneret:with-html-string\n                (:h1 (princ-to-string (random max)))\n                (:button.button\n                 :onclick (ps:ps (nyxt/ps:lisp-eval\n                                  (:title \"re-load/re-generate the random number\")\n                                  (ffi-buffer-reload buffer)))\n                 :title \"Re-generate the random number again\"\n                 \"New number\"))))\n          (:p \"Several things to notice here:\")\n          (:ul\n           (:li \"Internal page command is much like a regular command in being a Lisp function\nthat you can call either from the REPL or from the \" (:nxref :command 'execute-command) \" menu.\")\n           (:ul\n            (:li \"With one important restriction: internal page commands should only have keyword\narguments. Other argument types are not supported. This is to make them\ninvocable through the URL they are assigned. For example, when you invoke the \"\n                 (:code \"random-number\") \" command you've written, you'll see the \"\n                 (:code \"nyxt:nyxt-user:random-number?max=%1B1000000\")\n                 \" URL in the status buffer. The keyword argument is being seamlessly translated\ninto a URL query parameter.\")\n            (:li \"There's yet another important restriction: the values you provide to the\ninternal page command should be serializable to URLs. Which restricts the\narguments to numbers, symbols, and strings, for instance.\"))\n           (:li \"Those commands should return the content of the page in their body, like\ninternal schemes do.\")\n           (:li \"If you want to return HTML, then \" (:nxref :macro 'spinneret:with-html-string)\n                \" is your best friend, but no one restricts you from producing HTML in any other\nway, including simply writing it by hand ;)\")\n           (:li (:code \"nyxt/ps:lisp-eval\")\n                \" is a Parenscript macro to request Nyxt to run arbitrary code. The signature is: \"\n                (:code \"((&key (buffer '(nyxt:current-buffer)) title) &body body)\")\n                \". You can bind it to a \" (:code \"<button>\") \"'s \" (:code \"onClick\")\n                \" event, for example.\"))\n          (:p \"If you're making an extension, you might find other macros more useful. \"\n              (:nxref :macro 'define-internal-page-command)\n              \", for example, defines a command to only be visible when in the corresponding mode\nis enabled. Useful to separate the context-specific commands from the\nuniversally useful (\" (:code \"-global\")\n              \") ones. If there's a page that you'd rather not have a command for, you can\nstill define it as:\")\n          (:ncode\n            '(define-internal-page not-a-command ()\n              (:title \"*Hello*\" :page-mode 'base-mode)\n              \"Hello there!\"))\n          (:p \" and use as:\")\n          (:ncode\n            '(buffer-load-internal-page-focus 'not-a-command))\n          (:p \"See the slots and documentation of \" (:nxref :class-name 'internal-page)\n              \" to understand what you can pass to \"\n              (:nxref :macro 'define-internal-page) \".\")))\n\n      (:nsection :title \"Hooks\"\n        (:p \"Hooks provide a powerful mechanism to tweak the behavior of various\nevents that occur in the context of windows, buffers, modes, etc.\")\n        (:p \"A hook holds a list of \" (:i \"handlers\") \".  Handlers are named and\ntyped functions.  Each hook has a dedicated handler constructor.\")\n        (:p\n         \"Hooks can be 'run', that is, their handlers are run according to\nthe \" (:nxref :slot 'nhooks:combination :class-name 'nhooks:hook) \" slot of the hook.  This combination is a function\nof the handlers.  Depending on the combination, a hook can run the handlers\neither in parallel, or in order until one fails, or even \" (:i \"compose\")\n         \" them (pass the result of one as the input of the next).  The handler types\nspecify which input and output values are expected.\")\n        (:p \"To add or delete a hook, you only need to know a couple of functions:\"\n            (:ul\n             (:li (:nxref :class-name 'nhooks:handler) \" a class to wrap hook handlers in.\")\n             (:li (:nxref :function 'nhooks:add-hook) \" (also known as \"\n                  (:code \"hooks:add-hook\")\n                  \") allows you to add a handler to a hook,for it to be invoked when the hook fires.\")\n             (:li (:code \"nhooks:on\") \" (also available as \" (:code \"hooks:on\")\n                  \") as a shorthand for the \" (:code \"nhooks:add-hook\") \".\")\n             (:li (:nxref :function 'nhooks:remove-hook) \" (also available as \"\n                  (:code \"hooks:remove-hook\") \") that removes the handler from a certain hook.\")\n             (:li (:code \"nhooks:once-on\") \" (also available as \" (:code \"hooks:once-on\")\n                  \") as a one-shot version of \" (:code \"nhooks:on\")\n                  \" that removes the handler right after it's completed.\")))\n        (:p \"Many hooks are executed at different points in Nyxt, among others:\")\n        (:ul\n         (:li \"Global hooks, such as \" (:nxref :slot 'after-init-hook :class-name 'browser)\n              \" or \" (:nxref :slot 'after-startup-hook :class-name 'browser) \".\")\n         (:li \"Window- or buffer-related hooks.\")\n         (:ul\n          (:li (:nxref :slot 'window-make-hook :class-name 'window) \" for when a new window is created.\")\n          (:li (:nxref :slot 'window-delete-hook :class-name 'window) \" for when a window is deleted.\")\n          (:li (:nxref :slot 'window-set-buffer-hook :class-name 'window)\n               \" for when the \" (:nxref :function 'current-buffer) \" changes in the window.\")\n          (:li (:nxref :slot 'buffer-load-hook :class-name 'network-buffer)\n               \" for when there's a new page loading in the buffer.\")\n          (:li (:nxref :slot 'buffer-loaded-hook :class-name 'network-buffer)\n               \" for when this page is mostly done loading (some scripts/image/styles may not\nbe fully loaded yet, so you may need to wait a bit after it fires.)\")\n          (:li (:nxref :slot 'request-resource-hook :class-name 'network-buffer)\n               \" for when a new request happens. Allows redirecting and blocking requests, and\nis a good place to do something conditioned on the links being loaded.\")\n          (:li (:nxref :slot 'prompt-buffer-ready-hook :class-name 'prompt-buffer)\n               \" fires when the prompt buffer is ready for user input. You may need to call \"\n               (:nxref :function 'prompter:all-ready-p)\n               \" on the prompt to ensure all the sources it contains are ready too, and then\nyou can safely set new inputs and select the necessary suggestions.\"))\n         (:li \"Commands :before and :after methods.\")\n         (:ul\n          (:li \"Try, for example, \"\n               (:code \"(defmethod set-url :after (&key (default-action nil)) ...)\")\n               \" to do something after the set-url finishes executing.\"))\n         (:li \"Modes 'enable' and 'disable' methods and their :before, :after, and :around methods.\")\n         (:li \"Mode-specific hooks, like \" (:nxref :slot 'nyxt/mode/download:before-download-hook\n                                             :class-name 'nyxt/mode/download:download-mode)\n              \" and \" (:nxref :slot 'nyxt/mode/download:after-download-hook\n                        :class-name 'nyxt/mode/download:download-mode)\n              \" for \" (:nxref :class-name 'nyxt/mode/download:download) \".\"))\n        (:p \"For instance, if you want to force 'old.reddit.com' over 'www.reddit.com', you\ncan set a hook like the following in your configuration file:\")\n        (:ncode\n          '(defun old-reddit-handler (request-data)\n            (let ((url (url request-data)))\n              (setf (url request-data)\n                    (if (search \"reddit.com\" (quri:uri-host url))\n                        (progn\n                          (setf (quri:uri-host url) \"old.reddit.com\")\n                          (log:info \"Switching to old Reddit: ~s\" (render-url url))\n                          url)\n                        url)))\n            request-data)\n          '(define-configuration web-buffer\n            ((request-resource-hook\n              (hooks:add-hook %slot-default% 'old-reddit-handler)))))\n        (:p \"(See \" (:nxref :function 'url-dispatching-handler)\n            \" for a simpler way to achieve the same result.)\")\n        (:p \"Or, if you want to set multiple handlers at once,\")\n        (:ncode\n          '(define-configuration web-buffer\n            ((request-resource-hook\n              (reduce #'hooks:add-hook\n               '(old-reddit-handler auto-proxy-handler)\n               :initial-value %slot-default%)))))\n        (:p \"Some hooks like the above example expect a return value, so it's\nimportant to make sure we return \" (:nxref :class-name 'request-data) \" here.  See the\ndocumentation of the respective hooks for more details.\"))\n\n      (:nsection :title \"Password management\"\n        (:p \"Nyxt provides a uniform interface to some password managers including \"\n            (:a :href \"https://keepassxc.org/\" \"KeepassXC\")\n            \" and \" (:a :href \"https://www.passwordstore.org/\" \"Password Store\") \". \"\n            \"The supported installed password manager is automatically detected.\"\n            \"See the \" (:code \"password-interface\") \" buffer slot for customization.\")\n        (:p \"You may use the \" (:nxref :macro 'define-configuration) \" macro with\nany of the password interfaces to configure them. Please make sure to\nuse the package prefixed class name/slot designators within\nthe \" (:nxref :macro 'define-configuration) \".\")\n        (:ul\n         (:li (:nxref :command 'nyxt/mode/password:save-new-password) \": Query for name and new password to persist in the database.\")\n         (:li (:nxref :command 'nyxt/mode/password:copy-password) \": \" (command-docstring-first-sentence 'nyxt/mode/password:copy-password)))\n\n        (:nsection :title \"KeePassXC support\"\n          (:p \"The interface for KeePassXC should cover most use-cases for KeePassXC, as it\nsupports password database locking with\")\n          (:ul\n           (:li (:nxref :slot 'password:master-password :class-name 'password:keepassxc-interface) \",\")\n           (:li (:nxref :slot 'password:key-file :class-name 'password:keepassxc-interface) \",\")\n           (:li \"and \" (:nxref :slot 'password:yubikey-slot :class-name 'password:keepassxc-interface)))\n          (:p \"To configure KeePassXC interface, you might need to add something like this\nsnippet to your config:\")\n          (:ncode\n            ;; FIXME: Why does `define-configuration' not work for password\n            ;; interfaces? Something's fishy with user classes...\n            '(defmethod initialize-instance :after ((interface password:keepassxc-interface) &key &allow-other-keys)\n              \"It's obviously not recommended to set master password here,\nas your config is likely unencrypted and can reveal your password to someone\npeeking at the screen.\"\n              (setf (password:password-file interface) \"/path/to/your/passwords.kdbx\"\n               (password:key-file interface) \"/path/to/your/keyfile\"\n               (password:yubikey-slot interface) \"1:1111\"))\n            '(define-configuration nyxt/mode/password:password-mode\n              ((nyxt/mode/password:password-interface (make-instance 'password:keepassxc-interface))))\n            '(define-configuration buffer\n              ((default-modes (append (list 'nyxt/mode/password:password-mode) %slot-value%)))))))\n\n      (:nsection :title \"Appearance\"\n        (:p \"Much of the visual style can be configured by the user. You can use the\nfacilities provided by \" (:nxref :package :theme) \" and \"\n(:nxref :slot 'nyxt:theme :class-name 'nyxt:browser \"browser theme slot\")\n\". The simplest option would be to use a built-in theme:\")\n\t(:ncode\n          '(define-configuration browser\n            ((theme theme:+dark-theme+\n              :doc \"Setting dark theme.\nThe default is `theme:+light-theme+'.\"))))\n\t(:p \"There's also an option of creating a custom theme. For example, to set a theme\nto a midnight-like one, you can add this snippet\nto your configuration file:\")\n        (:ncode\n          '(define-configuration browser\n            ((theme (make-instance\n\t\t     'theme:theme\n\t\t     :background-color \"black\"\n\t\t     :action-color \"#37a8e4\"\n\t\t     :primary-color \"#808080\"\n\t\t     :secondary-color \"darkgray\")\n              :doc \"You can omit the colors you like in default theme, and they will stay as they were.\"))))\n        (:p \"This, on the next restart of Nyxt, will repaint all the interface elements into\na dark-ish theme.\")\n\t(:p \"As a more involved theme example, here's how one can redefine most of the\nsemantic colors Nyxt uses to be compliant with Solarized Light theme:\")\n\t(:ncode\n\t  '(define-configuration browser\n            ((theme (make-instance\n\t\t     'theme:theme\n\t\t     :background-color \"#eee8d5\"\n\t\t     :action-color \"#268bd2\"\n\t\t     :primary-color \"#073642\"\n\t\t     :secondary-color \"#586e75\"\n\t\t     :success-color \"#2aa198\"\n\t\t     :warning-color \"#dc322f\"\n\t\t     :highlight-color \"#d33682\")\n              :doc \"Covers all the semantic groups (`warning-color', `codeblock-color' etc.)\nNote that you can also define more nuanced colors, like `warning-color+', so\nthat the interface gets even nicer. Otherwise Nyxt generates the missing colors\nautomatically, which should be good enough... for most cases.\"))))\n        (:p \"As an alternative to the all-encompassing themes, you can alter the style of\nevery individual class controlling Nyxt interface elements. All such classes have a \"\n            (:nxref :function 'nyxt:style)\n            \" slot that you can configure with your own CSS like this:\")\n        (:ncode\n          '(define-configuration nyxt/mode/style:dark-mode\n            ((style\n              (theme:themed-css (theme *browser*)\n                `(*\n                  :background-color ,theme:background-color \"!important\"\n                  :background-image none \"!important\"\n                  :color \"red\" \"!important\")\n                `(a\n                  :background-color ,theme:background-color \"!important\"\n                  :background-image none \"!important\"\n                  :color \"#AAAAAA\" \"!important\"))))\n\t    :doc \"Notice the use of `theme:themed-css' for convenient theme color injection.\"))\n        (:p \"This snippet alters the \" (:nxref :slot 'style :class-name 'nyxt/mode/style:dark-mode)\n            \" of Nyxt dark mode to have a more theme-compliant colors, using the \"\n            (:code \"theme:themed-css\")\n            \" macro (making all the theme colors you've configured earlier available as\nvariables like \" (:code \"theme:on-primary-color\") \".)\")\n\n        (:nsection :title \"Status buffer appearance\"\n          (:p \"You can customize the layout and styling of \" (:nxref :class-name 'status-buffer)\n              \" using the methods it uses for layout. These methods are: \")\n          (:dl\n           (:dt (:nxref :function 'nyxt:format-status))\n           (:dd \"General layout of the status buffer, including the parts it consists of.\")\n           (:dt (:nxref :function 'nyxt::format-status-buttons))\n           (:dd \"The (\\\"Back\\\", \\\"Forward\\\", \\\"Reload\\\") buttons section.\")\n           (:dt (:nxref :function 'nyxt::format-status-url))\n           (:dd \"The current URL display section.\")\n           (:dt (:nxref :function 'nyxt::format-status-tabs))\n           (:dd \"Tab listing.\")\n           (:dt (:nxref :function 'nyxt::format-status-modes))\n           (:dd \"List of modes.\"))\n          (:p \"To complement the layout produced by these \" (:code \"format-*\")\n              \" functions, you might need to add more rules or replace the \"\n              (:nxref :slot 'style :class-name 'status-buffer \"style of status buffer\") \".\")))\n\n      (:nsection :title \"Scripting\"\n        (:p \"You can evaluate code from the command line with \"\n            (:code \"--eval\") \" and \" (:code \"--load\") \".  From a shell:\")\n        (:ncode\n          \"$ nyxt --no-config --eval '+version+' \\\n  --load my-lib.lisp --eval '(format t \\\"Hello ~a!~&\\\" (my-lib:my-world))'\")\n        (:p \"You can evaluate multiple --eval and --load in a row, they are\nexecuted in the order they appear.\")\n        (:p \"You can also evaluate a Lisp file from the Nyxt interface with\nthe \" (:nxref :command 'load-file) \" command.  For\nconvenience, \" (:nxref :command 'load-config-file) \" (re)loads your initialization file.\")\n        (:p \"You can even make scripts.  Here is an example foo.lisp:\")\n        (:ncode\n          \"#!/bin/sh\n#|\nexec nyxt --script \\\"$0\\\"\n|#\n\n;; Your code follows:\n\\(format t \\\"~a~&\\\" +version+)\")\n        (:p \"--eval and --load can be commanded to operate over an\nexisting instance instead of a separate instance that exits immediately.\")\n        (:p \"The \" (:nxref :slot 'remote-execution-p :class-name 'browser)\n            \" of the remote instance must be non-nil:\")\n        (:ncode\n          '(define-configuration browser\n            ((remote-execution-p t))))\n        (:p \"To let know a private instance of Nyxt to load a foo.lisp script and run its \"\n            (:code \"foo\") \" function:\")\n        (:ncode\n          \"nyxt --profile nosave --remote --load foo.lisp --eval '(foo)' --quit\")\n        (:p \"Note that \" (:code \"--quit\")\n            \" at the end of each Nyxt CLI call here. If you don't provide \" (:code \"--quit\")\n            \" when dealing with a remote instance, it will go into a REPL mode, allowing an\nimmediate communication with an instance:\")\n        (:pre (:code \"nyxt --remote\n(echo \\\"~s\\\" (+ 1 2)) ;; Shows '3' in the message buffer of remote Nyxt\")))\n\n      (:nsection :title \"Advanced configuration\"\n        (:p \"While \" (:nxref :macro 'define-configuration) \" is convenient, it is mostly\nrestricted to class slot configuration.  If you want to do anything else on\nclass instantiation, you'll have to specialize the\nlower-level \" (:nxref :function 'customize-instance)\n\" generic function.  Example:\")\n        (:ncode\n          '(defmethod customize-instance ((buffer buffer) &key)\n            (echo \"Buffer ~a created.\" buffer)))\n        (:p \"All classes with metaclass \" (:nxref :class-name 'user-class) \" call \"\n            (:nxref :function 'customize-instance) \" on instantiation,\nafter \" (:nxref :function 'initialize-instance)(:code \" :after\") \".  The primary method is reserved\nto the user, however the \" (:code \":after\") \" method is reserved to the Nyxt\ncore to finalize the instance.\"))\n\n      (:nsection :title \"Extensions\"\n        (:p \"To install an extension, copy inside the \"\n            (:nxref :variable '*extensions-directory*) \" (default to \"\n            (:code \"~/.local/share/nyxt/extensions\")\").\")\n        (:p \"Extensions are regular Common Lisp systems.\")\n        (:p \"Please find a catalog of Nyxt extensions \"\n            (:a :href (nyxt-url 'list-extensions) \"here\") \".\"))\n\n      (:nsection :title \"Blocking ads using AdBlock rules\"\n        (:p \"With WebkitGTK backend you can use \"\n            (:a :href \"https://github.com/dudik/blockit\" \"BlocKit\")\n            \" extension to block ads.\")\n        (:p \"In short, you have to install \"\n            (:a :href \"https://crates.io/crates/adblock-rust-server\" \"adblock-rust-server\")\n            \" to a directory visible in \" (:code \"PATH\")\n            \" environment variable and the shared library (\"\n            (:code \"blockit.so\") \") to \" (:code gtk-extensions-directory)\n            \". After that, follow instructions on BlocKit github page.\"))\n\n      (:nsection :title \"Troubleshooting\"\n\n        (:nsection :title \"Debugging and reporting errors\"\n          (:p \"Report bugs using \" (:nxref :command 'nyxt:report-bug) \".\"))\n\n        (:nsection :title \"Bwrap error on initialization (Ubuntu)\"\n          (:p \"If Nyxt crashes on start due to \" (:code \"bwrap\")\n              \", then disable or configure the \" (:code \"apparmor\") \" service.\"))\n\n        (:nsection :title \"Playing videos\"\n          (:p \"Nyxt delegates video support to third-party plugins.\")\n          (:p \"When using the WebKitGTK backends, GStreamer and its plugins are\nleveraged.  Depending on the video, you will need to install some of the\nfollowing packages:\")\n          (:ul\n           (:li \"gst-libav\")\n           (:li \"gst-plugins-bad\")\n           (:li \"gst-plugins-base\")\n           (:li \"gst-plugins-good\")\n           (:li \"gst-plugins-ugly\"))\n          (:p \"On Debian-based systems, you might be looking for (adapt the version numbers):\")\n          (:ul\n           (:li \"libgstreamer1.0-0\")\n           (:li \"gir1.2-gst-plugins-base-1.0\"))\n          (:p \"For systems from the Fedora family:\")\n          (:ul\n           (:li \"gstreamer1-devel\")\n           (:li \"gstreamer1-plugins-base-devel\"))\n          (:p \"After the desired plugins have been installed, clear the GStreamer cache at \"\n              (:code \"~/.cache/gstreamer-1.0\") \" and restart Nyxt.\"))\n\n        (:nsection :title \"Website crashes\"\n          (:p \"If some websites systematically crash, try to install all the required\nGStreamer plugins as mentioned in the 'Playing videos' section.\"))\n\n        (:nsection :title \"Input method support (CJK, etc.)\"\n          (:p \"Depending on your setup, you might have to set some environment variables\nor run some commands before starting Nyxt, for instance\")\n          (:ncode\n            \"GTK_IM_MODULE=xim\nXMODIFIERS=@im=ibus\nibus --daemonize --replace --xim\")\n          (:p \"You can persist this change by saving the commands in\nyour \" (:code \".xprofile\") \" or similar.\"))\n\n        (:nsection :title \"HiDPI displays\"\n          (:p \"The entire UI may need to be scaled up on HiDPI displays.\")\n          (:p \"When using the WebKitGTK renderer, export the environment\nvariable below before starting Nyxt.  Note that \" (:code \"GDK_DPI_SCALE\") \" (not\nto be confused with \" (:code \"GDK_SCALE\") \") scales text only, so tweaking it\nmay be undesirable.\")\n          (:pre (:code \"export GDK_SCALE=2\nnyxt\n\")))\n\n        (:nsection :title \"StumpWM mouse scroll\"\n          (:p \"If the mouse scroll does not work for you, see the \"\n              (:a\n               :href \"https://github.com/stumpwm/stumpwm/wiki/FAQ#my-mouse-wheel-doesnt-work-with-gtk3-applications-add-the-following-to\"\n               \"StumpWM FAQ\")\n              \" for a fix.\"))\n\n        (:nsection :title \"Blank WebKitGTK views\"\n          (:p \"When experiencing rendering issues, try to disable compositing as\nbelow: \")\n          (:ncode\n            '(setf (uiop:getenv \"WEBKIT_DISABLE_COMPOSITING_MODE\") \"1\")))\n\n        (:nsection :title \"Missing cursor icons\"\n          (:p \"If you are having issues with the cursor not changing when\nhovering over buttons or links, it might be because Nyxt can't locate your cursor theme.\nTo fix that, try adding the following to your\" (:code \".bash_profile\") \" or similar:\")\n          (:ncode\n            \"export XCURSOR_PATH=${XCURSOR_PATH}:/usr/share/icons\nexport XCURSOR_PATH=${XCURSOR_PATH}:~/.local/share/icons\"))))))\n"
  },
  {
    "path": "source/message.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt)\n\n(define-class message-buffer (input-buffer)\n  ((window\n    nil\n    :type (maybe window)\n    :documentation \"The `window' to which the message buffer is attached.\")\n   (height\n    16\n    :type integer\n    :writer nil\n    :reader height\n    :export t\n    :documentation \"The height of the message buffer in pixels.\")\n   (style\n    (theme:themed-css (theme *browser*)\n      '(:font-face :font-family \"public sans\" :font-style \"normal\" :font-weight\n        \"400\" :src \"url('nyxt-resource:PublicSans-Regular.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"italic\" :font-weight\n        \"400\" :src \"url('nyxt-resource:PublicSans-Italic.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"normal\" :font-weight\n        \"100\" :src \"url('nyxt-resource:PublicSans-Thin.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"italic\" :font-weight\n        \"100\" :src \"url('nyxt-resource:PublicSans-ThinItalic.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"normal\" :font-weight\n        \"200\" :src \"url('nyxt-resource:PublicSans-ExtraLight.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"italic\" :font-weight\n        \"200\" :src \"url('nyxt-resource:PublicSans-ExtraLightItalic.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"normal\" :font-weight\n        \"300\" :src \"url('nyxt-resource:PublicSans-Light.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"italic\" :font-weight\n        \"300\" :src \"url('nyxt-resource:PublicSans-LightItalic.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"normal\" :font-weight\n        \"500\" :src \"url('nyxt-resource:PublicSans-Medium.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"italic\" :font-weight\n        \"500\" :src \"url('nyxt-resource:PublicSans-MediumItalic.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"normal\" :font-weight\n        \"600\" :src \"url('nyxt-resource:PublicSans-SemiBold.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"italic\" :font-weight\n        \"600\" :src \"url('nyxt-resource:PublicSans-SemiBoldItalic.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"normal\" :font-weight\n        \"700\" :src \"url('nyxt-resource:PublicSans-Bold.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"italic\" :font-weight\n        \"700\" :src \"url('nyxt-resource:PublicSans-BoldItalic.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"normal\" :font-weight\n        \"800\" :src \"url('nyxt-resource:PublicSans-ExtraBold.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"italic\" :font-weight\n        \"800\" :src \"url('nyxt-resource:PublicSans-ExtraBoldItalic.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"normal\" :font-weight\n        \"900\" :src \"url('nyxt-resource:PublicSans-Black.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"italic\" :font-weight\n        \"900\" :src \"url('nyxt-resource:PublicSans-BlackItalic.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"dejavu sans mono\" :src\n        \"url('nyxt-resource:DejaVuSansMono.ttf')\" \"format('ttf')\")\n      `(body\n        :background-color ,theme:background-color-\n        :color ,theme:on-background-color\n        :font-family ,theme:font-family\n        :font-size \"75vh\"\n        :line-height \"100vh\"\n        :padding 0\n        :padding-left \"4px\"\n        :margin 0))))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:export-predicate-name-p t)\n  (:metaclass user-class))\n\n(defmethod initialize-instance :after ((message-buffer message-buffer)\n                                       &key &allow-other-keys)\n  (ffi-print-message message-buffer \"Ready.\"))\n\n(defmethod (setf height) (value (message-buffer message-buffer))\n  (setf (ffi-height message-buffer) value)\n  (setf (slot-value message-buffer 'height) value))\n\n(defclass messages-appender (log4cl-impl:appender) ())\n\n(defmethod log4cl-impl:appender-do-append ((appender messages-appender)\n                                           logger level log-func)\n  (when (<= level (if (getf *options* :verbose)\n                      log4cl:+log-level-warn+\n                      log4cl:+log-level-error+))\n    (uiop:print-backtrace))\n  (when *browser*\n    (push\n     ;; TODO: Include time in *Messages* entries.\n     ;; (make-instance 'log4cl:pattern-layout :conversion-pattern \"<%p>\n     ;; [%D{%H:%M:%S}] %m%n\" )\n     (with-output-to-string (s)\n       (log4cl-impl:layout-to-stream\n        (slot-value appender 'log4cl-impl:layout) s logger level log-func))\n     (slot-value *browser* 'messages-content))))\n\n(defmacro %echo (text &key (logger 'log:info))\n  \"Echo TEXT in the message buffer.\nLOGGER is the log4cl logger to user, for instance `log:warn'.\"\n  (alex:with-gensyms (expanded-text)\n    `(progn\n       (let ((,expanded-text ,text))\n         (unless (str:emptyp ,expanded-text)\n           (,logger \"~a\" ,expanded-text))\n         ;; Allow empty strings to clear message buffer.\n         (print-message ,expanded-text)))))\n\n(export-always 'echo)\n(defun echo (&rest args)\n  \"Echo ARGS in the message view.\nThe first argument can be a format string and the following arguments will be\ninterpreted by `format'.\nUntrusted content should be given as argument with a format string.\"\n  (handler-case\n      (let ((text (apply #'format nil args)))\n        (%echo text))\n    (error (c)\n      (log:warn \"Warning while echoing: ~a\" c))))\n\n(export-always 'echo-warning)\n(defun echo-warning (&rest args)\n  \"Like `echo' but prefix with \\\"Warning\\\" and output to the standard error.\"\n  (handler-case\n      (let ((text (apply #'format nil args)))\n        (%echo (format nil \"Warning: ~a\" text)\n               :logger log:warn))\n    (error (c)\n      (log:warn \"Warning while echoing: ~a\" c))))\n\n(export-always 'echo-dismiss)\n(defmethod echo-dismiss ()\n  \"Clean the message buffer from the previous `echo'/`echo-warning' message.\"\n  (%echo \"\"))\n"
  },
  {
    "path": "source/mode/annotate.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/mode/annotate\n  (:documentation \"Package for `annotate-mode', mode to annotate documents.\n\nThe most important piece of functionality is the `annotation' class and its\nsubclasses: `url-annotation' and `snippet-annotation'.\n\nSee the `annotate-mode' for the external user-facing APIs.\"))\n(in-package :nyxt/mode/annotate)\n\n(define-mode annotate-mode ()\n  \"Annotate document with arbitrary comments.\nAnnotations are persisted to disk, see the `annotations-file' mode slot.\n\nSee `nyxt/mode/annotate' package documentation for implementation details and\ninternal programming APIs.\"\n  ((visible-in-status-p nil)\n   (annotations-file\n    (make-instance 'annotations-file)\n    :type annotations-file\n    :documentation \"File where annotations are saved.\")))\n\n(define-configuration context-buffer\n  ((default-modes (cons 'annotate-mode %slot-value%))))\n\n(defmethod annotations-file ((buffer buffer))\n  (annotations-file (find-submode 'annotate-mode buffer)))\n\n(define-class annotations-file (files:data-file nyxt-lisp-file)\n  ((files:base-path #p\"annotations\")\n   (files:name \"annotations\"))\n  (:export-class-name-p t))\n\n(define-class annotation ()\n  ((data\n    \"\"\n    :export nil\n    :documentation \"The annotation data.\")\n   (tags\n    '()\n    :type (list-of string))\n   (date (time:now)))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:documentation \"An umbrella annotation type.\nShould not be instantiated on its own. Instead, use `url-annotation' and\n`snippet-annotation'.\"))\n\n(define-class url-annotation (annotation)\n  ((url nil)\n   (page-title \"\"))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:documentation \"Annotation for a page with a certain URL.\nCommand to create one is `annotate-current-url'.\"))\n\n(define-class snippet-annotation (url-annotation)\n  ((snippet nil :documentation \"The snippet of text being annotated.\"))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:documentation \"Annotation in relation to a text on a certain page.\nThe page is handled by underlying `url-annotation', while the snippet is\nextracted by `annotate-highlighted-text' command.\"))\n\n(defmethod render ((annotation url-annotation))\n  (spinneret:with-html\n    (:dl\n     (:dt \"URL\")\n     (:dd (:a :href (url annotation) (render-url (url annotation))))\n     (:dt \"Title\")\n     (:dd (page-title annotation))\n     (:dt \"Tags\")\n     (:dd (:pre (format nil \"~{~a ~}\" (tags annotation))))\n     (:dt \"Text\")\n     (:dd (data annotation)))))\n\n(defmethod render ((annotation snippet-annotation))\n  (spinneret:with-html\n    (:dl\n     (:dt \"URL\")\n     (:dd (:a :href (url annotation) (render-url (url annotation))))\n     (:dt \"Title\")\n     (:dd (page-title annotation))\n     (:dt \"Snippet\")\n     (:dd (snippet annotation))\n     (:dt \"Tags\")\n     (:dd (:pre (format nil \"~{~a ~}\" (tags annotation))))\n     (:dt \"Text\")\n     (:dd (data annotation)))))\n\n(defun annotation-add (annotation)\n  (files:with-file-content (annotations (annotations-file (current-buffer)))\n    (push annotation annotations)))\n\n(defun annotations ()\n  (files:content (annotations-file (current-buffer))))\n\n(define-command annotate-current-url\n    (&key (buffer (current-buffer))\n     (data (prompt1 :prompt \"Annotation\"\n                    :sources (make-instance 'prompter:raw-source\n                                            :name \"Note\")))\n     (tags (prompt\n            :prompt \"Tag(s)\"\n            :sources (list (make-instance 'prompter:word-source\n                                          :name \"New tags\"\n                                          :enable-marks-p t)\n                           (make-instance 'keyword-source :buffer buffer)\n                           (make-instance 'annotation-tag-source)))))\n  \"Create an annotation of the URL of BUFFER.\n\nDATA and TAGS are passed as arguments to `url-annotation' make-instance.\"\n  (annotation-add (make-instance 'url-annotation\n                                 :url (url buffer)\n                                 :data data\n                                 :page-title (title buffer)\n                                 :tags tags)))\n\n(define-command annotate-highlighted-text\n    (&key (buffer (current-buffer))\n     (snippet (ffi-buffer-copy buffer))\n     (data (prompt1 :prompt \"Annotation\"\n                    :sources (make-instance 'prompter:raw-source\n                                            :name \"Note\")))\n     (tags (prompt\n            :prompt \"Tag(s)\"\n            :sources (list (make-instance 'prompter:word-source\n                                          :name \"New tags\"\n                                          :enable-marks-p t)\n                           (make-instance 'keyword-source :buffer buffer)\n                           (make-instance 'annotation-tag-source)))))\n  \"Create an annotation for the highlighted text of BUFFER.\n\nDATA, SNIPPET, and TAGS are passed as arguments to `snippet-annotation'\nmake-instance.\"\n  (annotation-add (make-instance 'snippet-annotation\n                                 :snippet snippet\n                                 :url (url buffer)\n                                 :page-title (title buffer)\n                                 :data data\n                                 :tags tags)))\n\n(defun render-annotations (annotations)\n  \"Show the ANNOTATIONS in a new buffer\"\n  (spinneret:with-html-string\n    (:h1 \"Annotations\")\n    (or\n     (loop for annotation in annotations\n           collect (:div (render annotation)\n                         (:hr)))\n     (:p \"No annotations available/selected.\"))))\n\n(define-internal-page show-annotations-for-current-url\n    (&key (id (id (current-buffer))))\n    (:title \"*Annotations*\")\n  \"Display the annotations associated to buffer with ID.\"\n  (let ((buffer (nyxt::buffer-get id)))\n    (render-annotations (sera:filter (curry #'url-equal (url buffer))\n                                     (files:content (annotations-file buffer))\n                                     :key (compose #'quri:uri #'url)))))\n\n(define-command-global show-annotations-for-current-url\n    (&key (buffer (current-buffer)))\n  \"Create a new buffer with the annotations of the current URL of BUFFER.\"\n  (buffer-load-internal-page-focus 'show-annotations-for-current-url\n                                   :id (id buffer)))\n\n(define-class annotation-source (prompter:source)\n  ((prompter:name \"Annotations\")\n   (prompter:constructor (files:content (annotations-file (current-buffer))))\n   (prompter:filter-preprocessor #'prompter:filter-exact-matches)\n   (prompter:enable-marks-p t)))\n\n(defmethod prompter:object-attributes ((annotation annotation)\n                                       (source prompter:source))\n  (declare (ignore source))\n  `((\"Data\" ,(data annotation) (:width 3))\n    (\"Tags\" ,(tags annotation) (:width 3))))\n\n(define-class annotation-tag-source (prompter:source)\n  ((prompter:name \"Tags\")\n   (prompter:filter-preprocessor\n    (lambda (initial-suggestions-copy source input)\n      (prompter:delete-inexact-matches\n       initial-suggestions-copy\n       source\n       (last-word input))))\n   (prompter:filter\n    (lambda (suggestion source input)\n      (prompter:fuzzy-match suggestion source (last-word input))))\n   (prompter:enable-marks-p t)\n   (prompter:constructor\n    (let ((annotations (files:content (annotations-file (current-buffer)))))\n      (sort (remove-duplicates\n             (mappend #'tags annotations)\n             :test #'string-equal)\n            #'string-lessp)))))\n\n(define-internal-page-command-global show-annotation ()\n    (buffer \"*Annotations*\")\n  \"Show prompted annotations.\"\n  (handler-case (render-annotations\n                 (prompt :prompt \"Show annotation(s)\"\n                         :sources (make-instance 'annotation-source)))\n    (nyxt:prompt-buffer-canceled () (render-annotations nil))))\n\n(define-internal-page-command-global show-annotations ()\n    (buffer \"*Annotations*\")\n  \"Show all annotations\"\n  (render-annotations (files:content (annotations-file buffer))))\n"
  },
  {
    "path": "source/mode/autofill.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/mode/autofill\n  (:documentation \"Mode to fill forms more rapidly.\n\nThe whole API is centered around `autofill' class and its slots:\n- `name' as the descriptive name of the autofill. Accessed with `autofill-name'.\n- and `fill' as the actual content-designating thing (string or a\n  function). Accessed with `autofill-fill'.\n\nThere's a shortcut function to create `autofill's: `make-autofill'.\n\n`autofill' is funcallable. When funcallable, returns the string produced by the\nautofill.\n\nThen, the command eponymously called `autofill' actually fills (with\n`ffi-buffer-paste') the contents into the page.\n\nSee the `autofill-mode' for the external user-facing APIs.\"))\n(in-package :nyxt/mode/autofill)\n\n(export-always 'make-autofill)\n(defun make-autofill (&rest args)\n  \"Shortcut to create `autofill's: ARGS are keyword initargs.\"\n  (apply #'make-instance 'autofill args))\n\n(define-mode autofill-mode ()\n  \"Mode to fill forms more rapidly.\n\nSee `nyxt/mode/autofill' package documentation for implementation details and\ninternal programming APIs.\"\n  ((visible-in-status-p nil)\n   (autofills\n    (list (make-autofill :name \"Name\" :fill \"My Name\")\n          (make-autofill :name \"Hello Printer\"\n                         :fill (lambda () (format nil \"hello!\"))))\n    :documentation \"To autofill run the command `autofill'.\nUse this slot to customize the autofill values available.\n\nThe fill can be a string value or a function.  The latter allows you to provide\ncontent dynamic to the context.\")\n   (keyscheme-map\n    (define-keyscheme-map \"autofill-mode\" ()\n      keyscheme:default\n      (list\n       \"C-i\" 'autofill)))))\n\n(define-configuration document-buffer\n  ((default-modes (cons 'autofill-mode %slot-value%))))\n\n(define-class autofill ()\n  ((name\n    \"\"\n    :type string\n    :accessor autofill-name\n    :documentation \"Displayable name of the autofill.\nIs especially useful for function autofills as `autofill-fill' doesn't tell\nanything meaningful for these.\")\n   (fill\n    \"\"\n    :type (or string function)\n    :accessor autofill-fill\n    :documentation \"The text that autofill will paste.\nCan be:\n- a string that will be pasted as is, or\n- a zero-argument function that will generate the text to paste.\n\nPlease note that this accessor cannot be renamed to `fill' because\nit will be in conflict with common-lisp:fill.\"))\n  (:metaclass closer-mop:funcallable-standard-class)\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:export-predicate-name-p t))\n\n(defmethod initialize-instance :after ((autofill autofill)\n                                       &key &allow-other-keys)\n  (closer-mop:set-funcallable-instance-function\n   autofill (typecase (autofill-fill autofill)\n              (string (lambda () (autofill-fill autofill)))\n              (function (autofill-fill autofill)))))\n\n(define-class autofill-source (prompter:source)\n  ((prompter:name \"Autofills\")\n   (prompter:constructor (autofills (find-submode 'autofill-mode)))\n   (prompter:actions-on-return\n    (lambda-command autofill* (autofills)\n      (ffi-buffer-paste (current-buffer)\n                        (funcall (first autofills))))))\n  (:export-class-name-p t)\n  (:metaclass user-class)\n  (:documentation \"A source listing all the available `autofill's\nin the current `autofill-mode'.\"))\n\n(defmethod prompter:object-attributes ((autofill autofill)\n                                       (source prompter:source))\n  (declare (ignore source))\n  `((\"Name\" ,(autofill-name autofill) (:width 1))\n    (\"Fill\" ,(let ((f (autofill-fill autofill)))\n               (typecase f\n                 (string (write-to-string f))\n                 (t \"function\")))\n            (:width 3))))\n\n(define-command autofill ()\n  \"Fill in a field with a value from a saved list.\"\n  (prompt :prompt \"Autofill\"\n          :sources 'autofill-source))\n"
  },
  {
    "path": "source/mode/base.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt)\n\n(define-mode base-mode ()\n  \"Bind general-purpose commands defined by `define-command'.\nThis mode is a good candidate to be passed to `make-buffer'.\"\n  ((visible-in-status-p nil)\n   (keyscheme-map\n    (define-keyscheme-map \"base-mode\" ()\n      keyscheme:default\n      (list\n       \"C-l\" 'set-url\n       \"M-l\" 'set-url-new-buffer\n       \"C-r\" 'reload-current-buffer\n       \"M-r\" 'reload-buffers\n       \"C-shift-tab\" 'switch-buffer-previous\n       \"C-tab\" 'switch-buffer-next\n       \"C-T\" 'reopen-buffer\n       \"C-t\" 'make-buffer-focus\n       \"M-o\" 'toggle-prompt-buffer-focus\n       \"f1 r\" 'manual\n       \"f1 t\" 'tutorial\n       \"f1 a\" 'describe-any\n       \"f1 b\" 'describe-bindings\n       \"f1 C\" 'describe-class\n       \"f1 c\" 'describe-command\n       \"f1 f\" 'describe-function\n       \"f1 k\" 'describe-key\n       \"f1 p\" 'describe-package\n       \"f1 s\" 'describe-slot\n       \"f1 v\" 'describe-variable\n       \"f11\" 'toggle-fullscreen\n       \"f10\" 'toggle-toolbars\n       \"C-Y\" 'nyxt/mode/download:list-downloads\n       \"C-space\" 'execute-command)\n      keyscheme:cua\n      (list\n       \"f5\" 'reload-current-buffer\n       \"C-/\" 'reopen-buffer\n       \"C-[\" 'switch-buffer-previous\n       \"C-]\" 'switch-buffer-next\n       \"C-pagedown\" 'switch-buffer-next\n       \"M-down\" 'switch-buffer\n       \"C-`\" 'switch-buffer-last\n       \"C-pageup\" 'switch-buffer-previous\n       \"C-w\" 'delete-current-buffer\n       \"C-n\" 'make-window\n       \"C-W\" 'delete-current-window\n       \"M-w\" 'delete-window\n       \"M-c l\" 'copy-url\n       \"M-c t\" 'copy-title\n       \"C-O\" 'load-file\n       \"C-o\" 'nyxt/mode/file-manager:open-file\n       \"C-q\" 'quit)\n      keyscheme:emacs\n      (list\n       \"C-x left\" 'switch-buffer-previous\n       \"C-x C-left\" 'switch-buffer-previous\n       \"C-x right\" 'switch-buffer-next\n       \"C-x C-right\" 'switch-buffer-next\n       \"M-p\" 'switch-buffer-previous\n       \"M-n\" 'switch-buffer-next\n       \"C-x o\" 'toggle-prompt-buffer-focus\n       \"C-x b\" 'switch-buffer\n       \"C-x k\" 'delete-buffer\n       \"C-x C-k\" 'delete-current-buffer\n       \"C-x C-b\" 'nyxt/mode/buffer-listing::list-buffers\n       \"C-M-l\" 'copy-url\n       \"C-M-t\" 'copy-title\n       \"C-h t\" 'tutorial\n       \"C-h r\" 'manual\n       \"C-h a\" 'describe-any\n       \"C-h b\" 'describe-bindings\n       \"C-h C\" 'describe-class\n       \"C-h c\" 'describe-command\n       \"C-h f\" 'describe-function\n       \"C-h k\" 'describe-key\n       \"C-h p\" 'describe-package\n       \"C-h s\" 'describe-slot\n       \"C-h v\" 'describe-variable\n       \"C-d\" 'nyxt/mode/download:list-downloads\n       \"C-x 5 2\" 'make-window\n       \"C-x 5 0\" 'delete-current-window\n       \"C-x 5 1\" 'delete-window\n       \"C-x C-f\" 'nyxt/mode/file-manager:open-file\n       \"M-x\" 'execute-command\n       \"M-1\" (read-from-string \"nyxt/mode/repeat:repeat-key\")\n       \"M-2\" (read-from-string \"nyxt/mode/repeat:repeat-key\")\n       \"M-3\" (read-from-string \"nyxt/mode/repeat:repeat-key\")\n       \"M-4\" (read-from-string \"nyxt/mode/repeat:repeat-key\")\n       \"M-5\" (read-from-string \"nyxt/mode/repeat:repeat-key\")\n       \"M-6\" (read-from-string \"nyxt/mode/repeat:repeat-key\")\n       \"M-7\" (read-from-string \"nyxt/mode/repeat:repeat-key\")\n       \"M-8\" (read-from-string \"nyxt/mode/repeat:repeat-key\")\n       \"M-9\" (read-from-string \"nyxt/mode/repeat:repeat-key\")\n       \"C-x C-c\" 'quit)\n      keyscheme:vi-normal\n      (list\n       \"o\" 'set-url\n       \"O\" 'set-url-new-buffer\n       \"R\" 'reload-current-buffer\n       \"r\" 'reload-buffers\n       \"u\" 'reopen-buffer\n       \"g b\" 'switch-buffer\n       \"[\" 'switch-buffer-previous\n       \"]\" 'switch-buffer-next\n       \"d\" 'delete-buffer\n       \"D\" 'delete-current-buffer\n       \"B\" 'make-buffer-focus\n       \"W\" 'make-window\n       \"C-w C-w\" 'make-window\n       \"C-w q\" 'delete-current-window\n       \"C-w C-q\" 'delete-window\n       \"y u\" 'copy-url\n       \"y t\" 'copy-title\n       \":\" 'execute-command\n       \"1\" (read-from-string \"nyxt/mode/repeat:repeat-key\")\n       \"2\" (read-from-string \"nyxt/mode/repeat:repeat-key\")\n       \"3\" (read-from-string \"nyxt/mode/repeat:repeat-key\")\n       \"4\" (read-from-string \"nyxt/mode/repeat:repeat-key\")\n       \"5\" (read-from-string \"nyxt/mode/repeat:repeat-key\")\n       \"6\" (read-from-string \"nyxt/mode/repeat:repeat-key\")\n       \"7\" (read-from-string \"nyxt/mode/repeat:repeat-key\")\n       \"8\" (read-from-string \"nyxt/mode/repeat:repeat-key\")\n       \"9\" (read-from-string \"nyxt/mode/repeat:repeat-key\")\n       \"Z Z\" 'quit))))\n  (:toggler-command-p nil))\n"
  },
  {
    "path": "source/mode/blocker.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/mode/blocker\n  (:documentation \"Package for `blocker-mode', mode to block requests for hosts.\n`blocker-mode' relies on:\n- `hostlist' as the hostlist representation.\n- `*default-hostlist*' as the most reliable hostlist.\n- `load-hostlists' as the function forcing hostlist update and the user-space\n  `update-hostlists' commands relying on it.\n\n- `request-resource-block' as the hook handler that does all the automagic\n  blocking.\"))\n(in-package :nyxt/mode/blocker)\n\n;; TODO: Add convenient interface to block hosts depending on the current URL.\n\n(define-class hostlist (files:data-file nyxt-remote-file)\n  ((hosts\n    '()\n    :type (or (cons string *) null)\n    :documentation \"List of hosts to ignore.\nThis is useful to reference hosts manually instead of via `nfiles:url'.\")\n   (files:update-interval\n    #.(* 60 60 24)\n    :type unsigned-byte\n    :documentation \"If URL is provided, update the list after this amount of\nseconds.\"))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:documentation \"A hostlist `blocker-mode' can use for its `hostlists' slot.\nSee `*default-hostlist*' for an example.\"))\n\n(defmethod hosts :around ((hostlist hostlist))\n  (or (call-next-method)\n      (let ((path (files:expand hostlist)))\n        (unless (uiop:file-exists-p path)\n          (echo \"Updating hostlist ~s...\" path))\n        (setf (slot-value hostlist 'hosts)\n              (files:content hostlist\n                             :force-update (not (uiop:file-exists-p path)))))))\n\n(export-always 'make-hostlist)\n(defun make-hostlist (&rest args)\n  \"Return a new `hostlist'.\nSee the `hostlist' class documentation.\"\n  (apply #'make-instance 'hostlist args))\n\n\n(export-always '*default-hostlist*)\n(defparameter *default-hostlist*\n  (make-instance\n   'hostlist\n   :url (quri:uri \"https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts\")\n   :base-path #p\"hostlist-stevenblack.txt\")\n  \"Default hostlist for `blocker-mode'.\")\n\n(define-mode blocker-mode ()\n  \"Enable blocking of listed hosts.\nTo customize the list of blocked hosts, set the `hostlists' slot.\nTo force hostlist update, use `update-hostlists'.\n\nExample:\n\n\\(defvar *my-blocked-hosts*\n  (nyxt/mode/blocker:make-hostlist\n   :hosts '(\\\"platform.twitter.com\\\"\n            \\\"syndication.twitter.com\\\"\n            \\\"m.media-amazon.com\\\")))\n\n\\(define-mode my-blocker-mode (nyxt/mode/blocker:blocker-mode)\n  \\\"Blocker mode with custom hosts from `*my-blocked-hosts*'.\\\"\n  ((nyxt/mode/blocker:hostlists\n     (list *my-blocked-hosts* nyxt/mode/blocker:*default-hostlist*))))\n\n\\(define-configuration :buffer\n  ((default-modes (append '(my-blocker-mode) %slot-default%))))\n\nSee `nyxt/mode/blocker' package documentation for implementation details and\ninternal programming APIs.\"\n  ((hostlists (list *default-hostlist*))\n   (blocked-hosts\n    (make-hash-table :test 'equal)\n    :export nil\n    :documentation \"The set of host names to block.\")))\n\n(defmethod blocked-hosts :around ((blocker-mode blocker-mode))\n  (let ((value (call-next-method)))\n    (unless (plusp (hash-table-count value))\n      (dolist (hostlist (hostlists blocker-mode))\n        ;; TODO: Allow running in the background, but warning, it could leak\n        ;; personal information to undesired third-party.\n        (dolist (host (hosts hostlist))\n          (setf (gethash host value) host))))\n    value))\n\n(defmethod enable ((mode blocker-mode) &key)\n  (when (network-buffer-p (buffer mode))\n    (hooks:add-hook (request-resource-hook (buffer mode))\n                    'request-resource-block)))\n\n(defmethod disable ((mode blocker-mode) &key)\n  (when (network-buffer-p (buffer mode))\n    (hooks:remove-hook (request-resource-hook (buffer mode))\n                       'request-resource-block)))\n\n(defmethod\n files:write-file ((profile nyxt-profile) (hostlist hostlist) &key destination)\n  \"Write the downloaded hostlist to disk.\nThis is the raw downloaded content and not the serialized parsed content.\nThis gives more integrity guarantees to the user and allows external manipulation.\"\n  (unless (uiop:emptyp (files:url-content hostlist))\n    (alex:write-string-into-file (files:url-content hostlist) destination\n                                 :if-exists :supersede)))\n\n(defmethod\n files:deserialize ((profile nyxt-profile) (hostlist hostlist) raw-content &key)\n  (flet ((empty-line? (line)\n           (< (length line) 2))\n         (comment? (line)\n           (string= line \"#\" :end1 1))\n         (custom-hosts? (line)\n           (not (str:starts-with? \"0.0.0.0\" line))))\n    (loop as line = (read-line raw-content nil)\n          while line\n          unless (or (empty-line? line)\n                     (comment? line)\n                     (custom-hosts? line))\n            collect (second (str:split \" \" line)))))\n\n(defmethod blocklisted-host-p ((mode blocker-mode) host)\n  \"Return non-nil of HOST if found in the hostlists of MODE.\nReturn nil if MODE's hostlist cannot be parsed.\"\n  (gethash host (blocked-hosts mode)))\n\n(defun request-resource-block (request-data)\n  \"Block resource queries from blocklisted hosts.\nThis is an acceptable handler for `request-resource-hook'.\"\n  (let ((mode (find-submode 'blocker-mode (buffer request-data))))\n    (if (and mode\n             (blocklisted-host-p\n              mode\n              (quri:uri-host (url request-data))))\n        (progn\n          (log:debug \"Dropping ~a for ~a (~a)\"\n                     (render-url (url request-data))\n                     (buffer request-data)\n                     (render-url (url (buffer request-data))))\n          nil)\n        ;; Pass request to the other handlers.\n        request-data)))\n\n(defmethod s-serialization:serializable-slots ((object blocker-mode))\n  \"Discard hostlists which can get pretty big.\"\n  (delete 'nyxt/mode/blocker::hostlists\n          (mapcar #'closer-mop:slot-definition-name\n                  (closer-mop:class-slots (class-of object)))))\n\n(define-command\n update-hostlists\n (&optional (blocker-mode\n             (find-submode 'nyxt/mode/blocker:blocker-mode (current-buffer))))\n  \"Forces update for all the hostlists of `blocker-mode'.\"\n  (clrhash (blocked-hosts blocker-mode)))\n"
  },
  {
    "path": "source/mode/bookmark.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/mode/bookmark\n  (:documentation \"Package for `bookmark-mode', mode to manage bookmarks.\nThe main object is `bookmark-entry'. The main function to add a bookmark is\n`persist-bookmark'.\n\nSee the `bookmark-mode' for the external user-facing APIs.\"))\n(in-package :nyxt/mode/bookmark)\n\n;;; We don't use CL-prevalence to serialize / deserialize bookmarks for a couple for reasons:\n;;; - It's too verbose, e.g. a list is\n;;; (:SEQUENCE 3 :CLASS CL:LIST :SIZE 2 :ELEMENTS ( \"bar\" \"baz\" ) )\n;;;\n;;; - We lack control on the line breaks.\n;;;\n;;; - It needs IDs for every object, which makes it hard for the user to\n;;;   hand-edit the file without breaking it.\n;;;\n;;; - Un-explicitly-set class slots are exported if they have an initform;\n;;;   removing the initform forces us to put lots of (slot-boundp ...).\n\n(export-always 'bookmark-mode)\n(define-mode bookmark-mode ()\n  \"Manage bookmarks.\nBookmarks can be persisted to disk, see the `bookmarks-file' mode slot.\n\nSee `nyxt/mode/bookmark' package documentation for implementation details and\ninternal programming APIs.\"\n  ((visible-in-status-p nil)\n   (bookmarks-file\n    (make-instance 'bookmarks-file)\n    :type bookmarks-file\n    :documentation \"File where bookmarks are saved.\")\n   (keyscheme-map\n    (define-keyscheme-map \"bookmarks-mode\" ()\n      keyscheme:default\n      (list\n       \"C-b\" 'list-bookmarks\n       \"C-m g\" 'bookmark-hint)\n      keyscheme:cua\n      (list\n       \"C-m o\" 'set-url-from-bookmark\n       \"C-m s\" 'add-bookmark\n       \"C-d\" 'add-bookmark\n       \"C-m k\" 'delete-bookmark)\n      keyscheme:emacs\n      (list\n       \"C-x r j\" 'set-url-from-bookmark\n       \"C-x r m\" 'add-bookmark\n       \"C-x r l\" 'add-bookmark\n       \"C-x r k\" 'delete-bookmark)\n      keyscheme:vi-normal\n      (list\n       \"m l\" 'list-bookmarks\n       \"m f\" 'bookmark-hint\n       \"m o\" 'set-url-from-bookmark\n       \"m M\" 'add-bookmark\n       \"m m\" 'add-bookmark\n       \"m d\" 'delete-bookmark)))\n   (style (theme:themed-css (theme *browser*)\n            '(\"dl\"\n              :margin-left \"8px\")\n            ;; Taken from buffer.lisp to save space for big bookmark lists.\n            `(button\n              :color ,theme:on-secondary-color\n              :display \"inline-block\"\n              :text-decoration \"none\"\n              :margin-right \"8px\")))))\n\n(define-configuration context-buffer\n  ((default-modes (cons 'bookmark-mode %slot-value%))))\n\n(defmethod bookmarks-file ((buffer buffer))\n  (bookmarks-file (find-submode 'bookmark-mode buffer)))\n\n(defun group-bookmarks (buffer)\n  (let ((bookmarks-table (make-hash-table :test #'equalp))\n        (bookmarks (files:content (bookmarks-file buffer))))\n    (dolist (bookmark bookmarks)\n      (let ((tags (tags bookmark)))\n        (if tags\n            (dolist (tag tags)\n              (push bookmark (gethash tag bookmarks-table nil)))\n            (push bookmark (gethash tags bookmarks-table nil)))))\n    bookmarks-table))\n\n(define-class bookmarks-file (files:data-file nyxt-lisp-file)\n  ((files:base-path #p\"bookmarks\")\n   (files:name \"bookmarks\"))\n  (:export-class-name-p t))\n\n(define-class bookmark-entry ()\n  ((url (quri:uri \"\"))\n   (title \"\")\n   (annotation \"\")\n   (date (time:now))\n   (tags\n    '()\n    :type (list-of string)))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:documentation \"Represents Nyxt bookmark.\n`url' is the identity of the `bookmark-entry', used in `equals'.\n`title', `annotation', `date', `tags' are useful pieces of metadata.\"))\n\n(defmethod prompter:object-attributes ((entry bookmark-entry) (source prompter:source))\n  (declare (ignore source))\n  `((\"Title\" ,(title entry) (:width 3))\n    (\"URL\" ,(render-url (url entry)) (:width 2))\n    (\"Tags\" ,(format nil \"~{~a ~}\" (tags entry)) (:width 1))\n    (\"Date\" ,(local-time:format-timestring nil\n                                           (date entry)\n                                           :format local-time:+asctime-format+)\n            (:width 1))))\n\n(export-always 'equals)\n(defmethod equals ((e1 bookmark-entry) (e2 bookmark-entry))\n  \"Entries are equal if the hosts and the paths are equal.\nIn particular, we ignore the protocol (e.g. HTTP or HTTPS does not matter).\"\n  (url-equal (url e1) (url e2)))\n\n(-> persist-bookmark\n    (quri:uri &key (:title string) (:date (or time:timestamp null)) (:tags t))\n    t)\n(export-always 'persist-bookmark)\n(defun persist-bookmark (url &key date title tags)\n  \"Store the bookmark for URL in `bookmarks-file' of the current buffer.\nCreates a `bookmark-entry' with DATE, TITLE, and TAGS, when provided.\nIf there's a bookmarks with the same URL, update the TITLE, TAGS, and DATE\ninstead.\"\n  (files:with-file-content (bookmarks (bookmarks-file (current-buffer)))\n    (unless (or (url-empty-p url)\n                (string= \"about:blank\" (render-url url)))\n      (multiple-value-bind (entries bookmarks-without-url)\n          (sera:partition (sera:partial #'url-equal url) bookmarks :key #'url)\n        (let ((entry (if entries\n                         (first entries)\n                         (make-instance 'bookmark-entry\n                                        :url url))))\n          (unless (str:emptyp title)\n            (setf (title entry) title))\n          (setf (tags entry)\n                (sort (delete-duplicates\n                       (remove \"\" tags :test #'string=)\n                       :test #'string=)\n                      #'string<))\n          (when date\n            (setf (date entry) date))\n          (push entry bookmarks-without-url)\n          (setf bookmarks bookmarks-without-url)))\n      (echo \"Saved bookmark ~a\" url))))\n\n(define-class bookmark-source (prompter:source)\n  ((prompter:name \"Bookmarks\")\n   (prompter:constructor (files:content (bookmarks-file (current-buffer))))\n   (prompter:enable-marks-p t)\n   (prompter:active-attributes-keys\n    '(\"URL\" \"Title\" \"Tags\")\n    :accessor nil))\n  (:export-class-name-p t)\n  (:documentation \"Source for bookmark search.\nBy default, matches URL, title, and tags of the bookmark, but can also match\nagainst date, given `prompter:active-attributes-keys' configuration.\"))\n\n(defmethod url-sources ((mode bookmark-mode) actions-on-return)\n  (make-instance 'bookmark-source :actions-on-return actions-on-return))\n\n(defun tag-suggestions ()\n  (let ((bookmarks (files:content (bookmarks-file (current-buffer)))))\n    ;; Warning: `sort' is destructive and `append' does not copy the last list,\n    ;; so if we used `delete-duplicates' here it would have modified the last\n    ;; list.\n    (sort (remove-duplicates (mappend #'tags bookmarks)\n                             :test #'string-equal)\n          #'string-lessp)))\n\n(define-class tag-source (prompter:source)\n  ((prompter:name \"Tags\")\n   (prompter:filter-preprocessor\n    (lambda (initial-suggestions-copy source input)\n      (prompter:delete-inexact-matches\n       initial-suggestions-copy\n       source\n       (last-word input))))\n   (prompter:filter\n    (lambda (suggestion source input)\n      (prompter:fuzzy-match suggestion source (last-word input))))\n   (prompter:enable-marks-p t)\n   (prompter:constructor (tag-suggestions))))\n\n(export-always 'url-bookmark-tags)\n(defun url-bookmark-tags (url)\n  \"Return the list of tags of the bookmark corresponding to URL.\"\n  (let ((bookmarks (files:content (bookmarks-file (current-buffer)))))\n    (when-let ((existing (find url bookmarks :key #'url :test #'url-equal)))\n      (tags existing))))\n\n(define-class new-tag-source (prompter:word-source)\n  ((prompter:name \"New tags\")\n   (prompter:filter-postprocessor\n    ;; On no input, suggest the empty tag\n    ;; which effectively acts as \"no tag\".\n    ;; Without it, we would be forced to\n    (lambda (suggestions source input)\n      (declare (ignore source input))\n      (or suggestions\n          (list \"\"))))\n   (prompter:enable-marks-p t)))\n\n(export-always 'bookmark)\n(defmethod bookmark ((url simple-array))\n  (bookmark (quri:uri url)))\n\n(defmethod bookmark ((url quri:uri))\n  (let ((title (prompt1\n                :prompt (format nil  \"Title for ~a\" (render-url url))\n                :input (fetch-url-title (render-url url))\n                :sources (make-instance 'prompter:raw-source\n                                        :name \"Title\")))\n        (tags (prompt\n               :prompt (format nil \"Tag(s) for ~a\" (render-url url))\n               :sources (list\n                         (make-instance 'new-tag-source)\n                         (make-instance 'tag-source\n                                        :marks (url-bookmark-tags url))))))\n    (persist-bookmark url :tags tags :title title)))\n\n(defmethod bookmark ((buffer buffer))\n  (let* ((url (url buffer))\n         (title (prompt1\n                 :prompt (format nil  \"Title for ~a\" (render-url url))\n                 :input (title buffer)\n                 :sources (make-instance 'prompter:raw-source\n                                         :name \"Title\")))\n         (tags (prompt\n                :prompt (format nil \"Tag(s) for ~a \" (render-url url))\n                :sources (list\n                          (make-instance 'new-tag-source)\n                          (make-instance 'keyword-source\n                                         :buffer buffer)\n                          (make-instance 'tag-source\n                                         :marks (url-bookmark-tags\n                                                 (url buffer)))))))\n    (persist-bookmark url :title title :tags tags)))\n\n(defmethod bookmark ((history-entry history-entry))\n  (let* ((url (url history-entry))\n         (title (prompt1\n                 :prompt (format nil  \"Title for ~a\" (render-url url))\n                 :input (title history-entry)\n                 :sources (make-instance 'prompter:raw-source\n                                         :name \"Title\")))\n         (tags (prompt\n                :prompt (format nil  \"Tag(s) for ~a\" (render-url url))\n                :sources (list\n                          (make-instance 'new-tag-source)\n                          (make-instance 'tag-source\n                                         :marks (url-bookmark-tags url))))))\n    (persist-bookmark url :tags tags :title title)))\n\n(define-command add-bookmark ()\n  \"Prompt for objects to bookmark.\"\n  (prompt\n   :prompt \"Add Bookmark(s)\"\n   :input (render-url (url (current-buffer)))\n   :sources (list\n             (make-instance 'prompter:raw-source\n                            :actions-on-return\n                            (lambda-mapped-command bookmark))\n             (make-instance 'buffer-source\n                            :actions-on-return\n                            (lambda-mapped-command bookmark))\n             (make-instance 'global-history-source\n                            :actions-on-return\n                            (lambda-mapped-command bookmark)))))\n\n(define-command delete-bookmark (&optional urls-or-bookmark-entries)\n  \"Delete bookmark(s) matching the chosen URLS-OR-BOOKMARK-ENTRIES.\nURLS-OR-BOOKMARK-ENTRIES could be a list or a single URL/`bookmark-entry'.\"\n  (if urls-or-bookmark-entries\n      (files:with-file-content (bookmarks (bookmarks-file (current-buffer)))\n        (setf bookmarks\n              (set-difference\n               bookmarks\n               (mapcar (lambda (url)\n                         (if (bookmark-entry-p url)\n                             url\n                             (make-instance 'bookmark-entry :url (quri:uri url))))\n                       (uiop:ensure-list urls-or-bookmark-entries))\n               :test #'equals)))\n      (let ((entries (prompt\n                      :prompt \"Delete bookmark(s)\"\n                      :sources (make-instance 'bookmark-source\n                                              :enable-marks-p t))))\n        (delete-bookmark entries))))\n\n(define-command edit-bookmark ()\n  \"Edit bookmark(s).\"\n  (let ((bookmarks (prompt\n                    :prompt \"Edit bookmark(s)\"\n                    :sources (make-instance 'bookmark-source\n                                            :enable-marks-p t))))\n    (loop for bookmark in bookmarks do\n      (let* ((url (url bookmark))\n             (title (prompt1\n                     :prompt (format nil  \"Title for ~a\" (render-url url))\n                     :input (title bookmark)\n                     :sources (make-instance 'prompter:raw-source\n                                             :name \"Title\")))\n             (tags (prompt\n                    :prompt (format nil  \"Tag(s) for ~a\" (render-url url))\n                    :sources (list\n                              (make-instance 'new-tag-source)\n                              (make-instance 'tag-source\n                                             :marks (url-bookmark-tags url))))))\n        (persist-bookmark url :title title :tags tags)))))\n\n(define-command set-url-from-bookmark ()\n  \"Set the URL for the current buffer from a bookmark.\nWith marks, open the first bookmark in the current buffer, and the rest in other\nbuffers in the background.\"\n  (prompt\n   :prompt \"Open bookmark(s)\"\n   :sources (make-instance\n             'bookmark-source\n             :actions-on-return\n             (list #'buffer-load*\n                   (lambda-command new-buffer-load (suggestion-values)\n                     \"Load bookmark(s) in new buffer(s).\"\n                     (mapc (lambda (url) (make-buffer :url (url url))) (rest suggestion-values))\n                     (make-buffer-focus :url (url (first suggestion-values))))\n                   (lambda-command copy-url* (suggestions)\n                     \"Copy bookmark URL.\"\n                     (trivial-clipboard:text (render-url (url (first suggestions)))))\n                   'delete-bookmark))))\n\n(export-always 'list-bookmarks)\n(define-internal-page-command-global list-bookmarks ()\n    (bookmarks-buffer \"*Bookmarks*\")\n  \"List all bookmarks in a new buffer.\nSplits bookmarks into groups by tags.\"\n  (let ((bookmarks (group-bookmarks bookmarks-buffer)))\n    (spinneret:with-html-string\n      (:nstyle (style (find-submode 'bookmark-mode (current-buffer)))) ; TODO: Make sure this is the right buffer\n      (render-menu 'bookmark-mode bookmarks-buffer)\n      (:h1 \"Bookmarks\")\n      (cond\n        ((zerop (hash-table-count bookmarks))\n         (:p (format nil \"No bookmarks in ~s.\" (files:expand (bookmarks-file bookmarks-buffer)))))\n        (t (maphash\n            (lambda (tag bookmarks)\n              (:nsection\n                :title (or tag \"Unsorted\")\n                :id (or tag \"unsorted\")\n                :open-p nil\n                (dolist (bookmark bookmarks)\n                  (let ((url (render-url (url bookmark)))\n                        (title (title bookmark))\n                        (tags  (tags bookmark)))\n                    (:div\n                     :class \"bookmark-entry\"\n                     (:dl\n                      (:dt\n                       (:button\n                        :onclick\n                        (ps:ps\n                          (let ((section (ps:chain (nyxt/ps:active-element document)\n                                                   (closest \".bookmark-entry\"))))\n                            (ps:chain section parent-node (remove-child section)))\n                          (nyxt/ps:lisp-eval (:title \"Delete\"\n                                              :buffer bookmarks-buffer)\n                                             (delete-bookmark url)))\n                        \"×\")\n                       (:a :href url title))\n                      (when tags\n                        (:dd (:pre (format nil \"Tags: ~{~a~^, ~}\" tags))))))))))\n            bookmarks))))))\n\n(defmethod serialize-object ((entry bookmark-entry) stream)\n  (unless (url-empty-p (url entry))\n    (flet ((write-slot (slot)\n             (let ((entry-slot (funcall slot entry)))\n               (unless (str:emptyp entry-slot)\n                 (format t \" :~a ~s\"\n                         (str:downcase slot)\n                         entry-slot)))))\n      (let ((*standard-output* stream))\n        (write-string \"(:url \")\n        (format t \"~s\" (render-url (url entry)))\n        (write-slot 'title)\n        (write-slot 'annotation)\n        (when (date entry)\n          (write-string \" :date \")\n          ;; If we don't force the timezone, the timestamp could be serialized\n          ;; differently depending on the local timezone, e.g.\n          ;;     2020-12-10T11:46:02.500515+01:00\n          ;; instead of\n          ;;     2020-12-10T10:46:02.500515Z\n          (format t \"~s\" (time:format-timestring nil (date entry)\n                                                       :timezone time:+utc-zone+)))\n        (when (tags entry)\n          (write-string \" :tags (\")\n          (format t \"~s\" (first (tags entry)))\n          (dolist (tag (rest (tags entry)))\n            (write-string \" \")\n            (write tag))\n          (write-string \")\"))\n        (write-string \")\")))))\n\n(defmethod files:serialize ((profile nyxt-profile) (file bookmarks-file) stream &key)\n  (let ((content\n         ;; Sort the entries to make serialization reproducible.\n         ;; Particularly useful when bookmarks are under version control.\n         ;;\n         ;; Need non-destructive sort here or else the cached version\n         ;; of file content may become corrupted. For example, with\n         ;; destructive SORT almost all bookmarks are removed when the\n         ;; user tries to remove just few.\n         (sera:sort-new (files:content file)\n                        #'url< :key #'url)))\n    (write-string \"(\" stream)\n    (sera:do-each (entry content)\n      (write-string +newline+ stream)\n      (serialize-object entry stream))\n    (format stream \"~%)~%\")\n    (echo \"Saved ~a bookmarks to ~s.\"\n          (length content)\n          (files:expand file))))\n\n(defmethod files:deserialize ((profile nyxt-profile) (path bookmarks-file) raw-content &key)\n  (let ((*package* (find-package :nyxt))\n        (entries (safe-read raw-content)))\n    (mapcar (lambda (entry)\n              (when (getf entry :url)\n                (setf (getf entry :url)\n                      (quri:uri (getf entry :url))))\n              (when (getf entry :date)\n                (setf (getf entry :date)\n                      (time:parse-timestring (getf entry :date))))\n              (apply #'make-instance 'bookmark-entry\n                     entry))\n            entries)))\n\n(define-command import-bookmarks-from-html\n    (&key (html-file (prompt1\n                      ;; TODO: Is there a more intuitive directory for bookmarks?\n                      :input (uiop:native-namestring (uiop:getcwd))\n                      :extra-modes 'nyxt/mode/file-manager:file-manager-mode\n                      :sources (make-instance\n                                'nyxt/mode/file-manager:file-source\n                                :extensions '(\"html\")))))\n  \"Import bookmarks from an HTML-FILE with bookmarks from other browsers.\"\n  (if (and (uiop:file-exists-p html-file)\n           (equal (pathname-type html-file) \"html\"))\n      (with-open-file (in-html html-file :external-format :utf-8)\n        (let ((a-tags (plump:get-elements-by-tag-name (plump:parse in-html) \"a\")))\n          (dolist (a-tag a-tags)\n            (let* ((url (plump:attribute a-tag \"href\"))\n                   (title (plump:render-text a-tag))\n                   (date (plump:attribute a-tag \"add_date\"))\n                   (tags (plump:attribute a-tag \"tags\"))\n                   (url-uri (quri:uri url)))\n              (when (str:starts-with? \"http\" (quri:uri-scheme url-uri))\n                (persist-bookmark url-uri\n                                  :title title\n                                  :date (ignore-errors (time:unix-to-timestamp (parse-integer date)))\n                                  :tags (when tags\n                                          (str:split \",\" tags))))))))\n      (echo \"The file doesn't exist or is not an HTML file.\")))\n\n(define-command bookmark-hint ()\n  \"Prompt for element hints and bookmark them.\"\n  (nyxt/mode/hint:query-hints\n   \"Bookmark hint\"\n   (lambda (result)\n     (dolist (url (mapcar #'url result))\n       (bookmark (quri:uri url))))\n   :selector \"a\"))\n"
  },
  {
    "path": "source/mode/bookmarklets.lisp",
    "content": ";;;; This package and file serves as a source for bookmarklets that\n;;;; originate outside of the Nyxt codebase. Eventually, the goal is\n;;;; to translate these bookmarklets into their equivalent Parenscript\n;;;; forms for easier interaction and editing.\n\n;;;; The Bookmarklets in this file are copyright Jesse Ruderman and\n;;;; are released into the public domain, per the license available\n;;;; here: https://www.squarefree.com/bookmarklets/copyright.html\n\n(nyxt:define-package :nyxt/mode/bookmarklets\n  (:documentation \"Collection of 'bookmarklets' (JavaScript snippets) to interact with web pages.\n\nAll bookmarklets are defined with `define-bookmarklet-command' and\n`define-bookmarklet-command-global'. All the defined bookmarklets are `command's\nand also global functions. Which means: they can have `:around' and other\nqualified methods to modify their behavior.\"))\n(in-package :nyxt/mode/bookmarklets)\n\n(define-mode bookmarklets-mode ()\n  \"Mode for 'bookmarklets' commands.\nBy default, this mode does nothing but expose the default bookmarklet commands.\"\n  ((visible-in-status-p nil)))\n\n(eval-always\n  (flet ((generate-body (source)\n           `(let* ((source ,source)\n                   (source (etypecase source\n                             (pathname (files:content (make-instance 'files:file :base-path source)))\n                             (string source)))\n                   (source (if (str:starts-with-p \"javascript:\" source)\n                               (quri:url-decode (subseq source 11))\n                               source)))\n              (ffi-buffer-evaluate-javascript-async buffer source))))\n    (export 'define-bookmarklet-command)\n    (defmacro define-bookmarklet-command (name documentation source)\n      \"Define a bookmarklet command of name NAME.\n\nThe SOURCE can either be\n- a JavaScript string to evaluate,\n- a `cl:pathname' to a JavaScript source file,\n- or a form evaluating to a JavaScript string.\"\n      `(define-command ,name (&optional (buffer (current-buffer)))\n         ,documentation\n         ,(generate-body source)))\n\n    (export 'define-bookmarklet-command-global)\n    (defmacro define-bookmarklet-command-global (name documentation source)\n      \"Define a global bookmarklet command of name NAME.\nSee `define-bookmarklet-command'.\"\n      `(define-command-global ,name (&optional (buffer (current-buffer)))\n         ,documentation\n         ,(generate-body source)))))\n\n\n(define-bookmarklet-command color-internal-external-links\n  \"Color internal links red, external links blue, and in-page links orange.\"\n  \"(function(){var i,x; for (i=0;x=document.links[i];++i)x.style.color=['blue','red','orange'][sim(x,location)]; function sim(a,b) { if (a.hostname!=b.hostname) return 0; if (fixPath(a.pathname)!=fixPath(b.pathname) || a.search!=b.search) return 1; return 2; } function fixPath(p){ p = (p.charAt(0)=='/' ? '' : '/') + p;/*many browsers*/ p=p.split('?')[0];/*opera*/ return p; } })()\")\n\n(define-bookmarklet-command urls-as-link-text\n  \"Changes the text of links to match their absolute URLs.\"\n  \"(function(){var i,c,x,h; for(i=0;x=document.links[i];++i) { h=x.href; x.title+=\\\" \\\" + x.innerHTML; while(c=x.firstChild)x.removeChild(c); x.appendChild(document.createTextNode(h)); } })()\")\n\n(define-bookmarklet-command hide-visited-urls\n  \"Hide visited URLs.\"\n  \"(function(){var newSS, styles=':visited {display: none}'; if(document.createStyleSheet) { document.createStyleSheet(\\\"javascript:'\\\"+styles+\\\"'\\\"); } else { newSS=document.createElement('link'); newSS.rel='stylesheet'; newSS.href='data:text/css,'+escape(styles); document.getElementsByTagName(\\\"head\\\")[0].appendChild(newSS); } })();\")\n\n(define-bookmarklet-command toggle-checkboxes\n  \"Toggle all checkboxes.\"\n  \"(function(){ function toggle(box){ temp=box.onchange; box.onchange=null; box.checked=!box.checked; box.onchange=temp; } var x,k,f,j; x=document.forms; for (k=0; k<x.length; ++k) { f=x[k]; for (j=0;j<f.length;++j) if (f[j].type.toLowerCase() == \\\"checkbox\\\") toggle(f[j]); } })();\")\n\n(define-bookmarklet-command view-password-field-contents\n  \"View passwords on page.\"\n  \"(function(){var s,F,j,f,i; s = \\\"\\\"; F = document.forms; for(j=0; j<F.length; ++j) { f = F[j]; for (i=0; i<f.length; ++i) { if (f[i].type.toLowerCase() == \\\"password\\\") s += f[i].value + \\\"\\n\\\"; } } if (s) alert(\\\"Passwords in forms on this page:\\n\\n\\\" + s); else alert(\\\"There are no passwords in forms on this page.\\\");})();\")\n\n(define-bookmarklet-command show-hidden-form-elements\n  \"Show hidden form elements.\"\n  \"(function(){var i,f,j,e,div,label,ne; for(i=0;f=document.forms[i];++i)for(j=0;e=f[j];++j)if(e.type==\\\"hidden\\\"){ D=document; function C(t){return D.createElement(t);} function A(a,b){a.appendChild(b);} div=C(\\\"div\\\"); label=C(\\\"label\\\"); A(div, label); A(label, D.createTextNode(e.name + \\\": \\\")); e.parentNode.insertBefore(div, e); e.parentNode.removeChild(e); ne=C(\\\"input\\\");/*for ie*/ ne.type=\\\"text\\\"; ne.value=e.value; A(label, ne); label.style.MozOpacity=\\\".6\\\"; --j;/*for moz*/}})()\")\n\n(define-bookmarklet-command enlarge-textareas\n  \"Increase height of all text areas by 5 vertical lines.\"\n  \"(function(){var i,x; for(i=0;x=document.getElementsByTagName(\\\"textarea\\\")[i];++i) x.rows += 5; })()\")\n\n(define-bookmarklet-command show-textbox-character-count\n  \"Displays a running count of the characters in each textbox.\"\n  \"(function(){var D=document,i,f,j,e;for(i=0;f=D.forms[i];++i)for(j=0;e=f[j];++j)if(e.type==\\\"text\\\"||e.type==\\\"password\\\"||e.tagName.toLowerCase()==\\\"textarea\\\")S(e);function S(e){if(!e.N){var x=D.createElement(\\\"span\\\"),s=x.style;s.color=\\\"green\\\";s.background=\\\"white\\\";s.font=\\\"bold 10pt sans-serif\\\";s.verticalAlign=\\\"top\\\";e.parentNode.insertBefore(x,e.nextSibling);function u(){x.innerHTML=e.value.length;}u();e.onchange=u;e.onkeyup=u;e.oninput=u;e.N=x;}else{e.parentNode.removeChild(e.N);e.N=0;}}})()\")\n\n(define-bookmarklet-command highlight-regexp\n  \"Highlights each match for a regular expression.\"\n  \"(function(){var count=0, text, regexp;text=prompt(\\\"Search regexp:\\\", \\\"\\\");if(text==null || text.length==0)return;try{regexp=new RegExp(\\\"(\\\" + text +\\\")\\\", \\\"i\\\");}catch(er){alert(\\\"Unable to create regular expression using text '\\\"+text+\\\"'.\\n\\n\\\"+er);return;}function searchWithinNode(node, re){var pos, skip, spannode, middlebit, endbit, middleclone;skip=0;if( node.nodeType==3 ){pos=node.data.search(re);if(pos>=0){spannode=document.createElement(\\\"SPAN\\\");spannode.style.backgroundColor=\\\"yellow\\\";middlebit=node.splitText(pos);endbit=middlebit.splitText(RegExp.$1.length);middleclone=middlebit.cloneNode(true);spannode.appendChild(middleclone);middlebit.parentNode.replaceChild(spannode,middlebit);++count;skip=1;}}else if( node.nodeType==1 && node.childNodes && node.tagName.toUpperCase()!=\\\"SCRIPT\\\" && node.tagName.toUpperCase!=\\\"STYLE\\\"){for (var child=0; child < node.childNodes.length; ++child){child=child+searchWithinNode(node.childNodes[child], re);}}return skip;}window.status=\\\"Searching for \\\"+regexp+\\\"...\\\";searchWithinNode(document.body, regexp);window.status=\\\"Found \\\"+count+\\\" match\\\"+(count==1?\\\"\\\":\\\"es\\\")+\\\" for \\\"+regexp+\\\".\\\";})();\")\n\n(define-bookmarklet-command zoom-images\n  \"Zoom images in.\"\n  \"(function(){ function zoomImage(image, amt) { if(image.initialHeight == null) { /* avoid accumulating integer-rounding error */ image.initialHeight=image.height; image.initialWidth=image.width; image.scalingFactor=1; } image.scalingFactor*=amt; image.width=image.scalingFactor*image.initialWidth; image.height=image.scalingFactor*image.initialHeight; } var i,L=document.images.length; for (i=0;i<L;++i) zoomImage(document.images[i], 2); if (!L) alert(\\\"This page contains no images.\\\"); })();\")\n\n(define-bookmarklet-command unzoom-images\n  \"Zoom images out.\"\n  \"(function(){ function zoomImage(image, amt) { if(image.initialHeight == null) { /* avoid accumulating integer-rounding error */ image.initialHeight=image.height; image.initialWidth=image.width; image.scalingFactor=1; } image.scalingFactor*=amt; image.width=image.scalingFactor*image.initialWidth; image.height=image.scalingFactor*image.initialHeight; } var i,L=document.images.length; for (i=0;i<L;++i) zoomImage(document.images[i],.5); if (!L) alert(\\\"This page contains no images.\\\"); })();\")\n\n(define-bookmarklet-command sort-table\n  \"Sort a table alphabetically.\"\n  \"function toArray (c){var a, k;a=new Array;for (k=0; k<c.length; ++k)a[k]=c[k];return a;}function insAtTop(par,child){if(par.childNodes.length) par.insertBefore(child, par.childNodes[0]);else par.appendChild(child);}function countCols(tab){var nCols, i;nCols=0;for(i=0;i<tab.rows.length;++i)if(tab.rows[i].cells.length>nCols)nCols=tab.rows[i].cells.length;return nCols;}function makeHeaderLink(tableNo, colNo, ord){var link;link=document.createElement('a');link.href='javascript:sortTable('+tableNo+','+colNo+','+ord+');';link.appendChild(document.createTextNode((ord>0)?'a':'d'));return link;}function makeHeader(tableNo,nCols){var header, headerCell, i;header=document.createElement('tr');for(i=0;i<nCols;++i){headerCell=document.createElement('td');headerCell.appendChild(makeHeaderLink(tableNo,i,1));headerCell.appendChild(document.createTextNode('/'));headerCell.appendChild(makeHeaderLink(tableNo,i,-1));header.appendChild(headerCell);}return header;}g_tables=toArray(document.getElementsByTagName('table'));if(!g_tables.length) alert(\\\"This page doesn't contain any tables.\\\");(function(){var j, thead;for(j=0;j<g_tables.length;++j){thead=g_tables[j].createTHead();insAtTop(thead, makeHeader(j,countCols(g_tables[j])))}}) ();function compareRows(a,b){if(a.sortKey==b.sortKey)return 0;return (a.sortKey < b.sortKey) ? g_order : -g_order;}function sortTable(tableNo, colNo, ord){var table, rows, nR, bs, i, j, temp;g_order=ord;g_colNo=colNo;table=g_tables[tableNo];rows=new Array();nR=0;bs=table.tBodies;for(i=0; i<bs.length; ++i)for(j=0; j<bs[i].rows.length; ++j){rows[nR]=bs[i].rows[j];temp=rows[nR].cells[g_colNo];if(temp) rows[nR].sortKey=temp.innerHTML;else rows[nR].sortKey=\\\"\\\";++nR;}rows.sort(compareRows);for (i=0; i < rows.length; ++i)insAtTop(table.tBodies[0], rows[i]);}\")\n\n(define-bookmarklet-command number-table-rows\n  \"Add numbers to table rows.\"\n  \"(function(){function has(par,ctag){for(var k=0;k<par.childNodes.length;++k)if(par.childNodes[k].tagName==ctag)return true;} function add(par,ctag,text){var c=document.createElement(ctag); c.appendChild(document.createTextNode(text)); par.insertBefore(c,par.childNodes[0]);} var i,ts=document.getElementsByTagName(\\\"TABLE\\\"); for(i=0;i<ts.length;++i) { var n=0,trs=ts[i].rows,j,tr; for(j=0;j<trs.length;++j) {tr=trs[j]; if(has(tr,\\\"TD\\\"))add(tr,\\\"TD\\\",++n); else if(has(tr,\\\"TH\\\"))add(tr,\\\"TH\\\",\\\"Row\\\");}}})()\")\n\n(define-bookmarklet-command number-lines\n  \"Numberlines in plaintext documents and PRE tags.\"\n  \"(function(){var i,p,L,d,j,n; for(i=0; p=document.getElementsByTagName(\\\"pre\\\")[i]; ++i) { L=p.innerHTML.split(\\\"\\r\\n\\\"); d=\\\"\\\"+L.length; for(j=0;j<L.length;++j) { n = \\\"\\\"+(j+1)+\\\". \\\"; while(n.length<d.length+2) n=\\\"0\\\"+n; L[j] = n + L[j]; } p.innerHTML=L.join(\\\"<br>\\\");/*join with br for ie*/ } })()\")\n\n(define-bookmarklet-command transpose-tables\n  \"Transpose all table row and columns.\"\n  \"(function(){var d=document,q=\\\"table\\\",i,j,k,y,r,c,t;for(i=0;t=d.getElementsByTagName(q)[i];++i){var w=0,N=t.cloneNode(0);N.width=\\\"\\\";N.height=\\\"\\\";N.border=1;for(j=0;r=t.rows[j];++j)for(y=k=0;c=r.cells[k];++k){var z,a=c.rowSpan,b=c.colSpan,v=c.cloneNode(1);v.rowSpan=b;v.colSpan=a;v.width=\\\"\\\";v.height=\\\"\\\";if(!v.bgColor)v.bgColor=r.bgColor;while(w<y+b)N.insertRow(w++).p=0;while(N.rows[y].p>j)++y;N.rows[y].appendChild(v);for(z=0;z<b;++z)N.rows[y+z].p+=a;y+=b;}t.parentNode.replaceChild(N,t);}})()\")\n\n(define-bookmarklet-command remove-color\n  \"Remove color from web pages.\"\n  \"(function(){var newSS, styles='* { background: white ! important; color: black !important } :link, :link * { color: #0000EE !important } :visited, :visited * { color: #551A8B !important }'; if(document.createStyleSheet) { document.createStyleSheet(\\\"javascript:'\\\"+styles+\\\"'\\\"); } else { newSS=document.createElement('link'); newSS.rel='stylesheet'; newSS.href='data:text/css,'+escape(styles); document.getElementsByTagName(\\\"head\\\")[0].appendChild(newSS); } })();\")\n\n(define-bookmarklet-command remove-images\n  \"Remove images from web pages.\"\n  \"(function(){function toArray (c){var a, k;a=new Array;for (k=0; k < c.length; ++k)a[k]=c[k];return a;}var images, img, altText;images=toArray(document.images);for (var i=0; i < images.length; ++i){img=images[i];altText=document.createTextNode(img.alt);img.parentNode.replaceChild(altText, img)}})();\")\n\n(define-bookmarklet-command invert-color\n  \"Invert the color of the web page.\"\n  ;; This bookmarklet was sourced from 'https://github.com/frontdevops/darkthemeswitcher-inline' with permission under free license.\n  \"(d=>{var css=`:root{background-color:#fefefe;filter:invert(100%)}*{background-color:inherit}img:not([src*=\\\".svg\\\"]),video{filter: invert(100%)}`,style,id=\\\"dark-theme-snippet\\\",ee=d.getElementById(id);if(null!=ee)ee.parentNode.removeChild(ee);else {style = d.createElement('style');style.type=\\\"text/css\\\";style.id=id;if(style.styleSheet)style.styleSheet.cssText=css;else style.appendChild(d.createTextNode(css));(d.head||d.querySelector('head')).appendChild(style)}})(document)\")\n\n(define-bookmarklet-command darken\n  \"Darken the page.\"\n  ;; This bookmarklet was sourced form 'https://github.com/x08d/222' with permission under the GPL v3.0\n  \"document.querySelectorAll('*').forEach(e=>e.setAttribute('style','background-color:#222 !important;background-image:none !important;color:#'+(/^A|BU/.test(e.tagName)?'36c;text-decoration:underline;':'eee;')+e.getAttribute('style')))\")\n"
  },
  {
    "path": "source/mode/buffer-listing.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/mode/buffer-listing\n  (:documentation \"Package for `buffer-listing-mode', mode for buffer listing.\"))\n(in-package :nyxt/mode/buffer-listing)\n\n(define-mode buffer-listing-mode ()\n  \"Mode for buffer-listing.\nHosts `list-buffers' page.\"\n  ()\n  (:toggler-command-p nil))\n\n(define-internal-page-command-global list-buffers ()\n    (listing-buffer \"*Buffers*\" 'nyxt/mode/buffer-listing:buffer-listing-mode)\n  \"Show all buffers.\"\n  (labels ((buffer-markup (buffer)\n             \"Present a buffer in HTML.\"\n             (let ((*print-pretty* nil))\n               (spinneret:with-html\n                 (:p :class \"buffer-listing\"\n                     (:nbutton\n                       :text \"✕\"\n                       :title \"Delete buffer\"\n                       `(nyxt::delete-buffer :buffers ,buffer)\n                       `(ffi-buffer-reload ,listing-buffer))\n                     (:nbutton\n                       :class \"buffer-button\"\n                       :text (format nil \"~a - ~a\"\n                                     (render-url (url buffer)) (title buffer))\n                       :title \"Switch to buffer\"\n                       `(nyxt::set-current-buffer ,buffer)))))))\n    (spinneret:with-html-string\n      (render-menu 'nyxt/mode/buffer-listing:buffer-listing-mode listing-buffer)\n      (:h1 \"Buffers\")\n      (:nstyle\n        '(.buffer-listing\n         :display \"flex\")\n        '(.buffer-button\n          :text-align \"left\"\n          :flex-grow \"1\"))\n      (:div\n       (dolist (buffer (buffer-list))\n         (buffer-markup buffer))))))\n"
  },
  {
    "path": "source/mode/certificate-exception.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/mode/certificate-exception\n  (:documentation \"Control which invalid certificates to accept or reject.\nImportant parts are:\n- `*default-certificate-exceptions*' variable.\n- and `add-domain-to-certificate-exceptions' command.\"))\n(in-package :nyxt/mode/certificate-exception)\n\n(export-always '*default-certificate-exceptions*)\n(defparameter *default-certificate-exceptions* '()\n  \"List of hostnames for which to ignore certificate errors.\nSee the `add-domain-to-certificate-exceptions' command.\")\n\n(define-mode certificate-exception-mode ()\n  \"Control which invalid certificates to accept or reject.\nThis applies to specific buffers.\nSee the `add-domain-to-certificate-exceptions' command.\"\n  ((visible-in-status-p nil)\n   (certificate-exceptions *default-certificate-exceptions*\n                           :type (list-of string))))\n\n(defmethod enable ((mode certificate-exception-mode) &key)\n  (setf (certificate-exceptions (buffer mode)) (certificate-exceptions mode)))\n\n(defmethod disable ((mode certificate-exception-mode) &key)\n  (setf (certificate-exceptions (buffer mode)) nil))\n\n(define-command add-domain-to-certificate-exceptions (&key (buffer (current-buffer)))\n  \"Add the current hostname to the buffer's certificate exception list.\nThis is only effective if `certificate-exception-mode' is enabled.\n\nTo make this change permanent, you can customize\n`*default-certificate-exceptions*' in your config file:\n\n\\(setf nyxt/mode/certificate-exception:*default-certificate-exceptions*\n      '(\\\"nyxt-browser.com\\\" \\\"example.org\\\"))\"\n  (if (find-submode 'certificate-exception-mode buffer)\n      (let ((input (prompt1\n                    :prompt \"URL host to add to exception list\"\n                    :input (render-url (url buffer))\n                    :sources (make-instance 'prompter:raw-source\n                                            :name \"URL\"))))\n        (and-let* ((url (quri:uri input))\n                   (host (and (not (url-empty-p url))\n                              (quri:uri-host url))))\n          (echo \"Added exception for ~s.\" host)\n          (pushnew host (certificate-exceptions buffer) :test #'string=)))\n      (echo \"Enable certificate-exception-mode first.\")))\n\n;; TODO: Implement command remove-domain-from-certificate-exceptions.\n;;       Currently it is not possible due to WebKit limitations.\n\n(defmethod nyxt:default-modes append ((buffer web-buffer))\n  '(certificate-exception-mode))\n"
  },
  {
    "path": "source/mode/cruise-control.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/mode/cruise-control\n  (:documentation \"Mode for scrolling continuously at a configurable speed.\nThe main API point is `cruise-control-mode'.\"))\n(in-package :nyxt/mode/cruise-control)\n\n(define-mode cruise-control-mode (nyxt/mode/repeat:repeat-mode)\n  \"Mode for automatically scrolling up and down the page.\nIt inherits from `nyxt/mode/repeat:repeat-mode'.\"\n  ((velocity\n    0\n    :documentation \"The distance the page is scrolling on each update interval.\nA positive velocity corresponds to scrolling down, a negative velocity\ncorresponds to scrolling up.\")\n   (nyxt/mode/repeat:repeat-interval 0.10)\n   ;; We're overriding it explicitly so that the cleanup of repeat-mode does not\n   ;; erase the repeat action.\n   (nyxt/mode/process:cleanup nil)\n   (keyscheme-map\n    (define-keyscheme-map \"cruise-control-mode\" ()\n      keyscheme:default\n      (list\n       \"escape\" 'cruise-control-mode\n       \"0\" 'velocity-zero)\n      keyscheme:cua\n      (list\n       \"up\" 'velocity-decf\n       \"down\" 'velocity-incf)\n      keyscheme:emacs\n      (list\n       \"p\" 'velocity-decf\n       \"n\" 'velocity-incf)\n      keyscheme:vi-normal\n      (list\n       \"K\" 'velocity-decf\n       \"J\" 'velocity-incf)))\n   (nyxt/mode/repeat:repeat-action\n    (lambda (mode)\n      (unless (zerop (velocity mode))\n        (with-current-buffer (buffer mode)\n          (nyxt/mode/document:scroll-down :y-pixels (velocity mode))))))))\n\n(define-command velocity-incf (&key (cruise-control\n                                     (find-submode 'cruise-control-mode)))\n  \"Increase the velocity.\"\n  (incf (velocity cruise-control)))\n\n(define-command velocity-decf (&key (cruise-control\n                                     (find-submode 'cruise-control-mode)))\n  \"Decrease the velocity.\"\n  (decf (velocity cruise-control)))\n\n(define-command velocity-zero (&key (cruise-control\n                                     (find-submode 'cruise-control-mode)))\n  \"Zero the velocity. Scrolling will stop.\"\n  (setf (velocity cruise-control) 0))\n"
  },
  {
    "path": "source/mode/document.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/mode/document\n  (:shadow #:focus-first-input-field)\n  (:documentation \"Package for `document-mode', mode to interact with structured documents.\"))\n(in-package :nyxt/mode/document)\n\n(define-mode document-mode ()\n  \"Mode to interact with structured documents.\nThis is typically for HTML pages, but other formats could be supported too.\nIt does not assume being online.\n\nImportant pieces of functionality are:\n- Page scrolling and zooming.\n- QR code generation.\n- view-source: for URLs.\n- Buffer content summarization.\n- Heading navigation.\n- Frame selection.\"\n  ((visible-in-status-p nil)\n   (keyscheme-map\n    (define-keyscheme-map \"document-mode\" ()\n      keyscheme:default\n      (list\n       \"C-M-Z\" 'nyxt/mode/passthrough:passthrough-mode\n       \"M-i\" 'focus-first-input-field\n       \"C-M-c\" 'open-inspector\n       \"C-C\" 'open-inspector\n       \"C-p\" 'print-buffer\n       \"C-+\" 'zoom-page\n       \"C-=\" 'zoom-page              ; Because + shifted = on QWERTY.\n       \"C-button4\" 'zoom-page\n       \"C-hyphen\" 'unzoom-page\n       \"C-button5\" 'unzoom-page\n       \"C-0\" 'reset-page-zoom)\n      keyscheme:cua\n      (list\n       \"C-h\" 'jump-to-heading\n       \"C-M-h\" 'jump-to-heading-buffers\n       \"C-c\" 'copy\n       \"C-v\" 'paste\n       \"M-v\" 'paste-from-clipboard-ring\n       \"C-x\" 'cut\n       \"C-a\" 'select-all\n       \"C-z\" 'undo\n       \"C-Z\" 'redo\n       \"C-down\" 'scroll-to-bottom\n       \"C-up\" 'scroll-to-top\n       ;; Leave SPACE, END, HOME and arrow keys unbound for the renderer\n       \"keypadleft\" 'scroll-left\n       \"keypadright\" 'scroll-right\n       \"keypadup\" 'scroll-up\n       \"keypaddown\" 'scroll-down\n       \"keypadhome\" 'scroll-to-top\n       \"keypadend\" 'scroll-to-bottom\n       \"keypadpageup\" 'scroll-page-up\n       \"keypadprior\" 'scroll-page-up\n       \"keypadnext\" 'scroll-page-down\n       \"C-u C-o\" 'edit-with-external-editor)\n      keyscheme:emacs\n      (list\n       \"C-.\" 'jump-to-heading\n       \"C-M-.\" 'jump-to-heading-buffers\n       \"C-g\" 'nothing              ; Emacs users may hit C-g out of habit.\n       \"M-w\" 'copy\n       \"C-y\" 'paste\n       \"M-y\" 'paste-from-clipboard-ring\n       \"C-w\" 'cut\n       \"C-x h\" 'select-all\n       \"C-/\" 'undo\n       \"C-?\" 'redo ; / shifted on QWERTY\n       \"C-x C-+\" 'zoom-page\n       \"C-x C-=\" 'zoom-page ; Because + shifted = on QWERTY.\n       \"C-x C-hyphen\" 'unzoom-page\n       \"C-x C-0\" 'reset-page-zoom\n       \"C-p\" 'scroll-up\n       \"C-n\" 'scroll-down\n       \"M-<\" 'scroll-to-top\n       \"M->\" 'scroll-to-bottom\n       \"M-v\" 'scroll-page-up\n       \"C-v\" 'scroll-page-down\n       \"C-c C-e\" 'nyxt/mode/input-edit:input-edit-mode\n       \"C-u C-x C-f\" 'edit-with-external-editor)\n      keyscheme:vi-normal\n      (list\n       \"g h\" 'jump-to-heading\n       \"g H\" 'jump-to-heading-buffers\n       \"y y\" 'copy\n       \"p\" 'paste\n       ;; Debatable: means \"insert after cursor\" in Vi(m).\n       \"P\" 'paste-from-clipboard-ring\n       \"d d\" 'cut\n       \"u\" 'undo\n       \"C-r\" 'redo\n       \"+\" 'zoom-page\n       \"z i\" 'zoom-page\n       \"hyphen\" 'unzoom-page\n       \"z o\" 'unzoom-page\n       \"0\" 'reset-page-zoom\n       \"z z\" 'reset-page-zoom\n       \"h\" 'scroll-left\n       \"l\" 'scroll-right\n       \"k\" 'scroll-up\n       \"j\" 'scroll-down\n       \"g g\" 'scroll-to-top\n       \"G\" 'scroll-to-bottom\n       \"C-b\" 'scroll-page-up\n       \"shift-space\" 'scroll-page-up\n       \"pageup\" 'scroll-page-up\n       \"C-f\" 'scroll-page-down\n       \"space\" 'scroll-page-down\n       \"pagedown\" 'scroll-page-down)))))\n\n(define-configuration document-buffer\n  ((default-modes (cons 'document-mode %slot-value%))))\n\n(export-always 'active-element-tag)\n(defun active-element-tag (&optional (buffer (current-buffer)))\n  \"The name of the active element in BUFFER.\"\n  (ps-eval :buffer buffer (ps:@ (nyxt/ps:active-element document) tag-name)))\n\n(export-always 'input-tag-p)\n(-> input-tag-p ((or string null)) boolean)\n(defun input-tag-p (tag)\n  \"Whether TAG is inputtable.\"\n  (or (string= tag \"INPUT\")\n      (string= tag \"TEXTAREA\")))\n\n(define-command paste (&optional (buffer (current-buffer)))\n  \"Paste from clipboard into active element.\"\n  (ffi-buffer-paste buffer))\n\n(define-class ring-source (prompter:source)\n  ((prompter:name \"Clipboard ring\")\n   (ring :initarg :ring :accessor ring :initform nil)\n   (prompter:filter-preprocessor #'prompter:filter-exact-matches)\n   (prompter:constructor\n    (lambda (source)\n      (containers:container->list (ring source))))\n   (prompter:actions-on-return (lambda-command paste* (ring-items)\n                                 (ffi-buffer-paste (current-buffer) (first ring-items)))))\n  (:export-class-name-p t)\n  (:metaclass user-class)\n  (:documentation \"Source for previous clipboard contents.\nOnly includes the strings that were pasted/copied inside Nyxt.\"))\n\n(define-command paste-from-clipboard-ring ()\n  \"Show `*browser*' clipboard ring and paste selected entry.\"\n  (ring-insert-clipboard (clipboard-ring *browser*))\n  (prompt :prompt \"Paste from ring\"\n          :sources (make-instance 'ring-source :ring (clipboard-ring *browser*))))\n\n(define-command copy (&optional (buffer (current-buffer)))\n  \"Copy selected text to clipboard.\"\n  (ffi-buffer-copy buffer))\n\n(define-command cut (&optional (buffer (current-buffer)))\n  \"Cut the selected text in BUFFER.\"\n  (ffi-buffer-cut buffer))\n\n(define-command undo (&optional (buffer (current-buffer)))\n  \"Undo the last editing action.\"\n  (ffi-buffer-undo buffer))\n\n(define-command redo (&optional (buffer (current-buffer)))\n  \"Redo the last editing action.\"\n  (ffi-buffer-redo buffer))\n\n(define-command select-all (&optional (buffer (current-buffer)))\n  \"Select all the text in the text field.\"\n  (ffi-buffer-select-all buffer))\n\n(define-command focus-first-input-field (&key (buffer (current-buffer)))\n  \"Move the focus to the first inputtable element of BUFFER.\"\n  ;; There are two basic ways to have an editable widget on a webpage:\n  ;; - Using <input>/<textarea>,\n  ;; - or marking any other element as contenteditable:\n  ;;   https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/contenteditable\n  (loop with inputs = (clss:ordered-select \"input, textarea, [contenteditable]\" (document-model buffer))\n        for input across inputs\n        when (ps-eval :buffer buffer\n               (nyxt/ps:element-editable-p\n                (nyxt/ps:qs-nyxt-id document (ps:lisp (nyxt/dom:get-nyxt-id input)))))\n          do (nyxt/dom:focus-select-element input)\n          and do (return input)))\n\n(defmethod nyxt:on-signal-load-finished ((mode document-mode) url title)\n  (declare (ignore title))\n  (with-slots (buffer) mode\n    ;; Ensures that the buffer-local zoom ratio is honored.\n    (setf (ffi-buffer-zoom-ratio buffer)\n          (or (zoom-ratio buffer) (zoom-ratio-default buffer))))\n  url)\n\n(define-internal-page show-url-qrcode (&key url)\n    (:title \"*Buffer URL QR code*\")\n  \"Display the QR code containing URL.\nWarning: URL is a string.\"\n  (let* ((url (quri:render-uri (url url)))\n         (stream (flex:make-in-memory-output-stream)))\n    (cl-qrencode:encode-png-stream url stream)\n    (spinneret:with-html-string\n      (:p (:u url))\n      (:p (:img :src (str:concat \"data:image/png;base64,\"\n                                 (cl-base64:usb8-array-to-base64-string\n                                  (flex:get-output-stream-sequence stream)))\n                :alt url)))))\n\n(define-command-global show-url-qrcode (&key (buffer (current-buffer)))\n  \"Display the QR code containing the URL of BUFFER in a new page.\"\n  (buffer-load-internal-page-focus 'show-url-qrcode :url (quri:render-uri (url buffer))))\n\n(export-always 'get-url-source)\n(defun get-url-source (url)\n  \"Get HTML source for URL page, as a string.\"\n  (let ((buffer (find url (buffer-list) :test #'quri:uri= :key #'url)))\n    (unless buffer\n      (return-from get-url-source (echo-warning \"No buffer loaded URL: ~a\" url)))\n    (let ((dom (if (web-buffer-p buffer)\n                   (nyxt/dom:copy (document-model buffer))\n                   (plump:parse (ffi-buffer-get-document buffer)))))\n      (loop for e across (clss:select \"[nyxt-identifier]\" dom)\n            do (plump:remove-attribute e \"nyxt-identifier\"))\n      (map nil #'plump:remove-child (reverse (clss:select \".nyxt-hint\" dom)))\n      (plump:serialize dom nil))))\n\n(define-internal-scheme \"view-source\"\n    (lambda (url)\n      (values (get-url-source (quri:uri-path (quri:uri url)))\n              \"text/plain\")))\n\n(define-command-global view-source (&key (url (url (current-buffer))))\n  \"View source of the URL (by default current page) in a separate buffer.\"\n  (make-buffer-focus :url (quri:make-uri :scheme \"view-source\"\n                                         :path (quri:render-uri url))))\n\n(define-command scroll-to-top (&key (smooth-p (smooth-scrolling (current-buffer))))\n  \"Scroll to the top of the current page.\"\n  (ps-eval\n    (ps:chain window\n              (scroll-by (ps:create top (- (ps:chain document document-element scroll-height))\n                                    behavior (ps:lisp (if smooth-p \"smooth\" \"instant\")))))))\n\n(define-command scroll-to-bottom (&key (smooth-p (smooth-scrolling (current-buffer))))\n  \"Scroll to the bottom of the current page.\"\n  (ps-eval\n    (ps:chain window\n              (scroll-by (ps:create top (ps:chain document document-element scroll-height)\n                                    behavior (ps:lisp (if smooth-p \"smooth\" \"instant\")))))))\n\n(define-command scroll-down (&key (y-pixels (scroll-distance (current-buffer)))\n                             (smooth-p (smooth-scrolling (current-buffer))))\n  \"Scroll down the current page.\nThe amount scrolled is determined by the buffer's `scroll-distance'.\"\n  (ps-eval\n    (ps:chain window\n              (scroll-by (ps:create top (ps:lisp y-pixels)\n                                    behavior (ps:lisp (if smooth-p \"smooth\" \"instant\")))))))\n\n(define-command scroll-up (&key (y-pixels (scroll-distance (current-buffer)))\n                           (smooth-p (smooth-scrolling (current-buffer))))\n  \"Scroll up the current page.\nThe amount scrolled is determined by the buffer's `scroll-distance'.\"\n  (ps-eval\n    (ps:chain window\n              (scroll-by (ps:create top (ps:lisp (- y-pixels))\n                                    behavior (ps:lisp (if smooth-p \"smooth\" \"instant\")))))))\n\n(define-command scroll-left (&key (x-pixels (horizontal-scroll-distance (current-buffer)))\n                             (smooth-p (smooth-scrolling (current-buffer))))\n  \"Scroll left the current page.\nThe amount scrolled is determined by the buffer's `horizontal-scroll-distance'.\"\n  (ps-eval\n    (ps:chain window\n              (scroll-by (ps:create left (ps:lisp (- x-pixels))\n                                    behavior (ps:lisp (if smooth-p \"smooth\" \"instant\")))))))\n\n(define-command scroll-right (&key (x-pixels (horizontal-scroll-distance (current-buffer)))\n                              (smooth-p (smooth-scrolling (current-buffer))))\n  \"Scroll right the current page.\nThe amount scrolled is determined by the buffer's `horizontal-scroll-distance'.\"\n  (ps-eval\n    (ps:chain window\n              (scroll-by (ps:create left (ps:lisp x-pixels)\n                                    behavior (ps:lisp (if smooth-p \"smooth\" \"instant\")))))))\n\n(define-command scroll-page-down ()\n  \"Scroll down by one page height.\"\n  (ps-eval (ps:chain window (scroll-by 0 (* (ps:lisp (page-scroll-ratio (current-buffer)))\n                                          (ps:@ window inner-height))))))\n\n(define-command scroll-page-up ()\n  \"Scroll up by one page height.\"\n  (ps-eval (ps:chain window (scroll-by 0 (- (* (ps:lisp (page-scroll-ratio (current-buffer)))\n                                             (ps:@ window inner-height)))))))\n\n(define-command zoom-page (&key (buffer (current-buffer)))\n  \"Zoom in the current buffer.\"\n  (incf (ffi-buffer-zoom-ratio buffer) (zoom-ratio-step buffer)))\n\n(define-command unzoom-page (&key (buffer (current-buffer)))\n  \"Zoom out the current buffer.\"\n  (decf (ffi-buffer-zoom-ratio buffer) (zoom-ratio-step buffer)))\n\n(define-command reset-page-zoom (&key (buffer (current-buffer)))\n  \"Reset to the default zoom.\"\n  ;; Delete `zoom-ratio-default' slot when we are able to get the initform of a\n  ;; slot set by `define-configuration'.\n  (setf (ffi-buffer-zoom-ratio buffer) (zoom-ratio-default buffer)))\n\n(define-class heading ()\n  ((inner-text \"\" :documentation \"The inner text of the heading within the document.\")\n   (element nil :documentation \"The header-representing element of `document-model'.\")\n   (buffer :documentation \"The buffer to which this heading belongs.\")\n   (keywords :documentation \"Keywords associated with this heading.\")\n   (scroll-position :documentation \"The scroll position of the heading.\"))\n  (:documentation \"A heading representation with all the attached metadata.\nThe inner-text must not be modified, so that we can jump to the anchor of the same name.\"))\n\n(defmethod title ((heading heading))\n  (subseq (inner-text heading) 0 (position #\\[ (inner-text heading))))\n\n(defun get-headings (&key (buffer (current-buffer)))\n  (ps-labels :buffer buffer\n    ((heading-scroll-position\n      :buffer buffer (element)\n      (ps:chain (nyxt/ps:rqs-nyxt-id document (ps:lisp (nyxt/dom:get-nyxt-id element)))\n                (get-bounding-client-rect) y)))\n    (map 'list\n         (lambda (e)\n           (make-instance 'heading :inner-text (plump:text e)\n                                   :element e\n                                   :buffer buffer\n                                   :keywords (ignore-errors\n                                              (analysis:extract-keywords\n                                               (plump:text (plump:next-element e))))\n                                   :scroll-position (heading-scroll-position e)))\n         (clss:ordered-select \"h1, h2, h3, h4, h5, h6\" (document-model buffer)))))\n\n(defun current-heading (&optional (buffer (current-buffer)))\n  (when-let* ((scroll-position (document-scroll-position buffer))\n              (vertical-scroll-position (second scroll-position))\n              (headings (get-headings :buffer buffer)))\n    (first (sort headings\n                 (lambda (h1 h2)\n                   (< (abs (- (scroll-position h1) vertical-scroll-position))\n                      (abs (- (scroll-position h2) vertical-scroll-position))))))))\n\n(defun scroll-page-to-heading (heading)\n  (set-current-buffer (buffer heading) :focus nil)\n  (nyxt/dom:scroll-to-element (element heading)))\n\n(define-class heading-source (prompter:source)\n  ((prompter:name \"Headings\")\n   (buffer :accessor buffer :initarg :buffer)\n   (prompter:filter-preprocessor #'prompter:filter-exact-matches)\n   (prompter:actions-on-current-suggestion-enabled-p t)\n   (prompter:actions-on-current-suggestion\n    (lambda-command scroll-page-to-heading* (heading)\n      \"Scroll to heading.\"\n      (scroll-page-to-heading heading)))\n   (prompter:constructor (lambda (source)\n                           (get-headings :buffer (buffer source))))\n   (prompter:actions-on-return (lambda-unmapped-command scroll-page-to-heading))))\n\n(defmethod prompter:object-attributes ((heading heading) (source heading-source))\n  (declare (ignore source))\n  `((\"Title\" ,(format nil \"~a ~a\"\n                      (make-string (typecase (element heading)\n                                     (nyxt/dom:h1-element 1)\n                                     (nyxt/dom:h2-element 2)\n                                     (nyxt/dom:h3-element 3)\n                                     (nyxt/dom:h4-element 4)\n                                     (nyxt/dom:h5-element 5)\n                                     (nyxt/dom:h6-element 6)\n                                     (t 0))\n                                   :initial-element #\\*)\n                      (title heading)))\n    (\"Keywords\" ,(format nil \"~:{~a~^ ~}\" (keywords heading)))))\n\n(define-command jump-to-heading (&key (buffer (current-buffer)))\n  \"Jump to a particular heading, of type h1, h2, h3, h4, h5, or h6.\"\n  (prompt :prompt \"Jump to heading\"\n          :sources (make-instance 'heading-source :buffer buffer)))\n\n(define-command jump-to-heading-buffers ()\n  \"Jump to a heading (H1 to H6) among a set of buffers.\"\n  (let ((buffers (prompt\n                  :prompt \"Select headings from buffers\"\n                  :sources (make-instance 'buffer-source\n                                          :enable-marks-p t\n                                          :actions-on-return #'identity))))\n    (prompt\n     :prompt \"Jump to heading\"\n     :sources (loop for buffer in buffers\n                    collect (make-instance\n                             'heading-source\n                             :name (format nil \"Headings: ~a\" (title buffer))\n                             :buffer buffer)))))\n\n(export-always 'print-buffer)\n(define-command print-buffer ()\n  \"Print the current buffer.\"\n  (ps-eval (print)))\n\n\f\n;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n;; Frame selection engine:\n\n(define-class html-element ()\n  ((body \"\")))\n\n(define-class link (html-element)\n  ((url \"\")))\n\n(define-class image (html-element)\n  ((alt \"\" :documentation \"Alternative text for the image.\")\n   (url \"\")))\n\n(defun frame-element-select ()\n  \"Allow the user to draw a frame around elements to select them.\"\n  (let ((overlay-style (theme:themed-css (theme *browser*)\n                         `(\"#nyxt-overlay\"\n                           :position \"fixed\"\n                           :top \"0\"\n                           :left \"0\"\n                           :right \"0\"\n                           :bottom \"0\"\n                           :opacity 0.00\n                           :background ,theme:on-background-color\n                           :z-index #.(1- (expt 2 31)))))\n        (selection-rectangle-style (theme:themed-css (theme *browser*)\n                                     `(\"#nyxt-rectangle-selection\"\n                                       :position \"absolute\"\n                                       :top \"0\"\n                                       :left \"0\"\n                                       :border-style \"dotted\"\n                                       :border-width \"4px\"\n                                       :border-color ,theme:background-color\n                                       :background-color ,theme:on-background-color\n                                       :opacity 0.10\n                                       :z-index ,(1- (expt 2 30))))))\n    (ps-labels :async t\n      ((add-overlay\n        (overlay-style selection-rectangle-style)\n        \"Add a selectable overlay to the screen.\"\n        (defparameter selection\n          (ps:create x1 0 y1 0\n                     x2 0 y2 0\n                     set1 false\n                     set2 false))\n        (defun add-stylesheet ()\n          (unless (nyxt/ps:qs document \"#nyxt-stylesheet\")\n            (ps:let ((style-element (ps:chain document (create-element \"style\"))))\n              (setf (ps:@ style-element id) \"nyxt-stylesheet\")\n              (ps:chain document head (append-child style-element)))))\n        (defun add-style (style)\n          (ps:let ((style-element (nyxt/ps:qs document \"#nyxt-stylesheet\")))\n            (ps:chain style-element sheet (insert-rule style 0))))\n        (defun add-overlay ()\n          (ps:let ((element (ps:chain document (create-element \"div\"))))\n            (add-style (ps:lisp overlay-style))\n            (setf (ps:@ element id) \"nyxt-overlay\")\n            (ps:chain document body (append-child element))))\n        (defun add-selection-rectangle ()\n          (ps:let ((element (ps:chain document (create-element \"div\"))))\n            (add-style (ps:lisp selection-rectangle-style))\n            (setf (ps:@ element id) \"nyxt-rectangle-selection\")\n            (ps:chain document body (append-child element))))\n        (defun update-selection-rectangle ()\n          (ps:let ((element (nyxt/ps:qs document \"#nyxt-rectangle-selection\")))\n            (setf (ps:@ element style left) (+ (ps:chain selection x1) \"px\"))\n            (setf (ps:@ element style top) (+ (ps:chain selection y1) \"px\"))\n            (setf (ps:@ element style width)\n                  (+ (- (ps:chain selection x2)\n                        (ps:chain selection x1))\n                     \"px\"))\n            (setf (ps:@ element style height)\n                  (+ (- (ps:chain selection y2)\n                        (ps:chain selection y1))\n                     \"px\"))))\n        (defun add-listeners ()\n          (setf (ps:@ (nyxt/ps:qs document \"#nyxt-overlay\") onmousemove)\n                (lambda (e)\n                  (when (and (ps:chain selection set1)\n                             (not (ps:chain selection set2)))\n                    (setf (ps:chain selection x2) (ps:chain e |pageX|))\n                    (setf (ps:chain selection y2) (ps:chain e |pageY|))\n                    (update-selection-rectangle))))\n          (setf (ps:@ (nyxt/ps:qs document \"#nyxt-overlay\") onclick)\n                (lambda (e)\n                  (if (not (ps:chain selection set1))\n                      (progn\n                        (setf (ps:chain selection x1) (ps:chain e |pageX|))\n                        (setf (ps:chain selection y1) (ps:chain e |pageY|))\n                        (setf (ps:chain selection set1) true))\n                      (progn\n                        (setf (ps:chain selection x2) (ps:chain e |pageX|))\n                        (setf (ps:chain selection y2) (ps:chain e |pageY|))\n                        (setf (ps:chain selection set2) true))))))\n        (add-stylesheet)\n        (add-overlay)\n        (add-selection-rectangle)\n        (add-listeners)))\n      (add-overlay overlay-style selection-rectangle-style))))\n\n(defun frame-element-get-selection ()\n  \"Get the selected elements drawn by the user.\"\n  (ps-labels\n    ((get-selection\n      ()\n      (defun element-in-selection-p (selection element)\n        \"Determine if a element is bounded within a selection.\"\n        (ps:let* ((element-rect (ps:chain element (get-bounding-client-rect)))\n                  (offsetX (ps:chain window |pageXOffset|))\n                  (offsetY (ps:chain window |pageYOffset|))\n                  (element-left (+ (ps:chain element-rect left) offsetX))\n                  (element-right (+ (ps:chain element-rect right) offsetX))\n                  (element-top (+ (ps:chain element-rect top) offsetY))\n                  (element-bottom (+ (ps:chain element-rect bottom) offsetY)))\n          (if (and\n               (<= element-left (ps:chain selection x2))\n               (>= element-right (ps:chain selection x1))\n               (<= element-top (ps:chain selection y2))\n               (>= element-bottom (ps:chain selection y1)))\n              t nil)))\n      (defun object-create (element)\n        (cond ((equal \"A\" (ps:@ element tag-name))\n               (ps:create \"type\" \"link\" \"href\" (ps:@ element href) \"body\" (ps:@ element |innerHTML|)))\n              ((equal \"IMG\" (ps:@ element tag-name))\n               (ps:create \"type\" \"img\" \"src\" (ps:@ element src) \"alt\" (ps:@ element alt)))))\n      (defun collect-selection (elements selection)\n        \"Collect elements within a selection\"\n        (loop for element in elements\n              when (element-in-selection-p selection element)\n                collect (object-create element)))\n      (collect-selection (nyxt/ps:qsa document (list \"a\")) selection)))\n    (loop for element in (get-selection)\n          collect (str:string-case (cdr (assoc :type element))\n                    (\"link\"\n                     (make-instance 'link\n                                    :url (cdr (assoc :href element))\n                                    :body (cdr (assoc :body element))))\n                    (\"img\"\n                     (make-instance 'image\n                                    :url (cdr (assoc :src element))\n                                    :alt (cdr (assoc :alt element))))))))\n\n(define-parenscript frame-element-selection-ready ()\n  \"Check to see if the selection is complete.\"\n  (and (ps:chain selection set1)\n       (ps:chain selection set2)))\n\n(defun frame-element-clear ()\n  \"Clear the selection frame created by the user.\"\n  (ps-eval\n    (ps:chain (nyxt/ps:qs document \"#nyxt-rectangle-selection\") (remove))\n    (ps:chain (nyxt/ps:qs document \"#nyxt-overlay\") (remove))))\n\n(define-class frame-source (prompter:source)\n  ((prompter:name \"Selection Frame\")\n   (buffer :accessor buffer :initarg :buffer)\n   (prompter:constructor (lambda (source)\n                           (with-current-buffer (buffer source)\n                             (frame-element-select)\n                             (prog1\n                                 (loop\n                                   do (sleep 0.25)\n                                   when (frame-element-selection-ready)\n                                   return (frame-source-selection))\n                               (toggle-prompt-buffer-focus)))))))\n\n(define-command select-frame-new-buffer (&key (buffer (current-buffer)))\n  \"Select a frame and open the links in new buffers.\"\n  (prompt :prompt \"Open selected links in new buffers\"\n          :sources (make-instance\n                    'frame-source\n                    :buffer buffer\n                    :enable-marks-p t\n                    :actions-on-return (lambda-command open-new-buffers (urls)\n                                         (mapcar (lambda (i) (make-buffer :url (quri:uri i)))\n                                                 urls)))\n          :after-destructor (lambda () (with-current-buffer buffer\n                                    (frame-element-clear)))))\n\n(defun frame-source-selection ()\n  (remove-duplicates (mapcar #'url (frame-element-get-selection))\n                     :test #'equal))\n\n(pushnew 'document-mode nyxt::%default-modes)\n"
  },
  {
    "path": "source/mode/download.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/mode/download\n  (:documentation \"Package for `download-mode', mode to manage downloads and the\ndownloads page.\"))\n(in-package :nyxt/mode/download)\n\n(export-always 'renderer-download)\n(defclass renderer-download ()\n  ()\n  (:metaclass interface-class)\n  (:documentation \"A basis for renderer-specific download objects.\nShould be redefined by the renderer.\"))\n\n(define-class download (renderer-download)\n  ((url\n    (error \"URL required.\")\n    :documentation \"A string representation of the URL.\")\n   (status\n    :unloaded\n    :export t\n    :reader status\n    :type (member :unloaded :loading :finished :failed :canceled)\n    :documentation \"Status of the download.\")\n   (status-text\n    (make-instance 'user-interface:paragraph)\n    :export nil)\n   (completion-percentage\n    0.0\n    :reader t\n    :export t\n    :type float\n    :documentation \"The download complete percentage.\")\n   (bytes-downloaded\n    \"-\"\n    :reader t\n    :export t\n    :documentation \"The number of bytes downloaded.\")\n   (bytes-text\n    (make-instance 'user-interface:paragraph)\n    :export nil\n    :documentation \"The interface element that shows `bytes-downloaded'.\")\n   (destination-path\n    #p\"\"\n    :export t\n    :type pathname\n    :documentation \"The path where the download is written to.\")\n   (before-download-hook\n    (make-instance 'hook-download)\n    :type hook-download\n    :documentation \"Hook run before downloading a file.\nThe handlers take the `download' instance as argument.\n\nExample: echo the file name to be downloaded\n\n\\(define-configuration nyxt/mode/download:download\n  ((nyxt/mode/download:before-download-hook\n    (hooks:add-hook %slot-value%\n      (make-instance 'hooks:handler\n                     :fn (lambda (download)\n                           (echo \\\"The URL for the download is ~a\\\"\n                             (render-url (url download))))\n                     :name 'echo-name)))))\")\n   (after-download-hook\n    (make-instance 'hook-download)\n    :type hook-download\n    :documentation \"Hook run after a download has completed.\nThe handlers take the `download' instance as argument.\n\nExample: open the loaded files with XDG-open\n\\(define-configuration nyxt/mode/download:download\n  ((nyxt/mode/download:after-download-hook\n    (hooks:add-hook\n     %slot-value%\n     (make-instance\n       'hooks:handler\n       :fn (lambda (download)\n             (uiop:launch-program\n              (list \\\"xdg-open\\\"\n                 (uiop:native-namestring\n                   (nyxt/mode/download:destination-path download)))))\n       :name 'xdg-open-download)))))\")\n   (cancel-function\n    nil\n    :export t\n    :type (or null function)\n    :documentation \"The function that cancels the download.\nIt can be set by the download engine.\")\n   (progress-text\n    (make-instance 'user-interface:paragraph)\n    :export nil)\n   (progress\n    (make-instance 'user-interface:progress-bar)\n    :export nil))\n  (:metaclass user-class)\n  (:export-accessor-names-p t)\n  (:export-class-name-p t)\n  (:documentation \"This class is used to represent a download.\nThe `downloads' slot is populated by a list of these objects.\"))\n\n(hooks:define-hook-type download (function (download))\n  \"Hook acting on `download' objects.\")\n\n(defmethod initialize-instance :after ((download download)\n                                       &key &allow-other-keys)\n  (hooks:run-hook (before-download-hook download) download)\n  (list-downloads))\n\n(defmethod cancel-download ((download download))\n  \"Call `cancel-function' with URL as argument.\"\n  (funcall (cancel-function download))\n  (echo \"Download canceled: ~a.\" (url download))\n  (buffer-load-internal-page-focus 'list-downloads))\n\n(defmethod (setf status) (value (download download))\n  (setf (slot-value download 'status) value)\n  (setf (user-interface:text (status-text download))\n        (format nil \"Status: ~(~a~).\" value))\n  ;; Refresh downloads page and show it upon download completion.\n  (when (eq value :finished)\n    (buffer-load-internal-page-focus 'list-downloads)\n    (hooks:run-hook (after-download-hook download) download)))\n\n(defmethod (setf completion-percentage) (percentage (download download))\n  (setf (slot-value download 'completion-percentage) percentage)\n  (setf (user-interface:percentage (progress download))\n        (completion-percentage download))\n  (setf (user-interface:text (progress-text download))\n        (format nil \"Completion: ~,2f%\" (completion-percentage download))))\n\n(defmethod (setf bytes-downloaded) (bytes (download download))\n  (setf (slot-value download 'bytes-downloaded) bytes)\n  (setf (user-interface:text (bytes-text download))\n        (format nil \"Bytes downloaded: ~a\" (bytes-downloaded download))))\n\n(defmethod connect ((download download) buffer)\n  \"Connect the user-interface objects within the download to the\nbuffer. This allows the user-interface objects to update their\nappearance in the buffer when they are setf'd.\"\n  (user-interface:connect (status-text download) buffer)\n  (user-interface:connect (progress-text download) buffer)\n  (user-interface:connect (bytes-text download) buffer)\n  (user-interface:connect (progress download) buffer))\n\n(define-mode download-mode ()\n  \"Display list of downloads.\"\n  ((style\n    (theme:themed-css (theme *browser*)\n      `(\".download\"\n        :background-color ,theme:background-color\n        :color ,theme:on-background-color\n        :margin-top \"10px\"\n        :padding-left \"5px\"\n        :brightness \"80%\"\n        :border-radius \"2px\")\n      '(\".download-url\"\n        :overflow \"auto\"\n        :white-space \"nowrap\")\n      `(\".download-url a\"\n        :color ,theme:on-background-color\n        :font-size \"small\")\n      '(\".status p\"\n        :display \"inline-block\"\n        :margin-right \"10px\")\n      '(\".progress-bar-container\"\n        :height \"20px\"\n        :width \"100%\")\n      `(\".progress-bar-base\"\n        :background-color ,theme:secondary-color\n        :height \"100%\")\n      `(\".progress-bar-fill\"\n        :background-color ,theme:success-color\n        :height \"100%\"))))\n  (:toggler-command-p nil))\n\n(define-internal-page-command-global list-downloads ()\n    (buffer \"*Downloads*\" 'download-mode)\n  \"Display a buffer listing all downloads.\nWe iterate through the browser's downloads to draw every single\ndownload.\"\n  (spinneret:with-html-string\n    (:nstyle (style (find-submode 'download-mode)))\n    (render-menu 'download-mode buffer)\n    (:h1 \"Downloads\")\n    (:hr)\n    (:div\n     (or\n      (loop for download in (downloads *browser*)\n            for url = (url download)\n            for status-text = (status-text download)\n            for progress-text = (progress-text download)\n            for bytes-text = (bytes-text download)\n            for progress = (progress download)\n            for destination-path = (destination-path download)\n            do (connect download buffer)\n            collect\n            (:div :class \"download\"\n                  (when (member (status download) '(:unloaded :loading))\n                    (:nbutton\n                      :text \"✕ Cancel Download\"\n                      :title \"Cancel Download\"\n                      `(cancel-download ,download)))\n                  (when (eq (status download) :finished)\n                    (:nbutton\n                      :text \"Open File\"\n                      :title \"Open File\"\n                      `(nyxt/mode/file-manager:default-open-file-function\n                        ,destination-path)))\n                  (:p :class \"download-url\" (:a :href url url))\n                  (:div :class \"progress-bar-container\"\n                        (user-interface:to-html progress))\n                  (:div :class \"status\"\n                        (user-interface:to-html progress-text)\n                        (user-interface:to-html bytes-text)\n                        (user-interface:to-html status-text))))\n      (:p \"No downloads available.\")))))\n\n(define-command-global download-url ()\n  \"Download the page or file of the current buffer.\"\n  (ffi-buffer-download (current-buffer) (render-url (url (current-buffer)))))\n\n(define-command-global download-hint-url ()\n  \"Prompt for element hints and download them.\"\n  (let ((buffer (current-buffer)))\n    (nyxt/mode/hint:query-hints\n     \"Download link URL\"\n     (lambda (selected-links)\n       (loop for link in selected-links\n             do (ffi-buffer-download buffer (render-url (url link)))))\n     :selector \"a\")))\n"
  },
  {
    "path": "source/mode/emacs.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/mode/emacs\n  (:documentation \"Package for `emacs-mode', mode to host Emacs inspired\nkeybindings.\"))\n(in-package :nyxt/mode/emacs)\n\n(define-mode emacs-mode (nyxt/mode/keyscheme:keyscheme-mode)\n  \"Enable Emacs inspired keybindings.\n\nTo enable them by default, append the mode to the list of `default-modes' in\nyour configuration file.\n\nExample:\n\n\\(define-configuration buffer\n  ((default-modes (append '(emacs-mode) %slot-value%))))\"\n  ((glyph \"e\")\n   (keyscheme keyscheme:emacs)))\n"
  },
  {
    "path": "source/mode/expedition.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/mode/expedition\n  (:documentation \"Package for `expedition-mode',\nmode to traverse chosen URLs.\"))\n(in-package :nyxt/mode/expedition)\n\n(define-mode expedition-mode ()\n  \"Mode to traverse URLs delimited by a user specified buffer rectangle.\"\n  ((urls (list))\n   (index\n    0\n    :documentation \"The index of the current element in URLs.\")\n   (keyscheme-map\n    (define-keyscheme-map \"expedition-mode\" ()\n      keyscheme:cua\n      (list\n       \"C-[\" 'expedition-previous\n       \"C-]\" 'expedition-next)\n      keyscheme:emacs\n      (list\n       \"M-p\" 'expedition-previous\n       \"M-n\" 'expedition-next)))))\n\n(define-command expedition-next\n    (&key (expedition (find-submode 'expedition-mode)))\n  \"Go to the next URL in the expedition.\"\n  (if (> (length (urls expedition)) (+ 1 (index expedition)))\n      (progn\n        (incf (index expedition))\n        (ffi-buffer-load (buffer expedition)\n                         (nth (index expedition) (urls expedition))))\n      (echo \"End of expedition.\")))\n\n(define-command expedition-previous\n    (&key (expedition (find-submode 'expedition-mode)))\n  \"Go to the previous URL in the expedition.\"\n  (if (> (index expedition) 0)\n      (progn\n        (decf (index expedition))\n        (ffi-buffer-load (buffer expedition)\n                         (nth (index expedition) (urls expedition))))\n      (echo \"Start of expedition.\")))\n\n(define-command-global select-frame-expedition (&key (buffer (current-buffer)))\n  \"Run an expedition through a set of URLs selected with a rectangle.\"\n  (let* ((urls\n           (reverse\n            (prompt :prompt \"Start expedition with the following links\"\n                    :sources (make-instance 'nyxt/mode/document::frame-source\n                                            :buffer buffer\n                                            :enable-marks-p t)\n                    :after-destructor\n                    (lambda ()\n                      (with-current-buffer buffer\n                        (nyxt/mode/document::frame-element-clear))))))\n         (urls (mapcar #'quri:uri urls))\n         (buffer (make-buffer :title \"\" :url (first urls))))\n    (enable (make-instance 'expedition-mode :urls urls :buffer buffer))\n    (set-current-buffer buffer)))\n"
  },
  {
    "path": "source/mode/file-manager.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/mode/file-manager\n  (:documentation \"Package for `file-manager-mode', mode for file management\nfrom the prompt buffer.\"))\n(in-package :nyxt/mode/file-manager)\n\n(nyxt/mode/prompt-buffer::define-command-prompt directory-up (prompt-buffer)\n  \"Move one level up from the current PROMPT-BUFFER input.\"\n  (let* ((input (prompter:input prompt-buffer))\n         (path (uiop:parse-native-namestring input))\n         (parent (if (uiop:directory-pathname-p path)\n                     (uiop:pathname-parent-directory-pathname path)\n                     (uiop:pathname-directory-pathname path))))\n    (nyxt:set-prompt-buffer-input (namestring parent) prompt-buffer)))\n\n(define-mode file-manager-mode (nyxt/mode/prompt-buffer:prompt-buffer-mode)\n  \"Prompt buffer mode to manage file systems.\n\nProvides a handful of prompt buffer return actions such as deleting, renaming or\nopening files with external programs.\"\n  ((visible-in-status-p nil)\n   (keyscheme-map\n    (define-keyscheme-map \"file-manager-mode\" ()\n      keyscheme:default\n      (list\n       \"C-backspace\" 'directory-up)\n      keyscheme:emacs\n      (list\n       \"C-l\" 'directory-up)))))\n\n(export-always 'directory-elements)\n(-> directory-elements (types:pathname-designator) (list-of pathname))\n(defun directory-elements (directory)\n  \"Return list of all the files and subdirectories inside DIRECTORY.\"\n  (let ((directory (pathname directory)))\n    (append (uiop:subdirectories directory)\n            (uiop:directory-files directory))))\n\n(export-always 'recursive-directory-elements)\n(-> recursive-directory-elements (types:pathname-designator &key\n                                  (:include-directories-p boolean))\n    (list-of pathname))\n(defun recursive-directory-elements (directory &key include-directories-p)\n  \"Get contents of DIRECTORY and all of its recursive subdirectories.\nWhen INCLUDE-DIRECTORIES-P, include both directories and files,\notherwise (default) only include files.\"\n  (loop with included-directories = '()\n        with files = (directory-elements directory)\n        for directories = (sera:filter #'uiop:directory-pathname-p files)\n          then (sera:filter #'uiop:directory-pathname-p files)\n        while directories\n        do (dolist (dir directories)\n             (when include-directories-p\n               (push dir included-directories))\n             (setf files (delete dir (append files (directory-elements dir))\n                                 :test #'uiop:pathname-equal)))\n        finally (return (if include-directories-p\n                            (append files included-directories)\n                            files))))\n\n(defun current-user ()\n  #+sbcl (sb-posix:passwd-name (sb-posix:getpwuid (sb-posix:getuid))))\n\n(defun group-id (user)\n  \"Return the group ID of USER name.\"\n  #+sbcl (sb-posix:passwd-gid (sb-posix:getpwnam user)))\n\n(defun file-group-id (file)\n  #+sbcl (sb-posix:stat-gid (sb-posix:lstat file)))\n\n(export-always 'mtime)\n(defun mtime (path)\n  #+sbcl (sb-posix:stat-mtime (sb-posix:stat path)))\n\n(-> executable-p ((or types:pathname-designator) &key (:user string)) boolean)\n(defun executable-p (file &key (user (current-user)))\n  \"Return non-nil if FILE is executable for USER name.\nWhen the user is unspecified, take the current one.\"\n  (sera:true\n   (let* ((filename (uiop:native-namestring file))\n          (permissions (iolib/os:file-permissions filename)))\n     (or (and (string= (file-author file) user)\n              (member :user-exec permissions))\n         (and (= (file-group-id filename)\n                 (group-id user))\n              (member :group-exec permissions))\n         (member :other-exec permissions)))))\n\n(export-always 'executables)\n(defun executables ()\n  \"List of pathnames of user-executable programs under PATH enviroment variable.\"\n  (let ((paths (str:split \":\" (uiop:getenv \"PATH\") :omit-nulls t)))\n    (sera:filter\n     #'executable-p\n     (remove-if\n      #'uiop:hidden-pathname-p\n      (mapcar #'uiop:resolve-symlinks\n              (mappend #'uiop:directory-files paths))))))\n\n(define-class program-source (prompter:source)\n  ((prompter:name \"Programs\")\n   (prompter:constructor (executables))\n   (prompter:enable-marks-p t)\n   (prompter:filter-preprocessor #'prompter:filter-exact-matches))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:documentation \"Prompt source for user-accessible programs.\")\n  (:metaclass user-class))\n\n(defmethod prompter:object-attributes ((path pathname) (source prompter:source))\n  (declare (ignore source))\n  `((\"Path\" ,(uiop:native-namestring path) (:width 2))\n    (\"Name\" ,(if (uiop:directory-pathname-p path)\n                 (enough-namestring path (files:parent path))\n                 (pathname-name path))\n            (:width 1))\n    (\"Extension\" ,(or (files:pathname-type* path) \"\") (:width 1))\n    (\"Directory\" ,(uiop:native-namestring (files:parent path)) (:width 3))))\n\n(defun match-extension (ext)\n  (lambda (pathname)\n    (string-equal (pathname-type pathname) ext)))\n\n(defun make-file-source-preprocessor ()\n  \"Return a preprocessor that lists all files satisfying `extensions' and\n`allow-directories'.\nIt's suitable for `prompter:filter-preprocessor'.\"\n  (lambda (suggestions source input)\n    (declare (ignore suggestions))\n    (let* ((pathname (uiop:ensure-pathname (if (uiop:emptyp input)\n                                               *default-pathname-defaults*\n                                               input)))\n           (directory (uiop:pathname-directory-pathname pathname)))\n      (prompter:filter-exact-matches\n       (prompter:ensure-suggestions-list\n        source\n        (sera:filter\n         (or (path-filter source)\n             (lambda (path)\n               (or (and (uiop:directory-pathname-p path)\n                        (allow-directories source))\n                   (and (uiop:file-pathname-p path)\n                        (or (null (extensions source))\n                            (str:s-member (extensions source)\n                                          (pathname-type path)))))))\n         (directory-elements directory)))\n       source\n       input))))\n\n(define-class file-source (prompter:source)\n  ((prompter:name \"Files\")\n   (prompter:active-attributes-keys\n    '(\"Name\" \"Extension\" \"Directory\")\n    :accessor nil)\n   (prompter:filter-preprocessor (make-file-source-preprocessor))\n   (prompter:enable-marks-p t)\n   (open-file-in-new-buffer-p\n    t\n    :documentation \"Whether to open files and directories in a new buffer.\")\n   (extensions\n    nil\n    :type (list-of string)\n    :documentation \"List of extensions that are displayed.\nWhen nil, all extensions are allow-listed.\")\n   (allow-directories\n    t\n    :type boolean\n    :documentation \"Whether directories are displayed.\")\n   (path-filter\n    nil\n    :type (or null (function (pathname) boolean))\n    :documentation \"Function defining a predicate to filter files.\nIt takes a pathname and returns a boolean.  For simpler cases, use\n`allow-directories'.\")\n   (supported-media-types\n    ;; https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types\n    ;; and https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Containers\n    '(\"xhtml\" \"html\"\n      ;; Images.\n      \"gif\" \"avif\" \"jpg\" \"jpeg\" \"jfif\" \"pjpeg\" \"pjp\" \"png\" \"apng\" \"svg\" \"webp\"\n      ;; Generic container formats.\n      \"3gp\" \"aac\" \"mpg\" \"mpeg\" \"ogg\" \"mp4\" \"m4p\"\n      ;; Audio.\n      \"mp3\" \"oga\" \"m4a\"\n      ;; Video.\n      \"flac\" \"ogv\" \"m4v\" \"flv\" \"mov\" \"wmv\" \"webm\" \"mkv\"\n      ;; Documents\n      \"pdf\" \"txt\" \"org\")\n    :type (list-of string)\n    :documentation \"Media types that Nyxt opens.\nOther formats are opened relying on the OS.\")\n   (open-file-function\n    #'default-open-file-function\n    ;; TODO: Allow `data-path's?\n    :type (function ((or string pathname) &key\n                     (:supported-p boolean)\n                     (:new-buffer-p boolean)))\n    :documentation \"Function used to open files.\nTake the file name as the first argument and accept two keyword arguments:\n\n- :supported-p as to whether the file extension is supported by Nyxt (i.e. its\n  extension is one of `supported-media-types');\n- :new-buffer-p as to whether the file should be opened in a new buffer.\"))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:documentation \"Prompt source for file(s) on the disk.\")\n  (:metaclass user-class))\n\n(define-class open-file-source (file-source) ()\n  (:metaclass user-class))\n\n(defun supported-media-or-directory\n    (filename &optional (file-source (make-instance 'file-source)))\n  \"Return T if this filename's extension is a media that Nyxt can open\n(or a directory).\nSee `supported-media-types' of `file-mode'.\"\n  (or (and (uiop:directory-pathname-p filename)\n           (uiop:directory-exists-p filename))\n      (and-let* ((extension (pathname-type filename))\n                 (extensions (supported-media-types file-source)))\n        (find extension extensions :test #'string-equal))))\n\n(define-command-global edit-file-with-external-editor\n    (&optional\n     (files (prompt :prompt \"File(s) to edit\"\n                    :input (uiop:native-namestring (uiop:getcwd))\n                    :extra-modes 'nyxt/mode/file-manager:file-manager-mode\n                    :sources 'file-source)))\n  \"Edit the FILES using `external-editor-program'.\nIf FILES are not provided, prompt for them.\"\n  (echo \"Issued \\\"~{~a~^ ~}\\\" to edit ~s.\"\n        (external-editor-program *browser*) files)\n  (with-protect\n      (\"Failed editing: ~a. See `external-editor-program' slot.\" :condition)\n    (uiop:launch-program `(,@(external-editor-program *browser*)\n                           ,@(mapcar #'uiop:native-namestring files)))))\n\n(defmethod initialize-instance :after ((source open-file-source) &key)\n  (setf (slot-value source 'prompter:actions-on-return)\n        (append\n         (list\n          (lambda-command open-file* (files)\n            \"Open files with `open-file-function'.\"\n            ;; Open first file according to `open-file-in-new-buffer-p'\n            (let ((file (first files)))\n              (funcall (open-file-function source)\n                       file\n                       :new-buffer-p (open-file-in-new-buffer-p source)\n                       :supported-p (supported-media-or-directory file source)))\n            ;; Open the rest of the files in new buffers unconditionally.\n            (dolist (file (rest files))\n              (funcall (open-file-function source)\n                       file\n                       :new-buffer-p t\n                       :supported-p (supported-media-or-directory file source))))\n          (lambda-command delete-file* (files)\n            \"Delete files.\"\n            (mapcar #'delete-file files))\n          (lambda-command rename-file* (files)\n            \"Rename the first chosen file.\"\n            (let* ((file (first files))\n                   (name (files:basename file)))\n              (rename-file file\n                           (prompt1 :prompt (format nil \"New name for ~a\" name)\n                                    :sources 'prompter:raw-source\n                                    :input name))))\n          (lambda-command edit-file-with-external-editor* (files)\n            \"Edit files in external editor.\"\n            (edit-file-with-external-editor files))\n          ;; TODO: File/directory copying.\n          (lambda-command open-with* (files)\n            \"Open files with the selected program.\"\n            (let ((program (prompt1 :prompt \"Choose program\"\n                                    :sources 'program-source)))\n              (uiop:launch-program\n               (cons (uiop:native-namestring program)\n                     (mapcar #'uiop:native-namestring files)))))))))\n\n(export-always 'default-open-file-function)\n(defun default-open-file-function (filename &key supported-p new-buffer-p)\n  \"Open FILENAME in Nyxt if supported, or externally otherwise.\nFILENAME is the full path of the file (or directory).\n\nSee `supported-media-types' to customize the file types that are opened in\nNyxt and those that are opened externally.\n\nNEW-BUFFER-P defines whether the file/directory is opened in a new buffer.\nSUPPORTED-P says whether the file can be opened by Nyxt.\n\nCan be used as a `open-file-function'.\"\n  (handler-case\n      (cond\n        (supported-p\n         (let ((file-url (quri:make-uri-file :path filename)))\n           (if new-buffer-p\n               (make-buffer-focus :url file-url)\n               (ffi-buffer-load (current-buffer) file-url))))\n        (*open-program*\n         (let ((process (uiop:launch-program\n                         (list *open-program*\n                               (uiop:native-namestring filename))\n                         :error-output :stream)))\n           (nyxt:echo \"Opening ~s with ~s.\" filename *open-program*)\n           (run-thread \"file opener\"\n             (let ((status (uiop:wait-process process)))\n               (unless (= 0 status)\n                 (echo-warning \"When opening file ~s with ~s : ~a\"\n                               filename\n                               *open-program*\n                               (alex:read-stream-content-into-string\n                                (uiop:process-info-error-output process))))))))\n        (t (nyxt:echo \"Cannot open ~s with an external program.\" filename)))\n    ;; We can probably signal something and display a notification.\n    (error (c) (log:error \"Opening ~a: ~a~&\" filename c))))\n\n(define-command-global open-file\n    (&key (default-directory\n           (if (quri:uri-file-p (url (current-buffer)))\n               (uiop:pathname-directory-pathname\n                (quri:url-decode\n                 (quri:uri-path (url (current-buffer)))))\n               *default-pathname-defaults*)))\n  \"Open a file from the filesystem.\n\nThe user is prompted with the prompt buffer, files are browsable with\nfuzzy suggestion.\n\nDEFAULT-DIRECTORY specifies which directory to start from. Defaults to user home\ndirectory.\n\nBy default, it uses the `xdg-open' command. The user can override the\n`open-file-function' of `file-source' which takes the filename (or\ndirectory name) as parameter.\n\n`file-source' also has `supported-media-types'. You can append new types to\nit. Every type in `supported-media-types' will be opened directly in Nyxt.\"\n  (prompt :prompt \"Open file\"\n          :extra-modes 'file-manager-mode\n          :input (uiop:native-namestring default-directory)\n          :sources 'open-file-source))\n\n(define-command-global download-open-file ()\n  \"Open file in Nyxt or externally.\"\n  (open-file :default-directory\n             (files:expand (download-directory (current-buffer)))))\n"
  },
  {
    "path": "source/mode/force-https.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/mode/force-https\n  (:documentation \"Package for `force-https-mode', mode to redirect all HTTP traffic to HTTPS.\"))\n(in-package :nyxt/mode/force-https)\n\n;; TODO: Add style to loop help page?\n(defun https->http-loop-help (buffer url) ; TODO: Factor with tls-help?\n  \"This function is invoked upon HTTPS->HTTP->HTTPS loops to give users\nhelp on how to proceed.\"\n  (setf (nyxt::status buffer) :failed)\n  (nyxt::html-set\n   (spinneret:with-html-string\n     (:nstyle (style buffer))\n     (:h1 (format nil \"HTTPS → HTTP loop: ~a\" (render-url url)))\n     (:p \"The HTTPS address you are trying to visit redirects to HTTP while the \"\n         (:code \"force-https-mode\") \" is on.\")\n     (:p \"Since HTTP connections are not secure,\"\n         \" it's not recommended to proceed if you don't trust the target host.\")\n     (:p \" If you really want to proceed, you can disable \" (:code \"force-https-mode\")\n         \" temporarily.\"))\n   buffer))\n\n(define-mode force-https-mode ()\n  \"Redirect HTTP traffic to HTTPS.\n\nNotice that it may break websites whose certificates are not known, or those who\ndon't have an HTTPS version.\n\nTo permanently bypass the \\\"Unacceptable TLS Certificate\\\" error:\n\\(setf nyxt/mode/certificate-exception:*default-certificate-exceptions*\n       '(\\\"your.unacceptable.cert.website\\\"))\n\nExample:\n\n\\(define-configuration web-buffer\n  ((default-modes (append '(force-https-mode) %slot-default%))))\"\n  ((previous-url (quri:uri \"\"))))\n\n(defun force-https-handler (request-data)\n  \"Impose HTTPS on any link with HTTP scheme.\"\n  (let ((url (url request-data))\n        (mode (find-submode 'force-https-mode (buffer request-data))))\n    (cond\n      ((string/= (quri:uri-scheme url) \"http\")\n       request-data)\n      ((quri:uri= (previous-url mode) url)\n       (log:info \"HTTPS->HTTP redirection loop detected, stop forcing '~a'\" url)\n       (https->http-loop-help (buffer request-data) url)\n       nil)\n      (t\n       ;; Warning: Copy URL, else next line would modify the scheme of\n       ;; `previous-url' as well.\n       (setf (previous-url mode) (quri:copy-uri url))\n       (log:info \"HTTPS enforced on '~a'\" (render-url url))\n       ;; FIXME: http-only websites are displayed as \"https://foo.bar\"\n       ;; FIXME: some websites (e.g., go.com) simply time-out\n       (setf (url request-data)\n             (quri:copy-uri url :scheme \"https\"\n                                :port (quri.port:scheme-default-port \"https\")))\n       request-data))))\n\n(defmethod enable ((mode force-https-mode) &key)\n  (hooks:add-hook (request-resource-hook (buffer mode)) 'force-https-handler))\n\n(defmethod disable ((mode force-https-mode) &key)\n  (hooks:remove-hook (request-resource-hook (buffer mode)) 'force-https-handler))\n\n(defmethod on-signal-load-finished ((mode force-https-mode) url title)\n  (declare (ignore url title))\n  (when (eq (slot-value (buffer mode) 'nyxt::status) :finished)\n    (setf (previous-url mode) (quri:uri \"\")))\n  nil)\n"
  },
  {
    "path": "source/mode/help.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/mode/help\n  (:documentation \"Mode to enhance navigation on internal documentation pages.\"))\n(in-package :nyxt/mode/help)\n\n(define-mode help-mode ()\n  \"Mode for help and documentation pages.\n\nUseful to enable on Nyxt help pages (such as `manual' or `describe-*') to\nprovide convenient navigation keybindings.  For instance, \\\"s\\\" becomes bound\n`nyxt/mode/search-buffer:search-buffer'.\"\n  ((keyscheme-map\n    (define-keyscheme-map \"help-mode\" ()\n      keyscheme:default\n      (list\n       \"q\" 'delete-current-buffer\n       \"n\" 'nyxt/mode/document:scroll-down\n       \"p\" 'nyxt/mode/document:scroll-up\n       \"m\" 'nyxt/mode/document:jump-to-heading\n       \"s\" 'nyxt/mode/search-buffer:search-buffer\n       \"?\" (sym:resolve-symbol :describe-bindings :command)))))\n  (:toggler-command-p nil))\n"
  },
  {
    "path": "source/mode/hint-prompt-buffer.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/mode/hint-prompt-buffer\n  (:documentation \"Package for `hint-prompt-buffer-mode', prompt buffer mode for element hints.\"))\n(in-package :nyxt/mode/hint-prompt-buffer)\n\n(define-command scroll-to-hint (&key (buffer (current-buffer)))\n  \"Display the current hint and center it in BUFFER.\"\n  (with-current-buffer buffer\n    (nyxt/mode/hint:highlight-current-hint :element (current-suggestion-value)\n                                           :scroll t)))\n\n(define-mode hint-prompt-buffer-mode (nyxt/mode/prompt-buffer:prompt-buffer-mode)\n  \"`prompt-buffer' mode for element hinting.\n\nProvides keybindings `scroll-to-hint'.\"\n  ((visible-in-status-p nil)\n   (keyscheme-map\n    (define-keyscheme-map \"hint-prompt-buffer-mode\" ()\n      keyscheme:default\n      (list\n       \"C-l\" 'scroll-to-hint)))))\n"
  },
  {
    "path": "source/mode/hint.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/mode/hint\n  (:documentation \"Package for element hints infrastructure and `hint-mode'.\n\nExposes the APIs below:\n- `query-hints' as the main driver for hinting procedures.\n- `hint-source' for `prompt-buffer' interaction.\"))\n(in-package :nyxt/mode/hint)\n\n(define-mode hint-mode ()\n  \"Interact with elements by typing a short character sequence.\"\n  ((visible-in-status-p nil)\n   (hinting-type\n    :emacs\n    :type (member :emacs :vi)\n    :documentation \"Set the hinting mechanism.\nIn :emacs, hints are computed for the whole page, and the usual `prompt-buffer'\nfacilities are available.\nIn :vi, the `prompt-buffer' is collapsed to the input area, hints are computed\nin viewport only and they're followed when user input matches the hint string.\")\n   (show-hint-scope-p\n    nil\n    :type boolean\n    :documentation \"Whether `style' is applied to the hinted element.\nWhen t, the hinted element is, by default, shown its scope by applying a\nbackground color.\")\n   (hints-alphabet\n    \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\"\n    :type string\n    :documentation \"The alphabet (charset) to use for hints.\nOrder matters -- the ones that go first are more likely to appear more often and\nto index the top of the page.\")\n   (hints-selector\n    \"a, button, input, textarea, details, select\"\n    :type string\n    :documentation \"The elements to be hinted.\nThe hints-selector syntax is that of CLSS, and broadly, that of CSS. Use it to\ndefine which elements are picked up by element hinting.\n\nFor instance, to include images:\n\n    a, button, input, textarea, details, select, img:not([alt=\\\"\\\"])\")\n   (x-translation\n    0\n    :type integer\n    :documentation \"The horizontal translation as a percentage of the hint's size.\nA positive value shifts to the right.\")\n   (y-translation\n    0\n    :type integer\n    :documentation \"The vertical translation as a percentage of the hint's size.\nA positive value shifts to the bottom.\")\n   (x-placement\n    :left\n    :type (member :left :right)\n    :documentation \"The horizontal placement of the hints: either `:left' or `:right'.\")\n   (keyscheme-map\n    (define-keyscheme-map \"hint-mode\" ()\n      keyscheme:cua\n      (list\n       \"C-j\" 'follow-hint\n       \"C-J\" 'follow-hint-new-buffer\n       \"C-u C-j\" 'follow-hint-new-buffer-focus\n       \"M-c h\" 'copy-hint-url)\n      keyscheme:emacs\n      (list\n       \"M-g\" 'follow-hint\n       \"M-G\" 'follow-hint-new-buffer\n       \"C-u M-g\" 'follow-hint-new-buffer-focus\n       \"C-x C-w\" 'copy-hint-url)\n      keyscheme:vi-normal\n      (list\n       \"f\" 'follow-hint\n       \"; f\" 'follow-hint-new-buffer\n       \"F\" 'follow-hint-new-buffer-focus)))))\n\n(defmethod style ((mode hint-mode))\n  \"The style of the hint overlays.\"\n  (theme:themed-css (theme *browser*)\n    `(\".nyxt-hint\"\n      :background-color ,theme:background-color-\n      :color ,theme:on-background-color\n      :font-family ,theme:monospace-font-family\n      :font-size \".85rem\"\n      :transform ,(format nil \"translate(~a%,~a%)\"\n                          (+ (x-translation mode)\n                             (if (eq (x-placement mode) :right) -100 0))\n                          (y-translation mode))\n      :padding \"0px 0.3em\"\n      :border-color ,theme:primary-color+\n      :border-radius \"4px\"\n      :border-width \"2px\"\n      :border-style \"solid\"\n      :z-index #.(1- (expt 2 31)))\n    `(\".nyxt-hint.nyxt-mark-hint\"\n      :background-color ,theme:secondary-color\n      :color ,theme:on-secondary-color\n      :font-weight \"bold\")\n    `(\".nyxt-hint.nyxt-current-hint\"\n      :background-color ,theme:action-color\n      :color ,theme:on-action-color)\n    '(\".nyxt-hint.nyxt-match-hint\"\n      :padding \"0px\"\n      :border-style \"none\"\n      :opacity \"0.5\")\n    `(\".nyxt-element-hint\"\n      :background-color ,theme:action-color)))\n\n(define-configuration document-buffer\n  ((default-modes (cons 'hint-mode %slot-value%))))\n\n(define-parenscript-async hint-elements (hints)\n  (defun create-hint-element (hint)\n    (let ((hint-element (ps:chain document (create-element \"span\"))))\n      (setf (ps:@ hint-element class-name) \"nyxt-hint\"\n            (ps:@ hint-element id) (+ \"nyxt-hint-\" hint)\n            (ps:@ hint-element text-content) hint)\n      hint-element))\n\n  (defun set-hint-element-style (hint-element hinted-element)\n    (let* ((right-x-alignment-p (eq (ps:lisp (x-placement (find-submode 'hint-mode)))\n                                    :right))\n           (rect (ps:chain hinted-element (get-bounding-client-rect))))\n      (setf (ps:@ hint-element style position) \"absolute\"\n            (ps:@ hint-element style top) (+ (ps:@ window scroll-y) (ps:@ rect top) \"px\")\n            (ps:@ hint-element style left) (+ (ps:@ window scroll-x) (ps:@ rect left)\n                                              (when right-x-alignment-p (ps:@ rect width)) \"px\"))))\n\n  (defun create-hint-overlay (hinted-element hint)\n    \"Create a DOM element to be used as a hint.\"\n    (let ((hint-element (create-hint-element hint)))\n      (set-hint-element-style hint-element hinted-element))\n    hint-element)\n\n  (let* ((hints-parent (ps:chain document (create-element \"div\")))\n         (shadow (ps:chain hints-parent (attach-shadow (ps:create mode \"open\"))))\n         (style (ps:new (|CSSStyleSheet|)))\n         (hints (ps:lisp (list 'quote hints)))\n         (i 0))\n    (dolist (hinted-element (nyxt/ps:rqsa document \"[nyxt-hintable]\"))\n      (let ((hint (aref hints i)))\n        (ps:chain hinted-element (set-attribute \"nyxt-hint\" hint))\n        (ps:chain shadow (append-child (create-hint-overlay hinted-element hint)))\n        (when (ps:lisp (show-hint-scope-p (find-submode 'hint-mode)))\n          (ps:chain hinted-element class-list (add \"nyxt-element-hint\")))\n        (setf i (1+ i))))\n    (ps:chain style (replace-sync (ps:lisp (style (find-submode 'hint-mode)))))\n    (setf (ps:chain shadow adopted-style-sheets) (array style))\n    (setf (ps:@ hints-parent id) \"nyxt-hints\"\n          (ps:@ hints-parent style) \"all: unset !important;\")\n    ;; Unless the hints root is a child of body, zooming the page breaks the\n    ;; hint positioning.\n    (ps:chain document body (append-child hints-parent))\n    ;; Don't return a value.  Only the side-effects are of importance.\n    nil))\n\n(-> select-from-alphabet (t alex:non-negative-integer string) (values string &optional))\n(defun select-from-alphabet (code subsequence-length alphabet)\n  (let ((exponents (nreverse (loop for pow below subsequence-length\n                                   collect (expt (length alphabet) pow)))))\n    (coerce (loop for exp in exponents\n                  for quotient = (floor (/ code exp))\n                  collect (aref alphabet quotient)\n                  do (decf code (* quotient exp)))\n            'string)))\n\n(-> generate-hints (alex:non-negative-integer) (list-of string))\n(defun generate-hints (length)\n  (let ((alphabet (hints-alphabet (find-submode 'hint-mode))))\n    (cond\n      ((sera:single alphabet)\n       (loop for i from 1 to length\n             collect (select-from-alphabet 0 i alphabet)))\n      (t\n       (loop for i below length\n             collect (select-from-alphabet i\n                                           (max (ceiling (log length (length alphabet)))\n                                                1)\n                                           alphabet))))))\n\n(define-parenscript set-hintable-attribute (selector)\n  (ps:dolist (element (nyxt/ps:rqsa document (ps:lisp selector)))\n    (if (ps:lisp (eq :vi (hinting-type (find-submode 'hint-mode))))\n        (unless (nyxt/ps:element-overlapped-p element)\n          (ps:chain element (set-attribute \"nyxt-hintable\" \"\")))\n        (ps:chain element (set-attribute \"nyxt-hintable\" \"\")))))\n\n(define-parenscript remove-hintable-attribute ()\n  (ps:dolist (element (nyxt/ps:rqsa document \"[nyxt-hintable]\"))\n    (ps:chain element (remove-attribute \"nyxt-hintable\"))))\n\n(defun add-hints (&key selector (buffer (current-buffer)))\n  (set-hintable-attribute selector)\n  (update-document-model :buffer buffer)\n  (loop with hintable-elements = (sera:filter\n                                  (lambda (el) (plump:attribute el \"nyxt-identifier\"))\n                                  (clss:select \"[nyxt-hintable]\" (document-model buffer :use-cached-p t)))\n        with hints = (generate-hints (length hintable-elements))\n        for elem across hintable-elements\n        for hint in hints\n        initially (hint-elements hints)\n        do (plump:set-attribute elem \"nyxt-hint\" hint)\n        collect elem))\n\n(define-parenscript-async remove-hint-elements ()\n  (ps:let ((hints-parent (nyxt/ps:qs-id document \"nyxt-hints\")))\n    (ps:when hints-parent\n      (ps:chain hints-parent (remove))))\n  (when (ps:lisp (show-hint-scope-p (find-submode 'hint-mode)))\n    (ps:dolist (element (nyxt/ps:rqsa document \".nyxt-element-hint\"))\n      (ps:chain element class-list (remove \"nyxt-element-hint\")))))\n\n(defun remove-hints (&key (buffer (current-buffer)))\n  (remove-hint-elements)\n  (remove-hintable-attribute)\n  (update-document-model :buffer buffer))\n\n(export-always 'identifier)\n(defmethod identifier ((element plump:element))\n  \"ELEMENT's on-page identifier (constructed from `hint-alphabet' characters.)\"\n  (plump:attribute element \"nyxt-hint\"))\n\n(export-always 'highlight-current-hint)\n(define-parenscript highlight-current-hint (&key element scroll)\n  \"Accent the hint for the ELEMENT to be distinguishable from other hints.\nIf SCROLL (default to NIL), scroll the hint into view.\"\n  (let* ((shadow (ps:@ (nyxt/ps:qs document \"#nyxt-hints\") shadow-root))\n         (%element (nyxt/ps:qs shadow\n                               (ps:lisp (str:concat \"#nyxt-hint-\" (identifier element))))))\n    (when %element\n      (unless (ps:chain %element class-list (contains \"nyxt-current-hint\"))\n        ;; There should be, at most, a unique element with the\n        ;; \"nyxt-current-hint\" class.\n        ;; querySelectAll, unlike querySelect, handles the case when none are\n        ;; found.\n        (ps:dolist (current-hint (nyxt/ps:qsa shadow \".nyxt-current-hint\"))\n          (ps:chain current-hint class-list (remove \"nyxt-current-hint\"))))\n      (ps:chain %element class-list (add \"nyxt-current-hint\"))\n      (when (ps:lisp scroll)\n        (ps:chain %element (scroll-into-view (ps:create block \"center\")))))))\n\n(define-parenscript-async set-hint-visibility (hint state)\n  \"Set visibility STATE of HINT element.\n\nConsult https://developer.mozilla.org/en-US/docs/Web/CSS/visibility.\"\n  (let* ((shadow (ps:@ (nyxt/ps:qs document \"#nyxt-hints\") shadow-root))\n         (el (nyxt/ps:qs shadow (ps:lisp (str:concat \"#nyxt-hint-\" (identifier hint))))))\n    (when el (setf (ps:@ el style \"visibility\") (ps:lisp state)))))\n\n(define-parenscript-async dim-hint-prefix (hint prefix-length)\n  \"Dim the first PREFIX-LENGTH characters of HINT element.\"\n  (let* ((shadow (ps:@ (nyxt/ps:qs document \"#nyxt-hints\") shadow-root))\n         (el (nyxt/ps:qs shadow (ps:lisp (str:concat \"#nyxt-hint-\" (identifier hint))))))\n    (when el\n      (let ((span-element (ps:chain document (create-element \"span\"))))\n        (setf (ps:@ span-element class-name) \"nyxt-hint nyxt-match-hint\"\n              (ps:@ span-element style font-size) \"inherit\"\n              (ps:@ span-element text-content) (ps:lisp (subseq (identifier hint)\n                                                                0\n                                                                prefix-length))\n              (ps:chain el inner-h-t-m-l) (+ (ps:@ span-element outer-h-t-m-l)\n                                             (ps:lisp (subseq (identifier hint)\n                                                              prefix-length))))))))\n\n(define-class hint-source (prompter:source)\n  ((prompter:name \"Hints\")\n   (prompter:actions-on-current-suggestion-enabled-p t)\n   (prompter:filter-preprocessor\n    (if (eq :vi (hinting-type (find-submode 'hint-mode)))\n        (lambda (suggestions source input)\n          (declare (ignore source))\n          (loop for suggestion in suggestions\n                for hint = (prompter:value suggestion)\n                for hinted-element-id = (nyxt/dom:get-nyxt-id hint)\n                if (str:starts-with-p input\n                                      (prompter:attributes-default suggestion)\n                                      :ignore-case t)\n                  do (set-hint-visibility hint \"visible\")\n                  and do (when (show-hint-scope-p (find-submode 'hint-mode))\n                           (ps-eval\n                             (nyxt/ps:add-class-nyxt-id hinted-element-id\n                                                        \"nyxt-element-hint\")))\n                  and do (dim-hint-prefix hint (length input))\n                  and collect suggestion\n                else do (set-hint-visibility hint \"hidden\")\n                     and do (when (show-hint-scope-p (find-submode 'hint-mode))\n                              (ps-eval\n                                (nyxt/ps:remove-class-nyxt-id hinted-element-id\n                                                              \"nyxt-element-hint\")))))\n        #'prompter:delete-inexact-matches))\n   (prompter:filter\n    (if (eq :vi (hinting-type (find-submode 'hint-mode)))\n        (lambda (suggestion source input)\n          (declare (ignore source))\n          (str:starts-with-p input\n                             (prompter:attributes-default suggestion)\n                             :ignore-case t))\n        #'prompter:fuzzy-match))\n   (prompter:filter-postprocessor\n    (lambda (suggestions source input)\n      (declare (ignore source))\n      (multiple-value-bind (matching-hints other-hints)\n          (sera:partition\n           (lambda (element)\n             (str:starts-with-p input (plump:attribute element \"nyxt-hint\") :ignore-case t))\n           suggestions\n           :key #'prompter:value)\n        (append matching-hints other-hints))))\n   (prompter:actions-on-current-suggestion\n    (when (eq :emacs (hinting-type (find-submode 'hint-mode)))\n      (lambda-command highlight-current-hint* (suggestion)\n        \"Highlight hint.\"\n        (highlight-current-hint :element suggestion\n                                :scroll nil))))\n   (prompter:actions-on-marks\n    (lambda (marks)\n      (let ((%marks (mapcar (lambda (mark) (str:concat \"#nyxt-hint-\" (identifier mark)))\n                            marks)))\n        (ps-eval\n          (let ((shadow (ps:@ (nyxt/ps:qs document \"#nyxt-hints\") shadow-root)))\n            (dolist (marked (nyxt/ps:qsa shadow \".nyxt-mark-hint\"))\n              (ps:chain marked class-list (remove \"nyxt-mark-hint\")))\n            (dolist (mark (ps:lisp (list 'quote %marks)))\n              (ps:chain (nyxt/ps:qs shadow mark) class-list (add \"nyxt-mark-hint\"))))))))\n   (prompter:actions-on-return\n    (list 'identity\n          (lambda-command click* (elements)\n            (dolist (element (rest elements))\n              (nyxt/dom:click-element element))\n            (nyxt/dom:click-element (first elements))\n            nil)\n          (lambda-command focus* (elements)\n            (dolist (element (rest elements))\n              (nyxt/dom:focus-select-element element))\n            (nyxt/dom:focus-select-element (first elements))\n            nil)))))\n\n(export-always 'query-hints)\n(defun query-hints (prompt function\n                    &key (enable-marks-p t)\n                         (selector (hints-selector (find-submode 'hint-mode))))\n  \"Prompt for elements matching SELECTOR, hinting them visually.\nENABLE-MARKS-P defines whether several elements can be chosen.\nPROMPT is the text to show while prompting for hinted elements.\nFUNCTION is the action to perform on the selected elements.\"\n  (when-let*\n      ((buffer (current-buffer))\n       (result (prompt\n                :prompt prompt\n                ;; TODO: No need to find the symbol if we move this code (and\n                ;; the rest) to the hint-mode package.\n                :extra-modes (list (sym:resolve-symbol :hint-prompt-buffer-mode :mode))\n                :auto-return-p (eq :vi (hinting-type (find-submode 'hint-mode)))\n                :history nil\n                :height (if (eq :vi (hinting-type (find-submode 'hint-mode)))\n                            :fit-to-prompt\n                            :default)\n                :hide-suggestion-count-p (eq :vi (hinting-type (find-submode 'hint-mode)))\n                :sources (make-instance 'hint-source\n                                        :enable-marks-p enable-marks-p\n                                        :constructor\n                                        (lambda (source)\n                                          (declare (ignore source))\n                                          (add-hints :selector selector)))\n                :after-destructor (lambda () (with-current-buffer buffer (remove-hints))))))\n    (funcall function result)))\n\n(defmethod prompter:object-attributes :around ((element plump:element) (source hint-source))\n  `(,@(when (plump:attribute element \"nyxt-hint\")\n        `((\"Hint\" ,(plump:attribute element \"nyxt-hint\") (:width 1))))\n    ;; Ensure that all of Body and URL are there, even if empty.\n    ,@(loop with attributes = (call-next-method)\n            for attr in '(\"Body\" \"URL\")\n            for (same-attr val) = (assoc attr attributes :test 'string=)\n            if same-attr\n              collect `(,same-attr ,val (:width 3))\n            else collect `(,attr \"\" (:width 3)))\n    (\"Type\" ,(str:capitalize (str:string-case\n                                 (plump:tag-name element)\n                               (\"a\" \"link\")\n                               (\"img\" \"image\")\n                               (otherwise (plump:tag-name element))))\n            (:width 1))))\n\n(defmethod prompter:object-attributes ((input nyxt/dom:input-element) (source prompter:source))\n  (declare (ignore source))\n  (when (nyxt/dom:body input)\n    `((\"Body\" ,(str:shorten 80 (nyxt/dom:body input))))))\n\n(defmethod prompter:object-attributes ((textarea nyxt/dom:textarea-element) (source prompter:source))\n  (declare (ignore source))\n  (when (nyxt/dom:body textarea)\n    `((\"Body\" ,(str:shorten 80 (nyxt/dom:body textarea))))))\n\n(defmethod prompter:object-attributes ((a nyxt/dom:a-element) (source prompter:source))\n  (declare (ignore source))\n  (append\n   (and-let* (((plump:has-attribute a \"href\"))\n              (url-string (plump:attribute a \"href\")))\n     `((\"URL\" ,url-string)))\n   (when (nyxt/dom:body a)\n     `((\"Body\" ,(str:shorten 80 (nyxt/dom:body a)))))))\n\n(defmethod prompter:object-attributes ((button nyxt/dom:button-element) (source prompter:source))\n  (declare (ignore source))\n  (when (nyxt/dom:body button)\n    `((\"Body\" ,(str:shorten 80 (nyxt/dom:body button))))))\n\n(defmethod prompter:object-attributes ((details nyxt/dom:details-element) (source prompter:source))\n  (declare (ignore source))\n  (when (nyxt/dom:body details)\n    `((\"Body\" ,(str:shorten 80 (nyxt/dom:body details))))))\n\n(defmethod prompter:object-attributes ((select nyxt/dom:select-element) (source prompter:source))\n  (declare (ignore source))\n  `((\"Body\" ,(str:shorten 80 (nyxt/dom:body select)))))\n\n(defmethod prompter:object-attributes ((option nyxt/dom:option-element) (source prompter:source))\n  (declare (ignore source))\n  `((\"Body\" ,(nyxt/dom:body option))))\n\n(defmethod prompter:object-attributes ((img nyxt/dom:img-element) (source hint-source))\n  (append\n   (and-let* (((plump:has-attribute img \"href\"))\n              (url-string (plump:attribute img \"href\")))\n     `((\"URL\" ,url-string)))\n   (when (nyxt/dom:body img)\n     `((\"Body\" ,(str:shorten 80 (nyxt/dom:body img)))))))\n\n(defmethod %follow-hint ((element plump:element))\n  (nyxt/dom:click-element element))\n\n(defmethod %follow-hint ((a nyxt/dom:a-element))\n  (ffi-buffer-load (current-buffer) (url a)))\n\n(defmethod %follow-hint ((input nyxt/dom:input-element))\n  (str:string-case (plump:attribute input \"type\")\n                   (\"button\" (nyxt/dom:click-element input))\n                   (\"radio\" (nyxt/dom:check-element input))\n                   (\"checkbox\" (nyxt/dom:check-element input))\n                   (otherwise (nyxt/dom:focus-select-element input))))\n\n(defmethod %follow-hint ((textarea nyxt/dom:textarea-element))\n  (nyxt/dom:focus-select-element textarea))\n\n(defmethod %follow-hint ((details nyxt/dom:details-element))\n  (nyxt/dom:toggle-details-element details))\n\n(define-class options-source (prompter:source)\n  ((prompter:name \"Options\")\n   (prompter:filter-preprocessor #'prompter:filter-exact-matches))\n  (:export-class-name-p t)\n  (:documentation \"Prompt source for select tag options.\"))\n\n(defmethod %follow-hint ((select nyxt/dom:select-element))\n  (and-let* ((options (coerce (clss:select \"option\" select) 'list))\n             (values (prompt :prompt \"Value to select\"\n                             :sources (make-instance 'options-source\n                                                     :constructor options\n                                                     :enable-marks-p\n                                                     (plump:attribute select \"multiple\")))))\n    (dolist (option (mapcar (rcurry #'find options :test #'equalp) values))\n      (nyxt/dom:select-option-element option select))))\n\n(defmethod %follow-hint-new-buffer-focus ((a nyxt/dom:a-element))\n  (make-buffer-focus :url (url a)))\n\n(defmethod %follow-hint-new-buffer-focus ((element plump:element))\n  (%follow-hint element))\n\n(defmethod %follow-hint-new-buffer ((a nyxt/dom:a-element))\n  (make-buffer :url (url a) :load-url-p t))\n\n(defmethod %follow-hint-new-buffer ((element plump:element))\n  (%follow-hint element))\n\n(defmethod %copy-hint-url ((a nyxt/dom:a-element))\n  (ffi-buffer-copy (current-buffer) (render-url (url a))))\n\n(defmethod %copy-hint-url ((img nyxt/dom:img-element))\n  (ffi-buffer-copy (current-buffer) (render-url (url img))))\n\n(defmethod %copy-hint-url ((element plump:element))\n  (echo \"Unsupported operation for <~a> hint: can't copy hint URL.\"\n        (plump:tag-name element)))\n\n(define-command follow-hint ()\n  \"Follow the top element hint selection in the current buffer.\"\n  (query-hints \"Select elements\"\n               (lambda (results)\n                 (%follow-hint (first results))\n                 (mapcar #'%follow-hint-new-buffer (rest results)))))\n\n(define-command follow-hint-new-buffer ()\n  \"Like `follow-hint', but selection is handled in background buffers.\"\n  (query-hints \"Select elements\"\n               (lambda (result)\n                 (mapcar #'%follow-hint-new-buffer result))))\n\n(define-command follow-hint-new-buffer-focus ()\n  \"Like `follow-hint-new-buffer', but switch to the top background buffer.\"\n  (query-hints \"Select elements\"\n               (lambda (result)\n                 (%follow-hint-new-buffer-focus (first result))\n                 (mapcar #'%follow-hint-new-buffer (rest result)))))\n\n(define-command copy-hint-url ()\n  \"Save the element hint's URL to the clipboard.\"\n  (query-hints \"Select element\"\n               (lambda (result) (%copy-hint-url (first result)))\n               :enable-marks-p nil\n               :selector \"a\"))\n"
  },
  {
    "path": "source/mode/history-migration.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/mode/history-migration\n  (:documentation \"Package for `history-migration-mode', mode to import history from other\nbrowsers.\"))\n(in-package :nyxt/mode/history-migration)\n\n(define-mode history-migration-mode ()\n  \"Mode for importing history from other browsers.\"\n  ((visible-in-status-p nil))\n  (:toggler-command-p nil))\n\n(define-class external-browser-history-source (prompter:source)\n  ((prompter:name \"History files\")\n   (browser-lambda :accessor browser-lambda :initarg :browser-lambda)\n   (prompter:constructor\n    (lambda (source)\n      (echo \"Searching for history file. This may take some time.\")\n      (or (let ((results '()))\n            (uiop:collect-sub*directories\n             (user-homedir-pathname)\n             (constantly t)\n             (lambda (subdir)\n               (equal (iolib/os:file-kind subdir) :directory))\n             (lambda (subdir)\n               (let ((browser-history-file (funcall* (browser-lambda source) subdir)))\n                 (when browser-history-file\n                   (push browser-history-file results)))))\n            results)\n          (echo-warning \"No history files found.\"))))))\n\n(defmacro define-history-import-command (name docstring &key sql-query file-path)\n  \"Shorthand to define a global command for importing history from a browser.\nMake sure that the sql-query is a SELECT statement that selects:\n - url\n - title\n - last-access (unix time in seconds)\n - visits\nOr the equivalent columns for the browser in question.\"\n  `(define-command-global ,name ()\n     ,docstring\n     (let ((db-path ,file-path))\n       (files:with-file-content (history (history-file *browser*)\n                                 :default '())\n         (handler-bind ((sqlite:sqlite-error\n                          (lambda (_)\n                            (declare (ignore _))\n                            (echo-warning \"Please close the browser you wish to import history from before running this command.\")\n                            (invoke-restart 'abort))))\n           (sqlite:with-open-database (db db-path)\n             (echo \"Importing history from ~a.\" db-path)\n             (loop for (url title last-access visits) in (sqlite:execute-to-list db ,sql-query)\n                   do (unless (url-empty-p url)\n                        (vector-push-extend (make-instance 'history-entry\n                                                           :url url\n                                                           :title title)\n                                            (history-vector *browser*))))\n             (setf history (history-vector *browser*))\n             (echo \"History import finished.\")))))))\n\n(define-history-import-command import-history-from-firefox\n  \"Import history from Mozilla Firefox.\"\n  :sql-query \"SELECT url, title, last_visit_date/1000000, visit_count FROM moz_places WHERE last_visit_date not null\"\n  :file-path (prompt1 :prompt \"Choose Mozilla Firefox places.sqlite file\"\n                      :sources (make-instance 'external-browser-history-source\n                                              :browser-lambda (lambda (subdir)\n                                                                (when (uiop:file-exists-p (uiop:merge-pathnames* \"places.sqlite\" subdir))\n                                                                  (uiop:merge-pathnames* \"places.sqlite\" subdir))))))\n\n(define-history-import-command import-history-from-google-chrome\n  \"Import history from Google Chrome.\"\n  :sql-query \"SELECT url, title, last_visit_time/1000000-11644473600, visit_count FROM urls\"\n  :file-path (prompt1 :prompt \"Choose Google Chrome History file\"\n                      :sources (make-instance 'external-browser-history-source\n                                              :browser-lambda (lambda (subdir)\n                                                                (when (and (string= \"google-chrome\"\n                                                                                    (nfiles:basename (nfiles:parent subdir)))\n                                                                           (uiop:file-exists-p (uiop:merge-pathnames* \"History\" subdir)))\n                                                                  (uiop:merge-pathnames* \"History\" subdir))))))\n\n(define-history-import-command import-history-from-chromium\n  \"Import history from Chromium.\"\n  :sql-query \"SELECT url, title, last_visit_time/1000000-11644473600, visit_count FROM urls\"\n  :file-path (prompt1 :prompt \"Choose Chromium History file\"\n                      :sources (make-instance 'external-browser-history-source\n                                              :browser-lambda (lambda (subdir)\n                                                                (when (and (string= \"chromium\"\n                                                                                    (nfiles:basename (nfiles:parent subdir)))\n                                                                           (uiop:file-exists-p (uiop:merge-pathnames* \"History\" subdir)))\n                                                                  (uiop:merge-pathnames* \"History\" subdir))))))\n\n(define-history-import-command import-history-from-brave\n  \"Import history from Brave.\"\n  :sql-query \"SELECT url, title, last_visit_time/1000000-11644473600, visit_count FROM urls\"\n  :file-path (prompt1 :prompt \"Choose Brave History file\"\n                      :sources (make-instance 'external-browser-history-source\n                                              :browser-lambda (lambda (subdir)\n                                                                (when (and (string= \"Brave-Browser\"\n                                                                                    (nfiles:basename (nfiles:parent subdir)))\n                                                                           (uiop:file-exists-p (uiop:merge-pathnames* \"History\" subdir)))\n                                                                  (uiop:merge-pathnames* \"History\" subdir))))))\n\n(define-history-import-command import-history-from-vivaldi\n  \"Import history from Vivaldi.\"\n  :sql-query \"SELECT url, title, last_visit_time/1000000-11644473600, visit_count FROM urls\"\n  :file-path (prompt1 :prompt \"Choose Vivaldi History file\"\n                      :sources (make-instance 'external-browser-history-source\n                                              :browser-lambda (lambda (subdir)\n                                                                (when (and (str:starts-with-p \"vivaldi\"\n                                                                                              (nfiles:basename (nfiles:parent subdir)))\n                                                                           (uiop:file-exists-p (uiop:merge-pathnames* \"History\" subdir)))\n                                                                  (uiop:merge-pathnames* \"History\" subdir))))))\n"
  },
  {
    "path": "source/mode/history.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/mode/history\n  (:documentation \"Package for `history-mode', mode to manage browsing history.\"))\n\n(in-package :nyxt/mode/history)\n\n(define-mode history-mode ()\n  \"Mode to manage navigation history.\"\n  ((visible-in-status-p nil)\n   (history-blocklist\n    '()\n    :type (or null (list-of string))\n    :documentation \"A list of URL prefixes that are excluded from history.\")\n   (keyscheme-map\n    (define-keyscheme-map \"history-mode\" ()\n      keyscheme:default\n      (list\n       \"M-left\" 'history-backwards\n       \"M-right\" 'history-forwards\n       \"M-]\" 'history-forwards\n       \"M-[\" 'history-backwards)\n      keyscheme:emacs\n      (list\n       \"C-b\" 'history-backwards\n       \"C-f\" 'history-forwards)\n      keyscheme:vi-normal\n      (list\n       \"H\" 'history-backwards\n       \"L\" 'history-forwards)))))\n\n(define-configuration context-buffer\n  ((default-modes (cons 'history-mode %slot-value%))))\n\n(define-command history-backwards (&key (buffer (current-buffer)))\n  \"Navigate backwards.\"\n  (ffi-buffer-navigate-backwards buffer))\n\n(define-command history-forwards (&key (buffer (current-buffer)))\n  \"Navigate forwards.\"\n  (ffi-buffer-navigate-forwards buffer))\n\n(export-always 'blocked-p)\n(defmethod blocked-p (url (mode history-mode))\n  \"Check whether URL belongs to MODE's `history-blocklist'.\"\n  (find-if (rcurry #'str:starts-with? (render-url url))\n           (history-blocklist mode)))\n\n(define-internal-page-command-global list-history (&key (limit 100))\n    (buffer \"*History list*\")\n  \"Display the most recent browsing history entries up to LIMIT.\"\n  (spinneret:with-html-string\n    (:nstyle (style buffer))\n    (:h1 \"History\")\n    (:table :class \"resizable-table\"\n            (:tr (:th \"Title\") (:th \"URL\"))\n            (loop for entry in (recent-history-entries limit *browser*)\n                  for title = (title entry)\n                  for url = (quri:render-uri (url entry))\n                  collect (:tr (:td title) (:td (:a :href url url)))))))\n\n(defmethod add-url-to-history (url (mode history-mode) &key (title \"\"))\n  \"Push URL to `history-vector'.\"\n  (unless (blocked-p url mode)\n    (with-slots (history-vector history-file) *browser*\n        (vector-push-extend (make-instance 'history-entry\n                                       :url (quri:uri url)\n                                       :title title)\n                        history-vector)\n    (files:with-file-content (history history-file)\n      (setf history history-vector))\n    url)))\n\n(defmethod nyxt:on-signal-load-finished ((mode history-mode) url title)\n  (add-url-to-history url mode :title title)\n  url)\n"
  },
  {
    "path": "source/mode/input-edit.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/mode/input-edit\n  (:documentation \"Mode for editing HTML input areas with convenient keybindings.\n\nIn addition to the commands `input-edit-mode' exposes, there are\nseveral internal utilities for general HTML input editing:\n- Parenscript functions:\n  - `active-input-area-content'.\n  - `set-active-input-area-content'.\n  - `active-input-area-cursor'.\n  - `set-active-input-area-cursor'.\n- Macros `with-text-buffer' and `with-input-area'.\n- Definition macro `define-input-edit-command'.\n\nSeveral editing commands are based on `move-n-elements' internal\nfunction.\"))\n(in-package :nyxt/mode/input-edit)\n\n;;;; Commands for navigating/editing input fields on HTML pages.\n\n(define-parenscript active-input-area-content ()\n  (ps:chain (nyxt/ps:active-element document) value))\n\n(define-parenscript set-active-input-area-content (content)\n  (setf (ps:chain (nyxt/ps:active-element document) value) (ps:lisp content)))\n\n(define-parenscript active-input-area-cursor ()\n  (ps:chain (nyxt/ps:active-element document) selection-start))\n\n(define-parenscript set-active-input-area-cursor (selection-start selection-end)\n  (ps:chain (nyxt/ps:active-element document)\n            (set-selection-range\n             (ps:lisp selection-start)\n             (ps:lisp selection-end))))\n\n(export-always 'with-text-buffer)\n(defmacro with-text-buffer ((buffer-name cursor-name\n                             &optional initial-contents\n                               initial-cursor-position)\n                            &body body)\n  \"Create a BUFFER-NAME buffer with INITIAL-CONTENTS and CURSOR-NAME at\nINITIAL-CURSOR-POSITION.\nRun the BODY in the environment with these bound.\"\n  `(let ((,buffer-name (make-instance 'text-buffer:text-buffer))\n         (,cursor-name (make-instance 'text-buffer:cursor)))\n     (cluffer:attach-cursor ,cursor-name ,buffer-name)\n     (when ,initial-contents\n       (text-buffer::insert-string ,cursor-name ,initial-contents))\n     (when ,initial-cursor-position\n       (setf (cluffer:cursor-position ,cursor-name)\n             (truncate ,initial-cursor-position)))\n     ,@body))\n\n(export-always 'with-input-area)\n(defmacro with-input-area ((contents cursor-position) &body body)\n  \"Bind CONTENTS and CURSOR-POSITION to the ones in the currently focused\ninput field.\"\n  (let ((unprocessed-cursor (gensym)))\n    `(let* ((,contents (active-input-area-content))\n            (,unprocessed-cursor (active-input-area-cursor))\n            (,cursor-position (when (numberp ,unprocessed-cursor)\n                                (truncate (active-input-area-cursor)))))\n       (declare (ignorable ,contents ,cursor-position))\n       (if ,cursor-position\n           ,@body\n           (echo-warning \"Cannot get cursor. Are you in an input field?\")))))\n\n(defun move-n-elements (n)\n  (with-input-area (contents cursor-position)\n    (let ((new-position (+ cursor-position n)))\n      (set-active-input-area-cursor new-position\n                                    new-position))))\n\n(defmacro define-input-edit-command (name (&rest args) documentation &body body)\n  `(define-command ,name (,@args)\n     ,documentation\n     (with-current-buffer (or (current-prompt-buffer) (current-buffer))\n       ,@body)))\n\n(define-input-edit-command cursor-forwards ()\n  \"Move cursor forward by one element.\"\n  (move-n-elements 1))\n\n(define-input-edit-command cursor-backwards ()\n  \"Move cursor backwards by one element.\"\n  (move-n-elements -1))\n\n(define-input-edit-command cursor-forwards-word ()\n  \"Move cursor forwards a word.\"\n  (with-input-area (contents cursor-position)\n    (with-text-buffer (text-buffer cursor contents cursor-position)\n      (text-buffer::move-forward-word cursor\n                                      :conservative-word-move\n                                      (conservative-word-move (current-buffer)))\n      (set-active-input-area-cursor (cluffer:cursor-position cursor)\n                                    (cluffer:cursor-position cursor)))))\n\n(define-input-edit-command cursor-backwards-word ()\n  \"Move cursor backwards a word.\"\n  (with-input-area (contents cursor-position)\n    (with-text-buffer (text-buffer cursor contents cursor-position)\n      (text-buffer::move-backward-word cursor\n                                      :conservative-word-move\n                                      (conservative-word-move (current-buffer)))\n      (set-active-input-area-cursor (cluffer:cursor-position cursor)\n                                    (cluffer:cursor-position cursor)))))\n\n(define-input-edit-command delete-forwards ()\n  \"Delete character after cursor.\"\n  (with-input-area (contents cursor-position)\n    (with-text-buffer (text-buffer cursor contents cursor-position)\n      (text-buffer::delete-item-forward cursor)\n      (set-active-input-area-content\n       (text-buffer::string-representation text-buffer))\n      (set-active-input-area-cursor (cluffer:cursor-position cursor)\n                                    (cluffer:cursor-position cursor)))))\n\n(define-input-edit-command delete-backwards ()\n  \"Delete character before cursor.\"\n  (with-input-area (contents cursor-position)\n    (with-text-buffer (text-buffer cursor contents cursor-position)\n      (text-buffer::delete-item-backward cursor)\n      (set-active-input-area-content\n       (text-buffer::string-representation text-buffer))\n      (set-active-input-area-cursor (cluffer:cursor-position cursor)\n                                    (cluffer:cursor-position cursor)))))\n\n(define-input-edit-command delete-backwards-word ()\n  \"Delete backwards a word.\"\n  (with-input-area (contents cursor-position)\n    (with-text-buffer (text-buffer cursor contents cursor-position)\n      (text-buffer::delete-backward-word cursor)\n      (set-active-input-area-content\n       (text-buffer::string-representation text-buffer))\n      (set-active-input-area-cursor (cluffer:cursor-position cursor)\n                                    (cluffer:cursor-position cursor)))))\n\n(define-input-edit-command delete-forwards-word ()\n  \"Delete forwards a word.\"\n  (with-input-area (contents cursor-position)\n    (with-text-buffer (text-buffer cursor contents cursor-position)\n      (text-buffer::delete-forward-word cursor)\n      (set-active-input-area-content\n       (text-buffer::string-representation text-buffer))\n      (set-active-input-area-cursor (cluffer:cursor-position cursor)\n                                    (cluffer:cursor-position cursor)))))\n\n(define-mode input-edit-mode ()\n  \"Mode for editing input areas in HTML.\nOverrides many of the bindings in other modes, so you will have to\ndisable/enable it as necessary.\"\n  ((keyscheme-map\n    (define-keyscheme-map \"input-edit-mode\" ()\n      keyscheme:emacs\n      (list\n       \"C-b\" 'cursor-backwards\n       \"C-f\" 'cursor-forwards\n       \"C-d\" 'delete-forwards\n       \"M-b\" 'cursor-backwards-word\n       \"M-f\" 'cursor-forwards-word\n       \"M-backspace\" 'delete-backwards-word\n       \"C-backspace\" 'delete-backwards-word\n       \"M-d\" 'delete-forwards-word\n       \"C-g\" 'input-edit-mode)))))\n"
  },
  {
    "path": "source/mode/keyscheme.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/mode/keyscheme\n  (:documentation \"Package for `keyscheme-mode' that all modes setting\n`keyscheme' should inherit from.\nEnsures that a single keybindings mode, such as `nyxt/mode/emacs', is enabled.\"))\n(in-package :nyxt/mode/keyscheme)\n\n(define-mode keyscheme-mode ()\n  \"All modes that set `keyscheme' should inherit from this mode.\nEnsures that a single keyscheme mode, such as `nyxt/mode/emacs', is enabled.\n\nExample of defining a keyscheme mode:\n\n;; Beware that this may raise package locks condition on SBCL.\n(defvar keyscheme:my-keyscheme-mode\n        (keyscheme:make-keyscheme \\\"my-keyscheme-mode\\\" keyscheme:default))\n\n(define-mode my-keyscheme-mode (nyxt/mode/keyscheme:keyscheme-mode)\n  ((keyscheme keyscheme:my-keyscheme-mode)))\"\n  ((keyscheme                           ; This specialized `nyxt:keyscheme'.\n    keyscheme:cua\n    :documentation \"The `keymaps:keyscheme' to enable.\")\n   (previous-keyscheme\n    nil\n    :type (or keymaps:keyscheme null)\n    :documentation \"The active `keymaps:keyscheme' when disabling this mode.\"))\n  (:toggler-command-p nil))\n\n(defmethod enable :before ((mode keyscheme-mode) &key)\n  (setf (previous-keyscheme mode) (keyscheme (buffer mode)))\n  (mapc #'disable\n        (delete mode\n                (sera:filter #'keyscheme-mode-p (enabled-modes (buffer mode))))))\n\n(defmethod enable ((mode keyscheme-mode) &key)\n  (setf (keyscheme (buffer mode)) (keyscheme mode)))\n\n(defmethod disable ((mode keyscheme-mode) &key)\n  (setf (keyscheme (buffer mode)) (previous-keyscheme mode)))\n"
  },
  {
    "path": "source/mode/macro-edit.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/mode/macro-edit\n    (:documentation \"Package for `macro-edit-mode', mode for editing macros.\n\nThere are implementation details for (almost) every command in this mode:\n- `edit-macro': `render-functions'.\n- `add-command': `add-function', `remove-function', `macro-name', and\n  `generate-macro-form'.\n- `save-macro': `macro-form-valid-p'.\"))\n(in-package :nyxt/mode/macro-edit)\n\n(define-mode macro-edit-mode ()\n  \"Mode for creating and editing macros.\n\nSee `nyxt/mode/macro-edit' package documentation for implementation details.\"\n  ((macro-name\n    \"\"\n    :accessor nil\n    :documentation \"The descriptive name used for the macro.\")\n   (macro-description\n    \"\"\n    :accessor nil\n    :documentation \"The description used for the macro.\")\n   (functions\n    '()\n    :documentation \"Functions the user has added to their macro.\"))\n  (:toggler-command-p nil))\n\n(defmethod render-functions ((macro-editor macro-edit-mode))\n  (spinneret:with-html\n    (if (functions macro-editor)\n        (:table\n         (:tr\n          (:th \"Operations\")\n          (:th \"Command\"))\n         (loop for function in (functions macro-editor)\n               for index from 0\n               collect\n               (:tr (:td (:nbutton :class \"button\"\n                           :text \"Remove Command\"\n                           :title \"Remove from the macro\"\n                           `(nyxt/mode/macro-edit::remove-function\n                             (find-submode 'macro-edit-mode)\n                             ,index))\n                         (:a.button\n                          :title \"Help\"\n                          :target \"_blank\"\n                          :href (nyxt-url 'describe-function\n                                          :fn (name (nth\n                                                     index\n                                                     (functions macro-editor))))\n                          \"Command Information\"))\n                    (:td (let ((name (symbol-name (name function))))\n                           (if (str:upcase? name)\n                               (string-downcase name)\n                               name))))))\n        (:p \"No commands added to macro.\"))))\n\n(define-internal-page-command-global edit-macro ()\n    (buffer \"*Macro edit*\" 'nyxt/mode/macro-edit:macro-edit-mode)\n  \"Edit a macro.\"\n  (spinneret:with-html-string\n    (render-menu 'nyxt/mode/macro-edit:macro-edit-mode buffer)\n    (:h1 \"Macro editor\")\n    (:dl\n     (:dt \"Name\")\n     (:dd (:input :type \"text\" :id \"macro-name\"))\n     (:dt \"Description\")\n     (:dd (:input :type \"text\" :id \"macro-description\")))\n    (:h2 \"Commands\")\n    (:div\n     :id \"commands\"\n     (render-functions\n      (find-submode 'nyxt/mode/macro-edit:macro-edit-mode)))))\n\n(defmethod add-function ((macro-editor macro-edit-mode) command)\n  (alex:appendf (functions macro-editor)\n                (list command))\n  (ffi-buffer-reload (buffer macro-editor)))\n\n(defun delete-nth (n list)\n  (nconc (subseq list 0 n) (nthcdr (1+ n) list)))\n\n(defmethod remove-function ((macro-editor macro-edit-mode) command-index)\n  (setf (functions macro-editor)\n        (delete-nth command-index (functions macro-editor)))\n  (ffi-buffer-reload (buffer macro-editor)))\n\n(defmethod macro-name ((macro-editor macro-edit-mode))\n  (let ((name (ps-eval :buffer (buffer macro-editor)\n                (ps:chain (nyxt/ps:qs document \"#macro-name\") value))))\n    (cond ((not (str:emptyp name))\n           (setf (slot-value macro-editor 'macro-name) (string-upcase name)))\n          ((slot-value macro-editor 'macro-name)\n           (slot-value macro-editor 'macro-name))\n          (t nil))))\n\n(defmethod macro-description ((macro-editor macro-edit-mode))\n  (let ((name (ps-eval :buffer (buffer macro-editor)\n                (ps:chain (nyxt/ps:qs document \"#macro-description\") value))))\n    (cond ((not (str:emptyp name))\n           (setf (slot-value macro-editor 'macro-description) name))\n          ((slot-value macro-editor 'macro-description)\n           (slot-value macro-editor 'macro-description))\n          (t nil))))\n\n(defmethod generate-macro-form ((macro-editor macro-edit-mode))\n  (let ((name (intern (macro-name macro-editor)))\n        (description (macro-description macro-editor))\n        (commands (mapcar\n                   (lambda (command) `(,(name command)))\n                   (functions macro-editor))))\n    `(define-command-global ,name () ,description ,@commands)))\n\n(define-command add-command\n    (&optional (macro-editor (find-submode 'macro-edit-mode)))\n  \"Add a command to the macro.\"\n  (add-function macro-editor (prompt1\n                              :prompt \"Add command\"\n                              :sources 'command-source)))\n\n(defmethod macro-form-valid-p ((macro-editor macro-edit-mode))\n  (and (macro-name macro-editor)\n       (functions macro-editor)))\n\n(define-command save-macro\n    (&optional (macro-editor (find-submode 'macro-edit-mode)))\n  \"Save the macro to the `*auto-config-file*' file.\"\n  (if (macro-form-valid-p macro-editor)\n      (progn\n        (nyxt::auto-configure :form (generate-macro-form macro-editor))\n        (echo \"Saved macro to ~s.\" (files:expand *auto-config-file*)))\n      (echo \"Macro form is invalid; check it has a title and functions.\")))\n\n(define-command evaluate-macro\n    (&optional (macro-editor (find-submode 'macro-edit-mode)))\n  \"Evaluate the macro for testing.\"\n  (if (macro-form-valid-p macro-editor)\n      (progn\n        (eval (generate-macro-form macro-editor))\n        (echo \"Macro compiled, you may now use the ~s command.\"\n              (macro-name macro-editor)))\n      (echo \"Macro form is invalid; check it has a title and functions.\")))\n"
  },
  {
    "path": "source/mode/message.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/mode/message\n  (:documentation \"Package for `message-mode', a mode for displaying messages\nand logs.\nAll the APIs of the `message-mode' are exported, no implementation details.\"))\n(in-package :nyxt/mode/message)\n\n(define-mode message-mode ()\n  \"Mode for log and message listing.\nMainly used on `list-messages' page.\"\n  ()\n  (:toggler-command-p nil))\n\n(define-command clear-messages ()\n  \"Clear the *Messages* buffer and the underlying message data.\"\n  (setf (slot-value *browser* 'messages-content) '())\n  (echo \"Messages cleared.\"))\n\n(define-internal-page-command-global list-messages ()\n    (buffer \"*Messages*\" 'nyxt/mode/message:message-mode)\n  \"Show the *Messages* buffer.\"\n  (spinneret:with-html-string\n    (render-menu 'nyxt/mode/message:message-mode buffer)\n    (:h1 \"Messages\")\n    (:ul\n     (loop for message in (reverse (nyxt:messages-content *browser*))\n           collect (:li (:pre message))))))\n"
  },
  {
    "path": "source/mode/no-image.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/mode/no-image\n  (:documentation \"Package for `no-image-mode', mode to disable image display in buffer.\nUses `ffi-buffer-auto-load-image-enabled-p' internally.\"))\n(in-package :nyxt/mode/no-image)\n\n(define-mode no-image-mode ()\n  \"Disable images in current buffer.\nMight need page reload to take effect.\")\n\n(defmethod enable ((mode no-image-mode) &key)\n  (setf (ffi-buffer-auto-load-image-enabled-p (buffer mode)) nil))\n\n(defmethod disable ((mode no-image-mode) &key)\n  (setf (ffi-buffer-auto-load-image-enabled-p (buffer mode)) t))\n"
  },
  {
    "path": "source/mode/no-script.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/mode/no-script\n    (:documentation \"Package for `no-script-mode', mode to disable JavaScript.\n\nUses `ffi-buffer-javascript-markup-enabled-p' internally and hooks into\n`nyxt:on-signal-load-started' to allow toggling JS on internal pages.\"))\n(in-package :nyxt/mode/no-script)\n\n(define-mode no-script-mode ()\n  \"Disable Javascript in current buffer.\n\nInternal URLs ('nyxt://...') always have JavaScript enabled, otherwise they\nwouldn't be functional.  In other words, enabling `no-script-mode' is gracefully\nignored.\n\nSee `nyxt/mode/no-script' package documentation for implementation details and\ninternal programming APIs.\")\n\n(defmethod enable ((mode no-script-mode) &key)\n  (echo \"Reload the buffer for no-script-mode to take effect.\"))\n\n(defmethod disable ((mode no-script-mode) &key)\n  (setf (ffi-buffer-javascript-markup-enabled-p (buffer mode)) t))\n\n(defmethod nyxt:on-signal-load-started ((mode no-script-mode) url)\n  (setf (ffi-buffer-javascript-markup-enabled-p (buffer mode)) (internal-url-p url)))\n"
  },
  {
    "path": "source/mode/no-sound.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/mode/no-sound\n  (:documentation \"Package for `no-sound-mode', mode to disable sound.\n\nUses `ffi-buffer-sound-enabled-p' internally.\"))\n(in-package :nyxt/mode/no-sound)\n\n(define-mode no-sound-mode ()\n  \"Disable sound in current buffer.\n\nSee `nyxt/mode/no-sound' package documentation for implementation details and\ninternal programming APIs.\")\n\n(defmethod enable ((mode no-sound-mode) &key)\n  (setf (ffi-buffer-sound-enabled-p (buffer mode)) nil))\n\n(defmethod disable ((mode no-sound-mode) &key)\n  (setf (ffi-buffer-sound-enabled-p (buffer mode)) t))\n"
  },
  {
    "path": "source/mode/no-webgl.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/mode/no-webgl\n  (:documentation \"Package for `no-webgl-mode', mode to disable WebGL.\n\nUses `ffi-buffer-webgl-enabled-p' internally and saves the settings to\n`previous-webgl-setting'.\"))\n(in-package :nyxt/mode/no-webgl)\n\n(define-mode no-webgl-mode ()\n  \"Disable WebGL in current buffer.\n\nSee `nyxt/mode/no-webgl' package documentation for implementation details and\ninternal programming APIs.\"\n  ((previous-webgl-setting\n    nil\n    :documentation \"The state of WebGL before `no-webgl-mode' was enabled.\")))\n\n(defmethod enable ((mode no-webgl-mode) &key)\n  (setf (previous-webgl-setting mode) (ffi-buffer-webgl-enabled-p (buffer mode)))\n  (setf (ffi-buffer-webgl-enabled-p (buffer mode)) nil))\n\n(defmethod disable ((mode no-webgl-mode) &key)\n  (setf (ffi-buffer-webgl-enabled-p (buffer mode)) (previous-webgl-setting mode)))\n"
  },
  {
    "path": "source/mode/passthrough.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/mode/passthrough\n  (:documentation \"Package for `passthrough-mode', mode to forward all keybindings to the page.\n\nUtilizes the `nyxt/keyscheme' API, `nyxt/mode/keyscheme' APIs, and\n`current-keymaps-hook' or `input-buffer'.\"))\n(in-package :nyxt/mode/passthrough)\n\n(define-mode passthrough-mode ()\n  \"Mode that forwards all keys to the page.\n\nSee the mode `keyscheme-map' for special bindings and `nyxt/mode/passthrough'\npackage documentation for implementation details and internal programming APIs.\"\n  ((keyscheme-map\n    (define-keyscheme-map \"passthrough-mode\" ()\n      keyscheme:default\n      (list\n       \"C-M-z\" 'passthrough-mode)))))\n\n(defmethod enable ((mode passthrough-mode) &key)\n  (setf (forward-input-events-p (buffer mode)) t))\n\n(defmethod disable ((mode passthrough-mode) &key)\n  (setf (forward-input-events-p (buffer mode)) nil))\n"
  },
  {
    "path": "source/mode/password.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/mode/password\n  (:documentation \"Package for `password-mode', mode to interface with password\nmanagers.\n\nRelies on the `password' library for most package manager interactions. In\nparticular:\n- Specifies `password::execute' for KeePassXC to prompt for Yubikey tap.\n- Specifies `password:complete-interface' to prompt for details for interfaces\n  that need it.\n- Adds a `with-password' macro relying on `password:password-correct-p' to\n  decide whether the interface is properly connected and complete, and calling\n  `password:complete-interface' if it's not.\n\nAlso note the internal `make-password-interface-user-classes' function to force\npassword interfaces to become `user-class'es and thus\n`define-configuration'-friendly.\n\nSee the `password-mode' for the external user-facing APIs.\"))\n(in-package :nyxt/mode/password)\n\n(define-mode password-mode ()\n  \"Enable interface with third-party password managers.\nYou can customize the default interface with the mode slot `password-interface'.\n\nSee `nyxt/mode/password' package documentation for implementation details and\ninternal programming APIs.\"\n  ((visible-in-status-p nil)\n   (password-interface\n    (make-password-interface)\n    :type (or null password::password-interface)\n    :documentation \"The current password interface.\nSee `password:*interfaces*' for the list of all currently registered interfaces.\nTo use, say, KeepassXC, set this slot to\n\n  (make-instance 'password:keepassxc-interface)\n\nPassword interfaces are configurable through a `customize-instance' method.\")))\n\n(define-configuration context-buffer\n  ((default-modes (cons 'password-mode %slot-value%))))\n\n(defmethod password-interface ((buffer buffer))\n  (password-interface (find-submode 'password-mode buffer)))\n\n(defun make-password-interface-user-classes ()\n  \"Define classes with the `user-class' metaclass so that users may use\n`customize-instance'.\"\n  (dolist (interface password:*interfaces*)\n    (closer-mop:ensure-class (intern (symbol-name interface))\n                             :direct-superclasses (list interface)\n                             :metaclass 'user-class)))\n\n(make-password-interface-user-classes)\n\n(defun make-password-interface ()\n  \"Return the instance of the first password interface among\n`password:*interfaces*'\nfor which the `executable' slot is non-nil.\"\n  (some (lambda (interface)\n          (let ((instance (make-instance interface)))\n            (when (password:executable instance)\n              instance)))\n        password:*interfaces*))\n\n(defvar password-source-actions\n  (list (lambda-command clip-password (password-name)\n          (let ((buffer (buffer (current-source)))\n                (password-name (first password-name)))\n            (password:clip-password (password-interface buffer)\n                                    :password-name password-name)\n            (echo \"Password saved to clipboard for ~a seconds.\"\n                  (password:sleep-timer (password-interface buffer)))))\n        (lambda-command clip-username (password-name)\n          (let ((buffer (buffer (current-source)))\n                (password-name (first password-name)))\n            (if (password:clip-username (password-interface buffer)\n                                        :password-name password-name)\n                (echo \"Username saved to clipboard.\")\n                (echo \"No username found.\"))))))\n\n(define-class password-source (prompter:source)\n  ((prompter:name \"Passwords\")\n   (buffer :accessor buffer :initarg :buffer)\n   (password-instance :accessor password-instance :initarg :password-instance)\n   (prompter:constructor\n    (lambda (source)\n      (password:list-passwords (password-instance source))))\n   (prompter:actions-on-return password-source-actions)))\n\n(defun password-debug-info ()\n  (when-let ((interface (password-interface (current-buffer))))\n    (log:debug \"Password interface ~a uses executable ~s.\"\n               (class-name (class-of interface))\n               (password:executable interface))))\n\n(defmacro with-password (password-interface &body body)\n  `(if (password:password-correct-p ,password-interface)\n       ,@body\n       (progn\n         (password:complete-interface ,password-interface)\n         ,@body)))\n\n(define-command save-new-password (&optional (buffer (current-buffer)))\n  \"Save password to password interface.\"\n  (password-debug-info)\n  (cond\n    ((and (password-interface buffer)\n          (nyxt:has-method-p (password-interface\n                              (find-submode 'password-mode buffer))\n                             #'password:save-password))\n     (with-password (password-interface buffer)\n       (let* ((password-name\n                (prompt1 :prompt \"Name for new password\"\n                         :input (or (quri:uri-domain (url (current-buffer)))\n                                    \"\")\n                         :sources 'prompter:raw-source))\n              (new-password\n                (prompt1 :prompt \"New password (leave empty to generate)\"\n                         :sources 'prompter:raw-source\n                         :invisible-input-p t))\n              (username (prompt1 :prompt \"Username (can be empty)\"\n                                 :sources 'prompter:raw-source)))\n         (password:save-password (password-interface buffer)\n                                 :username username\n                                 :password-name password-name\n                                 :password new-password))))\n    ((null (password-interface buffer))\n     (echo-warning \"No password manager found.\"))\n    (t (echo-warning \"Password manager ~s does not support saving passwords.\"\n                     (string-downcase\n                      (class-name (class-of (password-interface buffer))))))))\n\n(defmethod password::execute :before\n    ((password-interface password:keepassxc-interface) (arguments list)\n     &rest run-program-args &key &allow-other-keys)\n  (declare (ignore arguments run-program-args))\n  (when (password::yubikey-slot password-interface)\n    (echo \"Tap your Yubikey to prove KeePassXC database access\")))\n\n(defmethod password:complete-interface\n    ((password-interface password:keepassxc-interface))\n  (loop until (password:password-correct-p password-interface)\n        unless (and (password::password-file password-interface)\n                    (string-equal \"kdbx\"\n                                  (pathname-type\n                                   (pathname (password::password-file\n                                              password-interface)))))\n          do (setf (password::password-file password-interface)\n                   (uiop:native-namestring\n                    (prompt1\n                     :prompt \"Password database file (.kdbx)\"\n                     :extra-modes 'nyxt/mode/file-manager:file-manager-mode\n                     :sources (make-instance 'nyxt/mode/file-manager:file-source\n                                             :extensions '(\"kdbx\")))))\n        unless (password::key-file password-interface)\n          do (if-confirm (\"Do you use key file for password database locking?\")\n                 (setf (password::key-file password-interface)\n                       (uiop:native-namestring\n                        (prompt1\n                         :prompt \"Password database key file\"\n                         :extra-modes 'nyxt/mode/file-manager:file-manager-mode\n                         :sources (make-instance\n                                   'nyxt/mode/file-manager:file-source)))))\n        unless (password::yubikey-slot password-interface)\n          do (if-confirm (\"Do you use Yubikey for password database locking\")\n                 (setf (password::yubikey-slot password-interface)\n                       (prompt1 :prompt \"Yubikey slot[:port]\"\n                                :sources (make-instance 'prompter:raw-source))))\n        do (setf (password::master-password password-interface)\n                 (prompt1\n                  :prompt\n                  (format nil \"Database password for ~a (leave empty if none)\"\n                          (password::password-file password-interface))\n                          :sources 'prompter:raw-source\n                          :height :fit-to-prompt\n                          :invisible-input-p t))))\n\n(define-command copy-password (&optional (buffer (current-buffer)))\n  \"Query password and save to clipboard.\nSee also `copy-password-prompt-details'.\"\n  (password-debug-info)\n  (if (password-interface buffer)\n      (with-password (password-interface buffer)\n        (prompt1 :prompt \"Password\"\n                 :input (quri:uri-domain (url buffer))\n                 :sources (make-instance\n                           'password-source\n                           :buffer buffer\n                           :password-instance (password-interface buffer))))\n      (echo-warning \"No password manager found.\")))\n\n(define-command copy-username (&optional (buffer (current-buffer)))\n  \"Query username and save to clipboard.\"\n  (password-debug-info)\n  (if (password-interface buffer)\n      (with-password (password-interface buffer)\n        (prompt :prompt \"Username\"\n                :input (quri:uri-domain (url buffer))\n                :sources (make-instance\n                          'password-source\n                          :buffer buffer\n                          :password-instance (password-interface buffer)\n                          :actions-on-return\n                          (sera:filter (sera:eqs 'clip-username)\n                                       password-source-actions\n                                       :key #'name))))\n      (echo-warning \"No password manager found.\")))\n"
  },
  {
    "path": "source/mode/process.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/mode/process\n  (:documentation \"Package for `process-mode', mode to conditionally act on\nfile/directory.\n\nAPIs that `process-mode' has:\n- Slots:\n  - `path-url' to set the path tracked by the mode.\n  - `firing-condition', continuously checked for whether to run the...\n  - `action'---the actual thing to run.\n  - `cleanup' runs after `firing-condition' returns :RETURN to clean up the mode.\n  - `thread' as the thread that all the `action's happen on. Usually needs not\n    be altered, because the default is intuitive enough.\n- Internal helpers:\n  - `thread-alive-p' to check on the `thread'.\n  - `call-cleanup' to relay things to `cleanup' code.\n\nIt also uses threading utilities like `destroy-thread*', `sera:synchronized',\nand `bt:thread-alive-p'.\"))\n(in-package :nyxt/mode/process)\n\n(define-mode process-mode ()\n  \"Conditionally execute a file/directory-related `action' in a separate thread.\n\nPossible applications:\n- Web server.\n- Refreshing a URL at regular intervals (`nyxt/mode/watch').\n- Live tracking of filesystem/data in a file/directory.\n\nThe mode itself should not be used directly. Rather, it should be subclassed and\nextended with custom logic.\n\nSee `nyxt/mode/process' package documentation for implementation details and\ninternal programming APIs.\"\n  ((visible-in-status-p nil)\n   (path-url\n    nil\n    :type (or quri:uri null)\n    :documentation \"URL where `action' runs.\nIt's not necessarily the same as the current buffer's URL.\")\n   (firing-condition\n    t\n    :type (or boolean (function (quri:uri process-mode)))\n    :documentation \"The condition for triggering `action'.\nIt's either a boolean (T to always fire, NIL to never fire) or a function of the\nURL and mode instance.  When the function returns :RETURN, the process is\nstopped.\")\n   (action\n    nil\n    :type (or (function (quri:uri process-mode)) null)\n    :documentation \"Function that takes a URL and a `process-mode' instance as\narguments.\")\n   (cleanup\n    nil\n    :type (or (function (quri:uri process-mode)) null)\n    :documentation \"Function to run when process ends.\nAccepts the path to the acted-on document and `process-mode' instance.\")\n   (thread\n    nil\n    :type (or bt:thread null)\n    :export nil\n    :documentation \"Thread where `action' runs.\"))\n  (:toggler-command-p nil))\n\n(defmethod thread-alive-p ((mode process-mode))\n  (and (thread mode) (bt:thread-alive-p (thread mode))))\n\n(defmethod enable ((mode process-mode) &key)\n  (when (and (firing-condition mode)\n             (action mode)\n             (not (thread-alive-p mode)))\n    (setf (path-url mode)\n          (or (path-url mode) (url (current-buffer))))\n    (setf (thread mode)\n          (run-thread \"process\"\n            (loop with cond = (firing-condition mode)\n                  with cond-func = (typecase cond\n                                     (function cond)\n                                     (boolean (constantly cond)))\n                  for condition-value = (funcall cond-func (path-url mode) mode)\n                  when (eq condition-value :return)\n                    do (progn\n                         (disable-modes* (sera:class-name-of mode) (buffer mode))\n                         (return))\n                  else\n                    when condition-value\n                      do (with-current-buffer (buffer mode)\n                           (sera:synchronized (mode)\n                             (funcall* (action mode) (path-url mode) mode))))))))\n\n(defmethod call-cleanup ((mode process-mode) &key)\n  (funcall* (cleanup mode) (path-url mode) mode))\n\n(defmethod disable ((mode process-mode) &key)\n  (call-cleanup mode)\n  (destroy-thread* (thread mode)))\n"
  },
  {
    "path": "source/mode/prompt-buffer.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/mode/prompt-buffer\n  (:documentation \"Package for `prompt-buffer-mode' to interact with the\n`prompt-buffer'.\"))\n(in-package :nyxt/mode/prompt-buffer)\n\n(define-mode prompt-buffer-mode ()\n  \"The prompt buffer is where all interactions between Nyxt and the user take\nplace.  It displays a list of suggestions which are filtered as the user types.\n\nMany prompter-buffer-specific commands are available; you can list them with\n`run-prompt-buffer-command', bound to \\\"f1 b\\\" by default.\n\nThe prompt buffer can have multiple `prompter:source's of suggestions.  Each\nsource has its own properties, such as the ability to mark multiple suggestions.\nThe same source can be used by different prompt buffers.\n\nEach source offers a set of `actions-on-return' for marked items.  These can be\nlisted and chosen from with the command `set-action-on-return' (bound to\n\\\"M-return\\\" by default).\"\n  ((visible-in-status-p nil)\n   (keyscheme-map\n    (define-keyscheme-map \"prompt-buffer-mode\" ()\n      keyscheme:default\n      (list\n       \"M-up\" 'expand\n       \"M-down\" 'contract\n       \"up\" 'previous-suggestion\n       \"button4\" 'previous-suggestion\n       \"down\" 'next-suggestion\n       \"button5\" 'next-suggestion\n       \"shift-home\" 'first-suggestion\n       \"pagehome\" 'first-suggestion\n       \"shift-end\" 'last-suggestion\n       \"pageend\" 'last-suggestion\n       \"pageup\" 'previous-page\n       \"pagedown\" 'next-page\n       \"shift-up\" 'previous-source\n       \"shift-down\" 'next-source\n       \"shift-left\" 'first-suggestion-within-source\n       \"shift-right\" 'last-suggestion-within-source\n       \"M-o\" 'toggle-prompt-buffer-focus\n       \"escape\" 'quit-prompt-buffer\n       \"M-a\" 'mark-all\n       \"M-u\" 'unmark-all\n       \"C-space\" 'toggle-mark-forwards\n       \"M-space\" 'toggle-mark-forwards\n       \"shift-space\" 'toggle-mark-backwards\n       \"M-shift-space\" 'toggle-mark-backwards\n       \"M-m\" 'toggle-mark-all\n       \"M-h\" 'history\n       \"f1 b\" 'run-prompt-buffer-command\n       \"f1 m\" 'describe-prompt-buffer\n       \"return\" 'run-action-on-return\n       \"keypadenter\" 'run-action-on-return\n       \"M-return\" 'set-action-on-return\n       \"M-keypadenter\" 'set-action-on-return\n       \"C-return\" 'toggle-mark-forwards\n       \"C-keypadenter\" 'toggle-mark-forwards\n       \"shift-return\" 'toggle-mark-forwards\n       \"shift-keypadenter\" 'toggle-mark-forwards\n       \"C-j\" 'run-action-on-current-suggestion\n       \"C-c C-j\" 'set-action-on-current-suggestion\n       \"tab\" 'insert-current-suggestion\n       \"C-c C-f\" 'toggle-actions-on-current-suggestion\n       \"C-]\" 'toggle-attributes-display)\n      keyscheme:cua\n      (list\n       \"C-up\" 'first-suggestion\n       \"C-shift-up\" 'first-suggestion-within-source\n       \"C-down\" 'last-suggestion\n       \"C-shift-down\" 'last-suggestion-within-source\n       \"C-v\" 'paste\n       \"C-x\" 'cut\n       \"C-a\" 'select-input-text)\n      keyscheme:emacs\n      (list\n       \"C-p\" 'previous-suggestion\n       \"C-n\" 'next-suggestion\n       \"M-<\" 'first-suggestion\n       \"M-,\" 'first-suggestion-within-source\n       \"M->\" 'last-suggestion\n       \"M-.\" 'last-suggestion-within-source\n       \"C-x o\" 'toggle-prompt-buffer-focus\n       \"M-v\" 'previous-page\n       \"C-v\" 'next-page\n       \"M-p\" 'previous-source\n       \"M-n\" 'next-source\n       \"M-[\" 'previous-source\n       \"M-]\" 'next-source\n       \"C-M-n\" 'scroll-other-buffer-down\n       \"C-M-p\" 'scroll-other-buffer-up\n       \"C-M-v\" 'scroll-page-down-other-buffer\n       \"C-M-V\" 'scroll-page-up-other-buffer\n       \"C-g\" 'quit-prompt-buffer\n       \"C-e\" 'move-end-of-input\n       \"C-a\" 'move-start-of-input\n       \"C-b\" 'nyxt/mode/input-edit:cursor-backwards\n       \"C-f\" 'nyxt/mode/input-edit:cursor-forwards\n       \"C-d\" 'nyxt/mode/input-edit:delete-forwards\n       \"M-b\" 'nyxt/mode/input-edit:cursor-backwards-word\n       \"M-f\" 'nyxt/mode/input-edit:cursor-forwards-word\n       \"C-backspace\" 'nyxt/mode/input-edit:delete-backwards-word\n       \"M-backspace\" 'nyxt/mode/input-edit:delete-backwards-word\n       \"M-d\" 'nyxt/mode/input-edit:delete-forwards-word\n       \"C-x h\" 'select-input-text\n       \"M-w\" 'copy-selection\n       \"C-y\" 'paste\n       \"C-w\" 'cut\n       \"C-h m\" 'describe-prompt-buffer\n       \"C-h b\" 'run-prompt-buffer-command\n       \"C-j\" 'run-action-on-current-suggestion)\n      keyscheme:vi-normal\n      (list\n       \"k\" 'previous-suggestion\n       \"j\" 'next-suggestion\n       \"C-k\" 'previous-suggestion\n       \"C-j\" 'next-suggestion\n       \"g g\" 'first-suggestion\n       \"G\" 'last-suggestion\n       \"C-b\" 'previous-page\n       \"C-f\" 'next-page\n       \"K\" 'previous-source\n       \"J\" 'next-source\n       \"C-K\" 'previous-source\n       \"C-J\" 'next-source\n       \"M-j\" 'scroll-other-buffer-down\n       \"M-k\" 'scroll-other-buffer-up\n       \"C-M-j\" 'scroll-page-down-other-buffer\n       \"C-M-k\" 'scroll-page-up-other-buffer\n       \"$\" 'move-end-of-input\n       \"^\" 'move-start-of-input\n       \"l\" 'nyxt/mode/input-edit:cursor-forwards\n       \"h\" 'nyxt/mode/input-edit:cursor-backwards\n       \"w\" 'nyxt/mode/input-edit:cursor-forwards-word\n       \"b\" 'nyxt/mode/input-edit:cursor-backwards-word\n       \"x\" 'nyxt/mode/input-edit:delete-forwards\n       \"d b\" 'nyxt/mode/input-edit:delete-backwards-word\n       \"d w\" 'nyxt/mode/input-edit:delete-forwards-word\n       \"z f\" 'toggle-actions-on-current-suggestion\n       \"z a\" 'toggle-attributes-display\n       \"y\" 'copy-selection\n       \"p\" 'paste\n       \"d d\" 'cut)\n      keyscheme:vi-insert\n      (list\n       \"return\" 'run-action-on-return\n       \"keypadenter\" 'run-action-on-return\n       \"M-return\" 'set-action-on-return\n       \"M-keypadenter\" 'set-action-on-return\n       \"C-k\" 'previous-suggestion\n       \"C-j\" 'next-suggestion\n       \"C-b\" 'previous-page\n       \"C-f\" 'next-page\n       \"C-K\" 'previous-source\n       \"C-J\" 'next-source))))\n  (:toggler-command-p nil))\n\n(export-always 'define-command-prompt)\n(defmacro define-command-prompt (name (prompt-buffer &rest arglist) &body body)\n  \"Like `define-command', but the first argument is special:\n- it is considered a keyword argument if `&keyword' is in arglist, `&optional'\n  otherwise,\n- it is bound to `current-prompt-buffer' if unspecified,\n- the body is skipped and a warning is emitted unless non-nil.\"\n  (multiple-value-bind (forms declares documentation)\n      (alex:parse-body body :documentation t)\n    (multiple-value-bind (required optional rest keywords aok? aux key?)\n        (alex:parse-ordinary-lambda-list arglist)\n      (flet ((unparse-arguments (prompt-buffer-sym)\n               (if keywords\n                   (push `((,(intern (string prompt-buffer-sym) \"KEYWORD\")\n                            ,prompt-buffer-sym) (current-prompt-buffer) nil)\n                         keywords)\n                   (push `(,prompt-buffer-sym (current-prompt-buffer) nil)\n                         optional))\n               (sera:unparse-ordinary-lambda-list\n                required optional rest keywords aok? aux key?)))\n        `(define-command ,name ,(unparse-arguments prompt-buffer)\n           ,@(sera:unsplice documentation)\n           ,@declares\n           (if prompt-buffer\n               (progn ,@forms)\n               (log:warn \"Can't call ~a on nil prompt buffer\" ',name)))))))\n\n(define-command-prompt next-suggestion (prompt-buffer)\n  \"Select next entry in prompt buffer.\"\n  (prompter:next-suggestion prompt-buffer)\n  (prompt-render-suggestions prompt-buffer))\n\n(define-command-prompt previous-suggestion (prompt-buffer)\n  \"Select previous entry in PROMPT-BUFFER.\"\n  (prompter:previous-suggestion prompt-buffer)\n  (prompt-render-suggestions prompt-buffer))\n\n(define-command-prompt first-suggestion (prompt-buffer)\n  \"Select first entry in PROMPT-BUFFER.\"\n  (prompter:first-suggestion prompt-buffer)\n  (prompt-render-suggestions prompt-buffer))\n\n(define-command-prompt last-suggestion (prompt-buffer)\n  \"Select last entry in PROMPT-BUFFER.\"\n  (prompter:last-suggestion prompt-buffer)\n  (prompt-render-suggestions prompt-buffer))\n\n(define-command-prompt first-suggestion-within-source (prompt-buffer)\n  \"Select first entry in the current PROMPT-BUFFER's source.\"\n  (let ((first-source-p (eq (prompter:current-source prompt-buffer)\n                            (first (prompter:previous-source prompt-buffer)))))\n    (if first-source-p\n        (prompter:first-suggestion prompt-buffer)\n        (prompter:next-suggestion prompt-buffer)))\n  (prompt-render-suggestions prompt-buffer))\n\n(define-command-prompt last-suggestion-within-source (prompt-buffer)\n  \"Select last entry in the current PROMPT-BUFFER's source.\"\n  (let ((last-source-p (eq (prompter:current-source prompt-buffer)\n                           (first (prompter:next-source prompt-buffer)))))\n    (if last-source-p\n        (prompter:last-suggestion prompt-buffer)\n        (prompter:previous-suggestion prompt-buffer)))\n  (prompt-render-suggestions prompt-buffer))\n\n(define-command-prompt next-source (prompt-buffer)\n  \"Select next source in PROMPT-BUFFER.\"\n  (prompter:next-source prompt-buffer)\n  (prompt-render-suggestions prompt-buffer))\n\n(define-command-prompt previous-source (prompt-buffer)\n  \"Select previous source in PROMPT-BUFFER.\"\n  (prompter:previous-source prompt-buffer)\n  (prompt-render-suggestions prompt-buffer))\n\n(define-command-prompt next-page (prompt-buffer &key (n 1))\n  \"Select entry by N next pages in PROMPT-BUFFER.\nIf N is negative, go to previous pages instead.\"\n  (unless (= 0 n)\n    (let ((step-page-index\n            (ps-eval :buffer prompt-buffer\n              (defun step-row (row)\n                (ps:chain\n                 (aref (ps:chain row parent-node rows)\n                       (max 0\n                            (min (- (ps:chain row parent-node rows length) 1)\n                                 (+ (if (< 0 (ps:lisp n)) 1 -1)\n                                    (ps:chain row row-index)))))))\n              (defun find-first-element-out-of-view (row)\n                (if (nyxt/ps:element-in-view-port-p row)\n                    (let ((new-row (step-row row)))\n                      (if (eq new-row row)\n                          row\n                          (find-first-element-out-of-view new-row)))\n                    row))\n              (-\n               (ps:chain\n                (find-first-element-out-of-view (nyxt/ps:qs document \"#selection\"))\n                row-index)\n               (ps:chain (nyxt/ps:qs document \"#selection\") row-index)))))\n      (and-let* ((index-diff step-page-index))\n        (prompter:next-suggestion prompt-buffer\n                                  index-diff)))\n    (prompt-render-suggestions prompt-buffer)))\n\n(define-command-prompt contract (prompt-buffer &key (delta 10))\n  (decf (ffi-height prompt-buffer) delta))\n\n(define-command-prompt expand (prompt-buffer &key (delta 10))\n  (incf (ffi-height prompt-buffer) delta))\n\n(define-command-prompt previous-page (prompt-buffer &key (n 1))\n  \"Select entry by N previous pages in PROMPT-BUFFER.\nIf N is negative, go to next pages instead.\"\n  (next-page :prompt-buffer prompt-buffer :n (- n)))\n\n(define-command-prompt run-action-on-return (prompt-buffer)\n  \"Have the PROMPT-BUFFER return the marks, then quit.\"\n  (prompter:run-action-on-return prompt-buffer))\n\n(defun make-attribute-suggestion (attribute &optional source input)\n  \"Return a `suggestion' wrapped around ATTRIBUTE.\"\n  (declare (ignore source input))\n  (make-instance 'prompter:suggestion\n                 :value attribute\n                 :attributes `((\"Attribute key\" ,attribute))))\n\n(define-class attribute-source (prompter:source)\n  ((prompter:name \"List of prompter attributes\")\n   (prompter:enable-marks-p t)\n   (prompter:suggestion-maker 'make-attribute-suggestion)\n   (prompter:actions-on-return #'return-marks-only)))\n\n(defun return-marks-only (suggestion-values)\n  \"Return marked suggestions only.\nThey are returned untouched.\nThis is useful for prompters where we want either marks or nothing, but not the\ncurrent unmarked suggestion.\"\n  (multiple-value-bind (suggestion source)\n      (prompter:%current-suggestion (current-prompt-buffer))\n    (if (and (typep source 'attribute-source)\n             (not (prompter:marks source)))\n        (remove (prompter:value suggestion) suggestion-values\n                :test #'equal)\n        suggestion-values)))\n\n(define-command-prompt toggle-attributes-display\n    (prompt-buffer &key (source (current-source prompt-buffer)))\n  \"Prompt for which prompter attributes to display.\"\n  (let ((attributes\n          (prompt :prompt \"Mark attributes to display\"\n                  :sources\n                  (make-instance\n                   'attribute-source\n                   :marks (intersection\n                           (prompter:active-attributes-keys source)\n                           (prompter:attributes-keys-non-default source)\n                           :test #'string=)\n                   :constructor (prompter:attributes-keys-non-default source)))))\n    (setf (prompter:active-attributes-keys source)\n          attributes)\n    (prompt-render-suggestions prompt-buffer)))\n\n(define-class prompt-buffer-command-source (command-source)\n  ((prompter:name \"Prompt buffer commands\")\n   (parent-prompt-buffer (error \"Parent prompt buffer required\"))\n   (global-p nil)\n   (buffer (current-prompt-buffer))\n   (prompter:suggestion-maker 'make-prompt-buffer-command-suggestion)))\n\n(defun make-prompt-buffer-command-suggestion (command source)\n  \"Return a `suggestion' wrapped around COMMAND.\"\n  (make-instance\n   'prompter:suggestion\n   :value command\n   :attributes (nyxt::command-attributes command (parent-prompt-buffer source))))\n\n(define-command-prompt run-prompt-buffer-command (prompt-buffer)\n  \"Prompt for a command to call in PROMPT-BUFFER.\"\n  (let ((command\n          (prompt1 :prompt \"Command to run in current prompt buffer\"\n                   :sources (make-instance\n                             'prompt-buffer-command-source\n                             :parent-prompt-buffer prompt-buffer))))\n    (funcall* command)))\n\n(defun prompt-buffer-actions-on-return (&optional (window (current-window)))\n  (or (and-let* ((first-prompt-buffer\n                  (first (nyxt::active-prompt-buffers window))))\n        (prompter:actions-on-return first-prompt-buffer))\n      (progn\n        (echo-warning \"No actions to choose from.\")\n        (error 'prompt-buffer-canceled))))\n\n(defun prompt-buffer-actions-on-current-suggestion\n    (&optional (window (current-window)))\n  (and-let* ((first-prompt-buffer (first (nyxt::active-prompt-buffers window))))\n    (prompter:actions-on-current-suggestion\n     (prompter:current-source first-prompt-buffer))))\n\n(defun make-action-suggestion (action &optional source input)\n  \"Return a `suggestion' wrapped around ACTION.\"\n  (declare (ignore source input))\n  (make-instance\n   'prompter:suggestion\n   :value action\n   :attributes `((\"Name\" ,(or (ignore-errors\n                               (symbol-name (typecase action\n                                              (command (name action))\n                                              (t action))))\n                              \"Lambda\"))\n                 (\"Documentation\" ,(documentation-line action 'function \"\")))))\n\n(define-class action-on-return-source (prompter:source)\n  ((prompter:name \"List of actions-on-return\")\n   (prompter:constructor (prompt-buffer-actions-on-return))\n   (prompter:suggestion-maker 'make-action-suggestion)))\n\n(define-class action-on-current-suggestion-source (prompter:source)\n  ((prompter:name \"List of actions-on-current-suggestion\")\n   (prompter:constructor (prompt-buffer-actions-on-current-suggestion))\n   (prompter:suggestion-maker 'make-action-suggestion)))\n\n(define-command-prompt set-action-on-return (prompt-buffer)\n  \"Prompt for an action to run over PROMPT-BUFFER `prompter:marks'.\"\n  (if (equal (mapcar #'type-of (prompter:sources (current-prompt-buffer)))\n             '(action-on-return-source))\n      (echo \"Already displaying actions-on-return of previous prompt buffer.\")\n      (when-let ((action (prompt1 :prompt \"Set return action to run over marks\"\n                                  :sources 'action-on-return-source)))\n        (prompter:run-action-on-return prompt-buffer action))))\n\n(define-command-prompt run-action-on-current-suggestion (prompt-buffer)\n  \"Run `prompter::default-action-on-current-suggestion' without closing\nPROMPT-BUFFER.\"\n  (prompter:run-action-on-current-suggestion prompt-buffer))\n\n(define-command-prompt set-action-on-current-suggestion (prompt-buffer)\n  \"Set `prompter:actions-on-current-suggestion' without closing PROMPT-BUFFER.\"\n  (when-let ((action (prompt1 :prompt \"Set current suggestion action\"\n                              :sources 'action-on-current-suggestion-source)))\n    (prompter:set-action-on-current-suggestion action prompt-buffer)))\n\n(define-command-prompt quit-prompt-buffer (prompt-buffer)\n  \"Close the PROMPT-BUFFER without further action.\"\n  (prompter:destroy prompt-buffer))\n\n(define-command-prompt toggle-actions-on-current-suggestion (prompt-buffer)\n  \"Toggle whether `prompter:actions-on-current-suggestion' are enabled for\nPROMPT-BUFFER.\"\n  (prompter:toggle-actions-on-current-suggestion prompt-buffer)\n  (echo \"Current suggestion actions: ~:[dis~;en~]abled.\"\n        (prompter:actions-on-current-suggestion-enabled-p\n         (current-source prompt-buffer))))\n\n(define-command-prompt toggle-mark-forwards\n    (prompt-buffer &key (direction :forward))\n  \"Mark current suggestion and `next-suggestion'.\nOnly available if current PROMPT-BUFFER source `enable-marks-p' is non-nil.\nDIRECTION can be `:forward' or `:backward' and specifies which suggestion to\nselect next.\"\n  (prompter:toggle-mark prompt-buffer)\n  (match direction\n    (:forward (next-suggestion prompt-buffer))\n    (:backward (previous-suggestion prompt-buffer))))\n\n(define-command-prompt toggle-mark-backwards (prompt-buffer)\n  \"Mark current suggestion and `previous-suggestion'.\nOnly available if `prompter:enable-marks-p' is non-nil.\"\n  (toggle-mark-forwards :prompt-buffer prompt-buffer\n                        :direction :backward))\n\n(define-command-prompt mark-all (prompt-buffer)\n  \"Mark all suggestions in the current source.\nOnly available if `prompter:enable-marks-p' is non-nil.\"\n  (prompter:mark-all prompt-buffer)\n  (prompt-render-suggestions prompt-buffer))\n\n(define-command-prompt unmark-all (prompt-buffer)\n  \"Unmark all visible suggestions in current source.\nOnly available if `prompter:enable-marks-p' is non-nil.\"\n  (prompter:unmark-all prompt-buffer)\n  (prompt-render-suggestions prompt-buffer))\n\n(define-command-prompt toggle-mark-all (prompt-buffer)\n  \"Toggle the mark over all visible suggestions in current source.\nOnly available if `prompter:enable-marks-p' is non-nil.\"\n  (prompter:toggle-mark-all prompt-buffer)\n  (prompt-render-suggestions prompt-buffer))\n\n(define-command-prompt copy-selection (prompt-buffer)\n  \"Save default property of selection to clipboard.\"\n  (let* ((marks (prompter:all-marks prompt-buffer))\n         (props (if marks\n                    (mapcar #'prompter:attributes-default marks)\n                    (list (prompter:attributes-default\n                           (prompter:%current-suggestion\n                            prompt-buffer)))))\n         ;; Reverse so that text is ordered from oldest mark to newest.\n         (text (str:join +newline+ (reverse props))))\n    (unless (str:emptyp text)\n      (trivial-clipboard:text text)\n      (echo \"~s copied to clipboard.\" text))))\n\n(define-command-prompt paste (prompt-buffer)\n  \"Paste clipboard text to input.\"\n  (ffi-buffer-paste prompt-buffer)\n  (nyxt::update-prompt-input prompt-buffer))\n\n(define-command-prompt cut (prompt-buffer)\n  \"Cut the input text to clipboard.\"\n  (ffi-buffer-cut prompt-buffer)\n  (nyxt::update-prompt-input prompt-buffer))\n\n(defun history-entries (&optional (window (current-window)))\n  (and-let* ((first-prompt-buffer (first (nyxt::active-prompt-buffers window))))\n    (containers:container->list\n     (prompter:history first-prompt-buffer))))\n\n(define-class prompt-buffer-history-source (prompter:source)\n  ((prompter:name \"Prompt buffer input history\")\n   (prompter:constructor (history-entries))))\n\n(define-command-prompt history (prompt-buffer)\n  \"Choose a PROMPT-BUFFER input history entry to insert as input.\"\n  (let ((history (prompter:history prompt-buffer)))\n    (if (and history (not (containers:empty-p history)))\n        (let ((input (prompt1 :prompt \"Input history\"\n                              :sources 'prompt-buffer-history-source)))\n          (unless (str:empty? input)\n            (nyxt:set-prompt-buffer-input input)))\n        (echo \"Prompt buffer has no history.\"))))\n\n(define-command-prompt insert-current-suggestion (prompt-buffer)\n  \"Insert current suggestion default property in the PROMPT-BUFFER input.\"\n  (when-let ((selection (prompter:attributes-default\n                         (prompter:%current-suggestion prompt-buffer))))\n    (nyxt:set-prompt-buffer-input selection)))\n\n(define-command-prompt move-start-of-input (prompt-buffer)\n  \"Move to the beginning of PROMPT-BUFFER input.\"\n  (ps-eval :async t :buffer prompt-buffer\n    (let ((input (nyxt/ps:qs document \"#input\")))\n      (setf (ps:@ input selection-start) 0\n            (ps:@ input selection-end) 0))))\n\n(define-command-prompt move-end-of-input (prompt-buffer)\n  \"Move to the end of PROMPT-BUFFER input.\"\n  (ps-eval :async t :buffer prompt-buffer\n    (let ((input (nyxt/ps:qs document \"#input\")))\n      (setf (ps:@ input selection-start) (ps:@ input value length)\n            (ps:@ input selection-end) (ps:@ input value length)))))\n\n(define-command-prompt select-input-text (prompt-buffer)\n  \"Select the prompt buffer's input text.\"\n  (ffi-buffer-select-all prompt-buffer))\n\n;; FIXME: Move scroll.lisp from document-mode so that prompt-buffer.lisp can reach\n;; it.  Ideas?\n\n(define-command-prompt scroll-other-buffer-up\n    (prompt-buffer &key (scroll-distance (scroll-distance (current-buffer))))\n  \"Scroll up the buffer behind the prompt.\"\n  (ps-eval :async t :buffer (current-buffer)\n    (ps:chain window (scroll-by 0 (ps:lisp (- scroll-distance))))))\n\n(define-command-prompt scroll-other-buffer-down\n    (prompt-buffer &key (scroll-distance (scroll-distance (current-buffer))))\n  \"Scroll down the buffer behind the prompt.\"\n  (ps-eval :async t :buffer (current-buffer)\n    (ps:chain window (scroll-by 0 (ps:lisp scroll-distance)))))\n\n(define-command-prompt scroll-page-up-other-buffer (prompt-buffer)\n  \"Scroll up the buffer behind the prompt by one page.\"\n  (ps-eval :async t :buffer (current-buffer)\n    (ps:chain window\n              (scroll-by 0 (- (* (ps:lisp (page-scroll-ratio (current-buffer)))\n                                 (ps:@ window inner-height)))))))\n\n(define-command-prompt scroll-page-down-other-buffer (prompt-buffer)\n  \"Scroll down the buffer behind the prompt by one page.\"\n  (ps-eval :async t :buffer (current-buffer)\n    (ps:chain window (scroll-by 0 (* (ps:lisp (page-scroll-ratio (current-buffer)))\n                                     (ps:@ window inner-height))))))\n\n(defmethod default-modes append ((buffer prompt-buffer)) '(prompt-buffer-mode))\n\n(defmethod default-modes :around ((buffer prompt-buffer))\n  (set-difference (call-next-method)\n                  (list (sym:resolve-symbol :document-mode :mode)\n                        (sym:resolve-symbol :base-mode :mode))))\n\n(define-command describe-prompt-buffer ()\n  \"Describe a prompt buffer instance.\"\n  (describe-bindings :buffer (current-prompt-buffer)))\n"
  },
  {
    "path": "source/mode/proxy.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/mode/proxy\n  (:shadow #:proxy) ; Because of exported slot of the same below.\n  (:documentation \"Package for `proxy-mode' to configure a proxy connection.\n\nProxy mode can be used to do all the networking (optionally\nincluding downloads) via a proxy server.\nIt can also be configured to forward all the networking through local services\nlike Tor.\"))\n(in-package :nyxt/mode/proxy)\n\n(define-mode proxy-mode ()\n  \"Enable forwarding of all network requests to a specific host.\nAs for every mode, it only applies to the current buffer.  If you want to enable\na proxy for all buffers, add it to the list of default modes.\n\nExample to use Tor as a proxy both for browsing and downloading:\n\n\\(define-configuration nyxt/mode/proxy:proxy-mode\n  ((nyxt/mode/proxy:proxy (make-instance 'proxy\n                                         :url (quri:uri \\\"socks5://localhost:9050\\\")\n                                         :allowlist '(\\\"localhost\\\" \\\"localhost:8080\\\")\n                                         :proxied-downloads-p t))))\n\n\\(define-configuration web-buffer\n  ((default-modes (append '(proxy-mode) %slot-value%))))\"\n  ((proxy\n    (make-instance 'nyxt:proxy\n                   :url (quri:uri \"socks5://localhost:9050\")\n                   :allowlist '(\"localhost\" \"localhost:8080\")\n                   :proxied-downloads-p t)\n    :type nyxt:proxy)))\n\n(defmethod enable ((mode proxy-mode) &key)\n  (if (web-buffer-p (buffer mode))\n      (progn\n        (setf (nyxt:proxy (buffer mode)) (proxy mode))\n        (echo \"Buffer ~a proxy set to ~a, allowlisting ~a.\"\n              (id (buffer mode))\n              (render-url (url (proxy mode)))\n              (allowlist (proxy mode))))\n      (echo-warning \"You cannot set the proxy for internal buffers.\")))\n\n(defmethod disable ((mode proxy-mode) &key)\n  (when (web-buffer-p (buffer mode))\n    (setf (nyxt:proxy (buffer mode)) nil)))\n"
  },
  {
    "path": "source/mode/reading-line.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/mode/reading-line\n  (:documentation \"Package for `reading-line-mode', for drawing a line to keep\ntrack of the reading position.\"))\n(in-package :nyxt/mode/reading-line)\n\n(define-mode reading-line-mode ()\n  \"Mode for drawing a line on screen that you can use to keep track of\nyour reading position.\n\nCommands:\n\n- `reading-line-cursor-up' and `reading-line-cursor-down' to move the reading\n  line cursor.\n\n- `jump-to-reading-line-cursor': If you navigate away from the reading line, you\n  can always invoke this command to jump back to your reading position.\"\n  ((visible-in-status-p nil)\n   (keyscheme-map\n    (define-keyscheme-map \"reading-line-mode\" ()\n      keyscheme:cua\n      (list\n       \"M-up\" 'reading-line-cursor-up\n       \"M-down\" 'reading-line-cursor-down)\n      keyscheme:emacs\n      (list\n       \"M-p\" 'reading-line-cursor-up\n       \"M-n\" 'reading-line-cursor-down)\n      keyscheme:vi-normal\n      (list\n       \"K\" 'reading-line-cursor-up\n       \"J\" 'reading-line-cursor-down)))\n   (style (theme:themed-css (theme *browser*)\n            `(\"#reading-line-cursor\"\n              :position \"absolute\"\n              :top \"10px\"\n              :left \"0\"\n              :width \"100%\"\n              :background-color ,theme:primary-color\n              :z-index ,(1- (expt 2 31)) ; 32 bit signed integer max\n              :opacity \"15%\"\n              :height \"20px\"))\n          :documentation \"The CSS applied to the reading line.\")))\n\n(define-command jump-to-reading-line-cursor (&key (buffer (current-buffer)))\n  \"Move the view port to show the reading line cursor.\"\n  (ps-eval :buffer buffer\n    (ps:chain (nyxt/ps:qs document \"#reading-line-cursor\")\n              (scroll-into-view-if-needed))))\n\n(define-command reading-line-cursor-up\n    (&key (step-size 20) (buffer (current-buffer)))\n  \"Move the reading line cursor up.\"\n  (ps-eval :buffer buffer\n    (let ((original-position\n            (ps:chain\n             (parse-int\n              (ps:@\n               (nyxt/ps:qs document \"#reading-line-cursor\") style top) 10))))\n      (setf (ps:@ (nyxt/ps:qs document \"#reading-line-cursor\") style top)\n            (+ (- original-position (ps:lisp step-size)) \"px\"))))\n  (jump-to-reading-line-cursor :buffer buffer))\n\n(define-command reading-line-cursor-down\n    (&key (step-size 20) (buffer (current-buffer)))\n  \"Move the reading line cursor down.\"\n  (ps-eval :buffer buffer\n    (let ((original-position\n            (ps:chain\n             (parse-int\n              (ps:@\n               (nyxt/ps:qs document \"#reading-line-cursor\") style top) 10))))\n      (setf (ps:@ (nyxt/ps:qs document \"#reading-line-cursor\") style top)\n            (+ (+ original-position (ps:lisp step-size)) \"px\"))))\n  (jump-to-reading-line-cursor :buffer buffer))\n\n(defmethod on-signal-load-finished ((mode reading-line-mode) url title)\n  (declare (ignore url title))\n  (enable mode))\n\n(defmethod enable ((mode reading-line-mode) &key)\n  (let ((content (spinneret:with-html-string\n                   (:nstyle (style mode))\n                   (:span :id \"reading-line-cursor\" \"\"))))\n    (ps-eval :async t :buffer (buffer mode)\n      (ps:chain document body\n                (|insertAdjacentHTML| \"afterbegin\" (ps:lisp content)))\n      (setf (ps:@\n             (nyxt/ps:qs document \"#reading-line-cursor\") style top) \"10px\"))))\n\n(defmethod disable ((mode reading-line-mode) &key)\n  (ps-eval :async t :buffer (buffer mode)\n    (setf (ps:@ (nyxt/ps:qs document \"#reading-line-cursor\") |outerHTML|)\n          \"\")))\n"
  },
  {
    "path": "source/mode/repeat.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/mode/repeat\n  (:documentation \"Package for `repeat-mode', a mode to repeat actions.\n\nParts of the API are:\n- `repeat-mode' slots:\n  - `repeat-action': the action to repeat.\n  - `repeat-count': the number of times to repeat the `repeat-action'.\n  - `repeat-interval': the interval to repeat `repeat-action' at.\n- Internal keyboard dispatchers (can be a good example of Nyxt input handling):\n  - `make-repeat-command-dispatcher'.\n  - `skip-repeat-dispatch'.\n\n`repeat-mode' is based on `nyxt/mode/process:process-mode' and customizes the\n`nyxt/mode/process:firing-condition', `nyxt/mode/process:action', and\n`nyxt/mode/process:cleanup' to repeat the action on the dedicated thread.\n\nSee the `repeat-mode' for the external user-facing APIs.\"))\n(in-package :nyxt/mode/repeat)\n\n(define-mode repeat-mode (nyxt/mode/process:process-mode)\n  \"Repeat the execution of a command while enabled.\n\nThe commands that `repeat-mode' exposes are:\n- `repeat-every' to repeat an action every X seconds.\n- `repeat-times' to repeat an action only X times.\n- `repeat-key' to repeat the command bound to the keybinding.\n\nSee `nyxt/mode/repeat' package documentation for implementation details and\ninternal programming APIs.\"\n  ((visible-in-status-p nil)\n   (repeat-count\n    nil\n    :type (or integer null)\n    :documentation \"The number of times to repeat the commands for.\")\n   (repeat-interval\n    1.0\n    :type alex:non-negative-real\n    :documentation \"Time in seconds after which `repeat-action' is repeated.\")\n   (repeat-action\n    nil\n    :type (or null (function (repeat-mode)))\n    :documentation \"The action to repeat.\nIt takes a `repeat-mode' instance as argument.\")\n   (nyxt/mode/process:firing-condition\n    (lambda (path-url mode)\n      (declare (ignore path-url))\n      (when (repeat-interval mode)\n        (sleep (repeat-interval mode)))\n      (cond ((repeat-count mode)\n             (if (zerop (repeat-count mode))\n                 :return\n                 (decf (repeat-count mode))))\n            (t t)))\n    :documentation \"See `nyxt/mode/process:firing-condition'.\")\n   (nyxt/mode/process:action\n    (lambda (path-url mode)\n      (declare (ignore path-url))\n      (funcall* (repeat-action mode) mode))\n    :documentation \"See `nyxt/mode/process:action'.\")\n   (nyxt/mode/process:cleanup\n    (lambda (path-url mode)\n      (declare (ignore path-url))\n      ;; Needed since the mode object might not have been garbage collected.\n      (setf (repeat-action mode) nil\n            (repeat-count mode) nil\n            (repeat-interval mode) 1.0))\n    :documentation \"See `nyxt/mode/process:cleanup'.\")))\n\n(defmethod enable ((mode repeat-mode) &key)\n  ;; TODO: Remember prompt input now that we have prompt-buffer hooks.\n  (unless (repeat-action mode)\n    (let ((prompted-action (prompt1 :prompt \"Command to repeat\"\n                                    :sources 'nyxt:command-source)))\n      (setf (repeat-action mode)\n            (lambda (mode)\n              (declare (ignore mode))\n              (funcall prompted-action))))))\n\n(define-command-global repeat-every (&key (seconds (sera:parse-float\n                                                    (prompt1 :prompt \"Repeat every X seconds\"\n                                                             :input \"5\"\n                                                             :hide-suggestion-count-p t\n                                                             :sources 'prompter:raw-source)\n                                                    :type 'single-float))\n                                     function)\n  \"Prompt for FUNCTION to be run every SECONDS.\"\n  (enable-modes* 'repeat-mode\n                 (current-buffer)\n                 :repeat-interval seconds\n                 :repeat-action function))\n\n(define-command-global repeat-times (&key (times\n                                           (parse-integer\n                                            (prompt1 :prompt \"Repeat for X times\"\n                                                     :input \"4\"\n                                                     :hide-suggestion-count-p t\n                                                     :sources 'prompter:raw-source)))\n                                     function)\n  \"Prompt for FUNCTION to be run a number of TIMES.\"\n  (enable-modes* 'repeat-mode\n                 (current-buffer)\n                 :repeat-count times\n                 :repeat-action function))\n\n(defvar *repeat-times-stack* 0\n  \"The current number of repetitions.\")\n\n(defun make-repeat-command-dispatcher (times)\n  \"Create a command dispatcher that counts the M-digit keys and adds them together.\nOnce a non-number key is pressed, it dispatches this key to a command and starts\nrepeating it like a regular `repeat-mode' does.\"\n  (lambda (command)\n    (if (eq 'repeat-key command)\n        (dispatch-command command)\n        (unwind-protect\n             (repeat-times :times times\n                           :function (lambda (mode)\n                                       (declare (ignore mode))\n                                       (nyxt::run command)))\n          (let ((command-name (name command))\n                (current-buffer (current-buffer)))\n            (echo \"Press a key sequence for command to repeat ~R times: ~a (~a)\"\n                  *repeat-times-stack*\n                  (first (keymaps:pretty-binding-keys\n                          command-name\n                          (current-keymaps current-buffer)\n                          :print-style (keymaps:name (keyscheme current-buffer))))\n                  (string-downcase command-name)))\n          (setf (command-dispatcher *browser*) #'dispatch-command\n                *repeat-times-stack* 0)\n          ;; Benign since each command runs in its own thread (see `nyxt::run').\n          (sleep 2)\n          (echo-dismiss)))))\n\n(define-command-global repeat-key\n    (&key (times (or\n                  (ignore-errors\n                   (parse-integer\n                    (keymaps:key-value (nyxt::last-key (current-buffer)))))\n                  (ignore-errors\n                   (parse-integer\n                    (prompt1 :prompt \"Repeat for X times\"\n                             :input \"4\"\n                             :hide-suggestion-count-p t\n                             :sources 'prompter:raw-source))))))\n  \"Repeat the command bound to the user-pressed keybinding TIMES times.\"\n  (setf *repeat-times-stack* (+ times (* 10 *repeat-times-stack*))\n        (command-dispatcher *browser*) (make-repeat-command-dispatcher *repeat-times-stack*))\n  (echo \"Press a key sequence for command to repeat ~R times:\" *repeat-times-stack*))\n"
  },
  {
    "path": "source/mode/search-buffer.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/mode/search-buffer\n  (:documentation \"Package for `search-buffer-mode', for incremental buffer search.\"))\n(in-package :nyxt/mode/search-buffer)\n\n(define-mode search-buffer-mode ()\n  \"Incremental search on a single or multiple buffers.\"\n  ((visible-in-status-p nil)\n   (style\n    (theme:themed-css (theme *browser*)\n      `(\"span[nyxt-search-mark]\"\n        :background-color ,theme:secondary-color !important\n        :color ,theme:on-secondary-color !important\n        :border-radius 2px\n        :z-index #.(1- (expt 2 31)))\n      `(\"span[nyxt-search-mark].nyxt-current-search-mark\"\n        :background-color ,theme:highlight-color !important)\n      '(:keyframes \"mark\"\n                   (0% :background-position 0 0%)\n                   (100% :background-position 0 100%)))\n    :documentation \"The style of the search overlays.\")\n   (keyscheme-map\n    (define-keyscheme-map \"search-buffer-mode\" ()\n      keyscheme:cua\n      (list\n       \"C-f\" 'search-buffer\n       \"f3\" 'search-buffer\n       \"M-f\" 'remove-search-marks)\n      keyscheme:emacs\n      (list\n       \"C-s s\" 'search-buffer\n       \"C-s k\" 'remove-search-marks)\n      keyscheme:vi-normal\n      (list\n       \"/\" 'search-buffer\n       \"?\" 'remove-search-marks))))\n  (:toggler-command-p nil))\n\n(define-configuration document-buffer\n  ((default-modes (cons 'search-buffer-mode %slot-value%))))\n\n(define-class search-match ()\n  ((pattern\n    \"\"\n    :type string\n    :documentation \"The requested search pattern.\")\n   (body\n    \"\"\n    :type string\n    :documentation \"The full context of the match.\nIt is the concatenation of text nodes that constitute the match.\")\n   (buffer\n    nil\n    :type (maybe buffer)\n    :documentation \"The buffer where the match is found.\")\n   (marked-p\n    nil\n    :type boolean\n    :writer nil\n    :reader marked-p\n    :documentation \"Whether the match is shown in its corresponding `buffer'.\nRequires running JavaScript code.\")\n   (nodes\n    '()\n    :type (list-of plump:node)\n    :documentation \"The list of text nodes where the match is found.\")\n   (id\n    0\n    :type alex:non-negative-fixnum\n    :documentation \"The unique identifier.\nUseful to reference the match via CSS selectors.\")\n   (identifier-beg\n    \"\"\n    :type (maybe string)\n    :documentation \"DOM coordinate that marks the beginning of the match.\")\n   (node-index-beg\n    0\n    :type alex:non-negative-fixnum\n    :documentation \"DOM coordinate that marks the beginning of the match.\")\n   (text-index-beg\n    0\n    :type alex:non-negative-fixnum\n    :documentation \"DOM coordinate that marks the beginning of the match.\")\n   (identifier-end\n    \"\"\n    :type (maybe string)\n    :documentation \"DOM coordinate that marks the end of the match.\")\n   (node-index-end\n    0\n    :type alex:non-negative-fixnum\n    :documentation \"DOM coordinate that marks the end of the match.\")\n   (text-index-end\n    0\n    :type alex:non-negative-fixnum\n    :documentation \"DOM coordinate that marks the end of the match.\"))\n  (:documentation \"A `search-match' captures the means to manipulate matches via\ntwo complementary ways: (1) the Lisp-side DOM (powered by `plump' and\n`nyxt/dom'), and (2) Javascript.\"))\n\n(defmethod (setf marked-p) (value (match search-match))\n  (when value (mark match))\n  (setf (slot-value match 'marked-p) value))\n\n(defmethod css-selector ((match search-match))\n  \"Return a CSS selector that uniquely identifies MATCH.\"\n  (format nil \"span[nyxt-search-mark=\\\"~a\\\"]\" (id match)))\n\n(defmethod mark ((match search-match))\n  \"Mark MATCH in its corresponding buffer.\n\nThe DOM is mutated via Javascript by wrapping MATCH around a span element.  In\nsome cases, when MATCH spans multiple text nodes, multiple span elements wrap\nMATCH.\n\nStyle it via CSS selector \\\"[nyxt-search-mark]\\\".\"\n  (ps-eval :async t :buffer (buffer match)\n    ;; StaticRange may improve performance at the cost of correctness.\n    (defun create-range () (ps:chain document (create-range)))\n\n    (defun create-match-element (value)\n      (let ((elem (ps:chain document (create-element \"span\"))))\n        (ps:chain elem (set-attribute \"nyxt-search-mark\" value))\n        elem))\n\n    (defun wrap (range new-parent) (ps:chain range (surround-contents new-parent)))\n\n    (defun test-node (node) (ps:chain (eq node this)))\n\n    (defun text-nodes-within-bounds (root node-beg node-end)\n      \"Return text nodes under ROOT, bounded by NODE-BEG and NODE-END.\"\n      ;; No need to raise an error when node-beg/end don't descend from root\n      ;; since, in the particular context of `mark', it always holds true.\n      (let ((nodes '())\n            ;; Tersely written due to PS limitations.\n            ;; 4 means that only text nodes are collected by the generator.\n            ;; https://developer.mozilla.org/en-US/docs/Web/API/TreeWalker/whatToShow\n            (walker (ps:chain document (create-tree-walker root 4 null false))))\n        (loop for text-node = (ps:chain walker (next-node))\n              while text-node\n              do (ps:chain nodes (push text-node)))\n        (let ((beg (ps:chain nodes (find-index test-node node-beg)))\n              (end (1+ (ps:chain nodes (find-last-index test-node node-end)))))\n          (setf nodes (ps:chain nodes (slice beg end))))))\n\n    (let* ((id (ps:lisp (id match)))\n           (elem-beg (nyxt/ps:qs-nyxt-id (ps:@ document body)\n                                         (ps:lisp (identifier-beg match))))\n           (elem-end (nyxt/ps:qs-nyxt-id (ps:@ document body)\n                                         (ps:lisp (identifier-end match))))\n           (node-beg (ps:@ elem-beg child-nodes (ps:lisp (node-index-beg match))))\n           (node-end (ps:@ elem-end child-nodes (ps:lisp (node-index-end match))))\n           (text-beg (ps:lisp (text-index-beg match)))\n           (text-end (ps:lisp (text-index-end match)))\n           (range (create-range)))\n      (ps:chain range (set-start node-beg text-beg))\n      (ps:chain range (set-end node-end text-end))\n      (ps:try\n       ;; Errors when a node is partially selected by the Range.\n       ;; https://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#td-partially-selected.\n       (wrap range (create-match-element id))\n       (:catch (error)\n         ;; If there are partially selected nodes, wrap each of the text nodes.\n         (let* ((root-elem (if (ps:chain elem-beg (contains node-end))\n                               elem-beg\n                               (ps:@ elem-end parent-element)))\n                (nodes (text-nodes-within-bounds root-elem node-beg node-end)))\n           (loop with last-index = (1- (ps:chain nodes length))\n                 for i from 0 to last-index\n                 do (cond ((= i 0)\n                           (let ((range (create-range)))\n                             (ps:chain range (select-node-contents node-beg))\n                             (ps:chain range (set-start node-beg text-beg))\n                             (wrap range (create-match-element id))))\n                          ((= i last-index)\n                           (let ((range (create-range)))\n                             (ps:chain range (select-node-contents node-end))\n                             (ps:chain range (set-end node-end text-end))\n                             (wrap range (create-match-element id))))\n                          (t\n                           (let ((range (create-range)))\n                             (ps:chain range (select-node-contents (ps:getprop nodes i)))\n                             (wrap range (create-match-element id))))))))))))\n\n(defmethod mark-alternate ((match search-match) &key (scroll t))\n  \"Mark MATCH and optionally SCROLL it into view.\n\nDiffers from `mark' in the sense that is allows for a more refined styling.\nThis is particularly useful when MATCH needs to stand out from others matches.\n\nStyle it via CSS selector \\\".nyxt-current-search-mark\\\".\"\n  (ps-eval :buffer (buffer match)\n    ;; Note that multiple span elements may feature class\n    ;; .nyxt-current-search-mark.  I.e., to a single match may correspond\n    ;; several span elements.\n    (ps:dolist (elem (nyxt/ps:qsa (ps:@ document body)\n                                  \"span[nyxt-search-mark].nyxt-current-search-mark\"))\n      (ps:chain elem class-list (remove \"nyxt-current-search-mark\")))\n    (let ((match-selector (ps:lisp (css-selector match))))\n      (ps:dolist (elem (nyxt/ps:qsa (ps:@ document body) match-selector))\n        (ps:chain elem class-list (add \"nyxt-current-search-mark\")))\n      (when (ps:lisp scroll)\n        (let ((match (nyxt/ps:qs (ps:@ document body) match-selector)))\n          (when match\n            (ps:chain match (scroll-into-view (ps:create block \"center\")))\n            (ps:chain match parent-element (focus))))))))\n\n(defmethod invisible-p ((match search-match))\n  \"Whether MATCH is invisible in its corresponding buffer.\"\n  (ps-eval :buffer (buffer match)\n    (let ((elem (nyxt/ps:qs (ps:@ document body) (ps:lisp (css-selector match)))))\n          (and elem (nyxt/ps:element-invisible-p elem)))))\n\n;; More powerful than sera:ellipsize or str:shorten.\n;; TODO Add tests.\n(defun centered-ellipsize (str beg end &key (len-max 80) (ellipsis \"[...]\"))\n  \"Return a substring of STR of length LEN-MAX, at most.\n\nThe substring of STR bounded by BEG and END is centered in the returned\ntruncated string, and ELLIPSIS is added at the boundary when needed.\"\n  (let ((len-str (length str))\n        (len-ellipsis (length ellipsis))\n        (len-match (1+ (- end beg))))\n    (cond ((or (< beg 0) (> end len-str))\n           (error \"Match out of bounds.\"))\n          ((>= len-max len-str) str)\n          ((> len-match len-max)\n           (str:concat (subseq str 0 (- len-max len-ellipsis))\n                       ellipsis))\n          ((> len-str len-max)\n           (let* ((delta (floor (/ (- len-max len-match) 2)))\n                  (new-beg (max 0 (- beg delta)))\n                  (new-end (min len-str (+ end delta)))\n                  (beg-omitted-p (not (zerop new-beg)))\n                  (end-omitted-p (not (= new-end len-str))))\n             (str:concat (when beg-omitted-p ellipsis)\n                         (subseq str\n                                 (if beg-omitted-p (+ new-beg len-ellipsis) new-beg)\n                                 (if end-omitted-p (- new-end len-ellipsis) new-end))\n                         (when end-omitted-p ellipsis)))))))\n\n(defmethod prompter:object-attributes ((match search-match) (source prompter:source))\n  `((\"Match ID\" ,(id match) (:width 1))\n    (\"Text\" ,(centered-ellipsize\n              (body match)\n              (text-index-beg match)\n              (if (sera:single (nodes match))\n                  (text-index-end match)\n                  (+ (reduce #'+\n                             (butlast (nodes match))\n                             :key (lambda (i) (length (plump:text i))))\n                     (text-index-end match))))\n            (:width 12))))\n\n(defun search-contiguous (pattern str &key (found-pattern nil)\n                                        (full-match-p nil)\n                                        (test #'string=))\n  \"Search for PATTERN in STR given that FOUND-PATTERN has been observed.\n\nWhen a match is found, return the substring of PATTERN and a list of position\nindices relative to STR.\n\nTEST is a function of 2 arguments that returns a boolean.  It determines what\nqualifies as a match.\n\nNote that only contiguous matches are considered.  For example:\n(search-contiguous \\\"match\\\" \\\"a m\\\" :found-pattern \\\"m\\\") -> NIL\n(search-contiguous \\\"match\\\" \\\"m a\\\" :found-pattern \\\"m\\\") -> NIL\n(search-contiguous \\\"match\\\" \\\"a\\\"   :found-pattern \\\"m\\\") -> (values \\\"ma\\\" (0 1))\"\n  (declare (type string pattern) (type string str))\n  (cond\n    ((or (str:empty? pattern) (str:empty? str)) nil)\n    ((string= \"\" (str:prefix (list found-pattern pattern)))\n     (error (format nil \"~a isn't a prefix of ~a\" found-pattern pattern)))\n    ((str:empty? found-pattern)\n     (loop with delta = (if full-match-p\n                            pattern\n                            (subseq pattern 0 (1- (length pattern))))\n           with len-str = (length str)\n           for i downfrom (min (length delta) (length str)) to 1\n           for beg = (search delta str :end1 i :start2 (- len-str i) :test test)\n           when beg\n             do (return (values (subseq delta 0 i) (list beg (+ beg i))))\n             and do (loop-finish)))\n    (t\n     (when-let* ((delta (sera:string-replace found-pattern pattern \"\"))\n                 (len-delta (min (length delta) (length str)))\n                 (beg (search delta str :end1 len-delta :end2 len-delta :test test)))\n       (values (str:concat found-pattern (subseq delta 0 len-delta))\n               (list beg (+ beg len-delta)))))))\n\n(defun search-all (pattern str &key (test #'string=))\n  \"Return all pairs of indices where PATTERN is found in STR.\n\nTEST is a function of 2 arguments that returns a boolean.  It determines what\nqualifies as a match.\"\n  (declare (type string pattern) (type string str))\n  (unless (string-equal \"\" pattern)\n    (loop with match-indices with len = (length pattern) with beg = 0\n          while beg\n          when (setf beg (search pattern str :start2 beg :test test))\n            do (push (list beg (incf beg len)) match-indices)\n          finally (return (nreverse match-indices)))))\n\n(export-always 'search-document)\n(defun search-document (pattern &key buffer node (mark-p nil) (test #'string-equal))\n  \"Search for PATTERN in BUFFER's DOM NODE.\n\nNODE is a `plump' or `nyxt/dom' object.\n\nTEST is a function of 2 arguments that returns a boolean.  It determines what\nqualifies as a match.\n\nMARK-P accepts the following values:\n- NIL, to disable marking matches;\n- T, to mark all matches;\n- INTEGER, to mark those many matches.\"\n  (let ((matches) (partial-match) (seen) (idx 0))\n    (labels\n        ((traverse-dfs (node)\n           \"Traverse NODE depth-first-search and collect search matches.\"\n           (loop for child across (plump:children node) and index from 0\n                 do (typecase child\n                      ;; Filter style, script and noscript elements.\n                      (plump:fulltext-element)\n                      (plump:nesting-node (traverse-dfs child))\n                      (plump:text-node\n                       (let ((text (plump:text child)))\n                         ;; Matches circumscribed to a single node\n                         (loop with id = (nyxt/dom:get-nyxt-id node)\n                               for match in (search-all pattern text :test test)\n                               do (push (make-instance\n                                         'search-match\n                                         :pattern pattern\n                                         :body text\n                                         :buffer buffer\n                                         :nodes (list child)\n                                         :id (incf idx)\n                                         :identifier-beg id\n                                         :node-index-beg index\n                                         :text-index-beg (first match)\n                                         :identifier-end id\n                                         :node-index-end index\n                                         :text-index-end (second match))\n                                        matches))\n                         ;; Matches spanning multiple nodes\n                         (multiple-value-bind (patt bounds)\n                             ;; Either match with what has been found thus\n                             ;; far, or from scratch.\n                             (if (search-contiguous pattern text :found-pattern seen :test test)\n                                 (search-contiguous pattern text :found-pattern seen :test test)\n                                 (search-contiguous pattern text :test test))\n                           (setf seen patt)\n                           (cond ((str:empty? seen)\n                                  (setf partial-match nil))\n                                 ((null partial-match)\n                                  (setf partial-match\n                                        (make-instance\n                                         'search-match\n                                         :pattern pattern\n                                         :buffer buffer\n                                         :nodes (list child)\n                                         :identifier-beg (nyxt/dom:get-nyxt-id node)\n                                         :node-index-beg index\n                                         :text-index-beg (first bounds))))\n                                 ((not (funcall test seen pattern))\n                                  (setf (nodes partial-match)\n                                        (append (nodes partial-match) (list child))))\n                                 (t\n                                  (setf (nodes partial-match)\n                                        (append (nodes partial-match) (list child))\n                                        (body partial-match)\n                                        (apply #'concatenate\n                                               'string\n                                               (mapcar #'plump:text (nodes partial-match)))\n                                        (id partial-match)\n                                        (incf idx)\n                                        (identifier-end partial-match)\n                                        (nyxt/dom:get-nyxt-id node)\n                                        (node-index-end partial-match)\n                                        index\n                                        (text-index-end partial-match)\n                                        (second bounds))\n                                  (push partial-match matches)\n                                  (setf partial-match nil\n                                        seen nil))))))))))\n      (traverse-dfs node))\n    ;; Search marks logic.\n    (cond ((null mark-p)\n           (setf matches (nreverse matches)))\n          ((integerp mark-p)\n           (setf matches (nreverse matches))\n           (loop for match in (nreverse (sera:firstn mark-p matches))\n                 do (setf (marked-p match) t))\n           matches)\n          (t\n           (loop for match in matches\n                 do (setf (marked-p match) t))\n           (setf matches (nreverse matches))))))\n\n(define-command remove-search-marks (&optional (buffer (current-buffer)))\n  \"Remove all search marks.\"\n  (ps-eval :buffer buffer\n    (dolist (match (nyxt/ps:qsa (ps:@ document body) \"span[nyxt-search-mark]\"))\n      (let ((parent (ps:chain match parent-element)))\n        (ps:chain match (insert-adjacent-h-t-m-l \"beforebegin\"\n                                                 (ps:@ match inner-h-t-m-l)))\n        (ps:chain match (remove))\n        ;; Ensure text nodes aren't empty and adjacent ones are concatenated.\n        (ps:chain parent (normalize))))))\n\n(defun maybe-remove-search-marks (&optional (buffer (current-buffer)))\n  (unless (keep-search-marks-p buffer) (remove-search-marks buffer)))\n\n(define-class search-buffer-source (prompter:source)\n  ((prompter:name \"Matches\")\n   (buffer (current-buffer))\n   (test-function\n    nil\n    :type (or null function)\n    :documentation \"The function that determines whether a search match is found.\n\nWhen nil, the logic behind `smart-case-test' is applied, i.e. the search becomes\ncase sensitive if upper case characters are used in the query.\n\nSet it to perform case-insensitive queries only:\n\\(define-configuration nyxt/mode/search-buffer:search-buffer-source\n  ((nyxt/mode/search-buffer:test-function #'string-equal)))\")\n   (maximum-marked-matches\n    1000\n    :type integer\n    :documentation \"Maximum number of marked search matches.\nThe possible values are:\n- NIL, to disable marking matches;\n- T, to mark all matches;\n- INTEGER, to mark those many matches.\n\nNote that it doesn't set an upper bound on the number of matches returned by\n`search-document'.  It only limits the number of marks added by the web\nrenderer, as it is an expensive computation.\")\n   (initial-delay\n    0.25\n    :documentation \"Seconds to wait before searching.\nTakes effect when the search pattern's length is less than `no-delay-length'.\")\n   (no-delay-length\n    3\n    :documentation \"Search starts immediately for patterns at least this long.\nFor shorter search patterns, `initial-delay' applies.\")\n   (prompter:actions-on-current-suggestion-enabled-p t)\n   (prompter:filter nil)\n   (prompter:filter-preprocessor\n    (lambda (preprocessed-suggestions source input)\n      (declare (ignore preprocessed-suggestions))\n      (let ((buffer (buffer source)))\n        (remove-search-marks buffer)\n        (unless (str:empty? input)\n          (when (< (length input) (no-delay-length source))\n            ;; Allow time for next keystroke to avoid long computations (see\n            ;; `prompter::update-thread').\n            (sleep (initial-delay source)))\n          (search-document input\n                           :buffer buffer\n                           :node (elt (clss:select \"body\" (document-model buffer)) 0)\n                           :test (or (test-function source) (smart-case-test input))\n                           :mark-p (maximum-marked-matches source))))))\n   (prompter:actions-on-current-suggestion\n    (lambda-command mark-match (suggestion)\n      \"Scroll to search match.\"\n      (set-current-buffer (buffer suggestion) :focus nil)\n      (maybe-update-marks suggestion (current-source))\n      (when (invisible-p suggestion)\n        (setf (slot-value (current-source) 'prompter:suggestions)\n              (delete suggestion\n                      (slot-value (current-source) 'prompter:suggestions)\n                      :key #'prompter:value))\n        ;; FIXME Hack that enables the above deletion to cascade.\n        ;; See https://github.com/atlas-engineer/nyxt/issues/2894.\n        (prompter:run-action-on-current-suggestion (current-prompt-buffer)))\n      (mark-alternate suggestion)))\n   (prompter:actions-on-return\n    (lambda-command maybe-remove-search-marks (marks)\n      (let ((match (first marks)))\n        (maybe-remove-search-marks (buffer match))\n        match)))\n   (prompter:constructor\n    (lambda (source) (add-stylesheet \"nyxt-search-stylesheet\"\n                                     (style (find-submode 'search-buffer-mode))\n                                     (buffer source))))\n   (prompter:destructor\n    (lambda (prompter source)\n      (declare (ignore source))\n      (let ((search-buffers (mapcar #'buffer (prompter:sources prompter))))\n        (mapcar #'maybe-remove-search-marks search-buffers)))))\n  (:export-accessor-names-p t)\n  (:export-class-name-p t)\n  (:metaclass user-class)\n  (:documentation \"The source for search suggestions.\nFetches all the suggestions in `buffer' that match the input as per `test-function'.\"))\n\n(defmethod maybe-update-marks (current-match (source search-buffer-source))\n  \"Recompute search marks, if needed.\n\nAt any given time, only `maximum-marked-matches' matches are marked.\"\n  (unless (marked-p current-match)\n    (let* ((matches (mapcar #'prompter:value (prompter:suggestions source)))\n           (match-batch (find current-match\n                              (sera:batches matches (maximum-marked-matches source))\n                              :test #'member)))\n      (remove-search-marks (buffer source))\n      (mapcar (lambda (match) (setf (marked-p match) nil)) matches)\n      (mapcar (lambda (match) (setf (marked-p match) t)) (nreverse match-batch)))))\n\n(define-command search-buffer ()\n  \"Search incrementally on the current buffer.\nTo remove the search marks when closing the search prompt, set DOCUMENT-BUFFER's\n`keep-search-marks-p' slot to nil by adding the following to the config file:\n\n  (define-configuration document-buffer\n    ((keep-search-marks-p nil)))\"\n  (prompt :prompt \"Search text\"\n          :sources (make-instance 'search-buffer-source)))\n\n(define-command search-buffers ()\n  \"Search incrementally in multiple buffers.\"\n  (let ((buffers (prompt :prompt \"Search in buffer(s)\"\n                         :sources (make-instance 'buffer-source\n                                                 :actions-on-return #'identity))))\n    (prompt :prompt \"Search text\"\n            :sources (mapcar (lambda (buffer)\n                               (make-instance\n                                'search-buffer-source\n                                :name (format nil \"Matches from ~a\" (url buffer))\n                                :buffer buffer))\n                             buffers))))\n"
  },
  {
    "path": "source/mode/small-web.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/mode/small-web\n  (:documentation \"Package for `small-web-mode', which powers Gopher/Gemini page interaction.\"))\n(in-package :nyxt/mode/small-web)\n\n(define-mode small-web-mode ()\n  \"Gopher/Gemini page interaction mode.\n\nRenders gopher elements (provided by `cl-gopher') to human-readable HTML.\n\nThe default style is rendering info messages to <pre> text, inlining\nimages/sounds and showing everything else as buttons.\n\nThe rendering of pages is done via the `render' method, while rendering of\nseparate lines constituting a page is done in `line->html'. If you're\nunsatisfied with how pages are rendered, override either of the two.\n\nFor example, if you want to render images as links instead of inline image\nloading, you'd need to override `line->html' in the following way:\n\n\\(defun image->link (line)\n  (spinneret:with-html-string\n    (:a :href (cl-gopher:uri-for-gopher-line line)\n        (cl-gopher:display-string line))))\n\n\\(defmethod line->html ((line cl-gopher:image)) (image->link line))\n\\(defmethod line->html ((line cl-gopher:gif)) (image->link line))\n\\(defmethod line->html ((line cl-gopher:png)) (image->link line))\n\nGemini support is a bit more brittle, but you can override `line->html' for\n`phos/gemtext' elements too.\"\n  ((visible-in-status-p nil)\n   (url :documentation \"The URL being opened.\")\n   (model :documentation \"The contents of the current page.\")\n   (redirections nil :documentation \"The list of redirection Gemini URLs.\")\n   (max-redirections\n    5\n    :documentation \"The maximum number of times a redirection is attempted.\")\n   (style (theme:themed-css (nyxt::theme *browser*)\n            `(body\n              :background-color ,theme:background-color)\n            `(pre\n              :background-color ,theme:secondary-color\n              :padding \"2px\"\n              :margin \"0\"\n              :border-radius 0)\n            '(.button\n              :margin \"0 3px 3px 0\"\n              :font-size \"15px\")\n            `(.search\n              :background-color ,theme:action-color\n              :color ,theme:on-action-color)\n            `(.error\n              :background-color ,theme:warning-color\n              :color ,theme:on-warning-color\n              :padding \"1em 0\"))))\n  (:toggler-command-p nil))\n\n;;; Gopher rendering.\n\n(defmethod cl-gopher:display-string :around ((line cl-gopher:gopher-line))\n  (cl-ppcre:regex-replace-all \"\\\\e\\\\[[\\\\d;]*[A-Za-z]\"\n                              (slot-value line 'cl-gopher:display-string)\n                              \"\"))\n\n(export-always 'line->html)\n(defgeneric line->html (line)\n  (:documentation \"Transform a Gopher or Gemini line to a reasonable HTML representation.\"))\n\n(export-always 'gopher-render)\n(defgeneric gopher-render (line)\n  (:documentation \"Produce a Gopher page content string/array given LINE.\nSecond return value should be the MIME-type of the content.\n\nImplies that `small-web-mode' is enabled.\"))\n\n(defmethod line->html ((line cl-gopher:gopher-line))\n  (spinneret:with-html-string\n    (:pre \"[\" (symbol-name (class-name (class-of line))) \"] \"\n          (cl-gopher:display-string line)\n          \" (\" (cl-gopher:uri-for-gopher-line line) \")\")))\n\n(defmethod line->html ((line cl-gopher:error-code))\n  (spinneret:with-html-string\n    (:pre :class \"error\"\n          \"Error: \" (cl-gopher:display-string line))))\n\n(defmethod line->html ((line cl-gopher:info-message))\n  (let ((line (cl-gopher:display-string line)))\n    (spinneret:with-html-string\n      (if (str:blankp line)\n          (:br)\n          (:pre line)))))\n\n(defmethod line->html ((line cl-gopher:submenu))\n  (spinneret:with-html-string\n    (:a :class \"button\" :href (cl-gopher:uri-for-gopher-line line)\n        (cl-gopher:display-string line))))\n\n(defun image->html (line)\n  (let ((uri (cl-gopher:uri-for-gopher-line line)))\n    (spinneret:with-html-string\n      (:a :href uri\n          (:img :src uri\n                :alt (cl-gopher:display-string line))))))\n\n(defmethod line->html ((line cl-gopher:image)) (image->html line))\n(defmethod line->html ((line cl-gopher:gif)) (image->html line))\n(defmethod line->html ((line cl-gopher:png)) (image->html line))\n\n(defmethod line->html ((line cl-gopher:sound-file))\n  (spinneret:with-html-string\n    (:audio :src (cl-gopher:uri-for-gopher-line line)\n            :controls t\n            (cl-gopher:display-string line))))\n\n(defmethod line->html ((line cl-gopher:search-line))\n  (spinneret:with-html-string\n    (:a :class \"button search\"\n        :href (cl-gopher:uri-for-gopher-line line)\n        (:b \"[SEARCH] \") (cl-gopher:display-string line))))\n\n(defmethod line->html ((line cl-gopher:html-file))\n  (let ((selector (cl-gopher:selector line)))\n    (spinneret:with-html-string\n      (:a :class \"button\"\n          :href (if (str:starts-with-p \"URL:\" selector)\n                    (sera:slice selector 4)\n                    selector)\n          (cl-gopher:display-string line))\n      (:br))))\n\n(defmethod line->html ((line cl-gopher:text-file))\n  (spinneret:with-html-string\n    (:a :class \"button\"\n        :href (cl-gopher:uri-for-gopher-line line)\n        (cl-gopher:display-string line))\n    (:br)))\n\n(defun file-link->html (line)\n  (spinneret:with-html-string\n    (:a :class \"button\"\n        :style (format nil \"background-color: ~a\" (theme:primary-color (theme *browser*)))\n        :href (cl-gopher:uri-for-gopher-line line)\n        (:b \"[FILE] \") (cl-gopher:display-string line))\n    (:br)))\n\n(defmethod line->html ((line cl-gopher:binary-file)) (file-link->html line))\n(defmethod line->html ((line cl-gopher:binhex-file)) (file-link->html line))\n(defmethod line->html ((line cl-gopher:dos-file)) (file-link->html line))\n(defmethod line->html ((line cl-gopher:uuencoded-file)) (file-link->html line))\n(defmethod line->html ((line cl-gopher:unknown)) (file-link->html line))\n\n(defmethod gopher-render ((line cl-gopher:gopher-line))\n  (when-let ((contents (cl-gopher:get-line-contents line))\n             (spinneret:*html-style* :tree)\n             (mode (find-submode 'small-web-mode)))\n    (setf (model mode) contents)\n    (values (spinneret:with-html-string\n              (:nstyle (style (current-buffer)))\n              (:nstyle (style mode))\n              (loop for line in (cl-gopher:lines contents)\n                    collect (:raw (line->html line))))\n            \"text/html;charset=utf8\")))\n\n(defmethod gopher-render ((line cl-gopher:html-file))\n  (let ((contents (cl-gopher:get-line-contents line)))\n    (values (cl-gopher:content-string contents) \"text/html;charset=utf8\")))\n\n(defmethod gopher-render ((line cl-gopher:text-file))\n  (let ((contents (cl-gopher:get-line-contents line)))\n    ;; TODO: Guess encoding?\n    (values (str:join +newline+ (cl-gopher:lines contents)) \"text/plain;charset=utf8\")))\n\n(defun render-binary-content (line &optional mime)\n  (let* ((url (quri:uri (cl-gopher:uri-for-gopher-line line)))\n         (file (pathname (quri:uri-path url)))\n         (mime (or mime (mimes:mime file)))\n         (contents (cl-gopher:get-line-contents line)))\n    (values (cl-gopher:content-array contents) mime)))\n\n(defmethod gopher-render ((line cl-gopher:image)) (render-binary-content line))\n(defmethod gopher-render ((line cl-gopher:binary-file)) (render-binary-content line))\n(defmethod gopher-render ((line cl-gopher:binhex-file)) (render-binary-content line))\n(defmethod gopher-render ((line cl-gopher:dos-file)) (render-binary-content line))\n(defmethod gopher-render ((line cl-gopher:uuencoded-file)) (render-binary-content line))\n(defmethod gopher-render ((line cl-gopher:gif)) (render-binary-content line \"image/gif\"))\n(defmethod gopher-render ((line cl-gopher:png)) (render-binary-content line \"image/png\"))\n\n(define-internal-scheme \"gopher\"\n    (lambda (url)\n      (handler-case\n          (let ((line (if (uiop:emptyp (quri:uri-path (quri:uri url)))\n                          (ffi-buffer-load (current-buffer) (str:concat url \"/\"))\n                          (cl-gopher:parse-gopher-uri url))))\n            (enable-modes :modes '(small-web-mode))\n            (if (and (typep line 'cl-gopher:search-line)\n                     (uiop:emptyp (cl-gopher:terms line)))\n                (progn (setf (cl-gopher:terms line)\n                             (prompt1 :prompt (format nil \"Search query for ~a\" url)\n                                      :sources 'prompter:raw-source))\n                       (ffi-buffer-load (current-buffer) (cl-gopher:uri-for-gopher-line line)))\n                (with-current-buffer (current-buffer) (gopher-render line))))\n        (cl-gopher:bad-submenu-error ()\n          (error-help (format nil \"Malformed line at ~s\" url)\n                      (format nil \"One of the lines on this page has an improper format.\nPlease report this to the server admin.\")))\n        (cl-gopher:bad-uri-error ()\n          (error-help (format nil \"Malformed URL: ~s\" url)\n                      (format nil \"The URL you inputted most probably has a typo in it.\nPlease, check URL correctness and try again.\")))\n        (condition (condition)\n          (error-help \"Unknown error\"\n                      (format nil \"Original text of ~a:~%~a\" (type-of condition) condition))))))\n\n;;; Gemini rendering.\n\n(defmethod line->html ((element gemtext:element))\n  (spinneret:with-html-string\n    (:pre (gemtext:text element))))\n\n(defmethod line->html ((element gemtext:paragraph))\n  (spinneret:with-html-string\n    (:p (gemtext:text element))))\n\n(defmethod line->html ((element gemtext:title))\n  (spinneret:with-html-string\n    (case (gemtext:level element)\n      (1 (:h1 (gemtext:text element)))\n      (2 (:h2 (gemtext:text element)))\n      (3 (:h3 (gemtext:text element))))))\n\n;; TODO: We used to build <ul>-lists out of those. Should we?\n(defmethod line->html ((element gemtext:item))\n  (spinneret:with-html-string\n    (:li (gemtext:text element))))\n\n(defmethod line->html ((element gemtext:link))\n  (spinneret:with-html-string\n    (let* ((url (render-url (gemtext:url element)))\n           (path (quri:uri-path (gemtext:url element)))\n           (mime (unless (uiop:emptyp path)\n                   (mimes:mime-lookup path)))\n           (text (cond\n                   ((not (uiop:emptyp (gemtext:text element)))\n                    (gemtext:text element))\n                   ((not (uiop:emptyp url))\n                    url)\n                   (t \"[LINK]\"))))\n      (cond\n        ((str:starts-with-p \"image/\" mime)\n         (:a :href url (:img :src url :alt text)))\n        ((str:starts-with-p \"audio/\" mime)\n         (:audio :src url :controls t text))\n        ((str:starts-with-p \"video/\" mime)\n         (:video :src url :controls t))\n        (t (:a :class \"button\" :href url text))))\n    (:br)))\n\n(export-always 'gemtext-render)\n(defun gemtext-render (gemtext &optional (buffer (current-buffer)))\n  \"Renders the Gemtext (Gemini markup format) to HTML.\n\nImplies that `small-web-mode' is enabled.\"\n  (let ((mode (find-submode 'small-web-mode buffer))\n        (elements (phos/gemtext:parse-string gemtext))\n        (spinneret::*html-style* :tree))\n    (setf (model mode) elements)\n    (values (spinneret:with-html-string\n              (:nstyle (style buffer))\n              (when mode\n                (:nstyle (style mode)))\n              (loop for element in elements\n                    collect (:raw (nyxt/mode/small-web:line->html element))))\n            \"text/html;charset=utf8\")))\n\n(define-internal-scheme \"gemini\"\n    (lambda (url)\n      (handler-case\n          (sera:mvlet* ((status meta body (gemini:request url)))\n            (enable-modes :modes '(small-web-mode))\n            (unless (member status '(:redirect :permanent-redirect))\n                (setf (nyxt/mode/small-web:redirections (find-submode 'small-web-mode)) nil))\n              (case status\n                ((:input :sensitive-input)\n                 (let ((text (quri:url-encode\n                              (handler-case\n                                  (prompt1 :prompt meta\n                                           :sources 'prompter:raw-source\n                                           :height :fit-to-prompt\n                                           :invisible-input-p (eq status :sensitive-input))\n                                (prompt-buffer-canceled () \"\")))))\n                   (ffi-buffer-load (current-buffer) (str:concat url \"?\" text))\n                   nil))\n                (:success\n                 (if (str:starts-with-p \"text/gemini\" meta)\n                     (gemtext-render body)\n                     (values body meta)))\n                ((:redirect :permanent-redirect)\n                 (push url (nyxt/mode/small-web:redirections (find-submode 'small-web-mode)))\n                 (if (< (length (nyxt/mode/small-web:redirections (find-submode 'small-web-mode)))\n                        (nyxt/mode/small-web:max-redirections (find-submode 'small-web-mode)))\n                     (progn (ffi-buffer-load (current-buffer) (quri:merge-uris (quri:uri meta) (quri:uri url))) nil)\n                     (error-help\n                      \"Error\"\n                      (format nil \"The server has caused too many (~a+) redirections.~& ~a~{ -> ~a~}\"\n                              (nyxt/mode/small-web:max-redirections (find-submode 'small-web-mode))\n                              (alex:lastcar (nyxt/mode/small-web:redirections (find-submode 'small-web-mode)))\n                              (butlast (nyxt/mode/small-web:redirections (find-submode 'small-web-mode)))))))\n                ((:temporary-failure :server-unavailable :cgi-error :proxy-error\n                  :permanent-failure :not-found :gone :proxy-request-refused :bad-request)\n                 (error-help \"Error\" meta))\n                (:slow-down\n                 (error-help\n                  \"Slow down error\"\n                  (format nil \"Try reloading the page in ~a seconds.\" meta)))\n                ((:client-certificate-required :certificate-not-authorised :certificate-not-valid)\n                 (error-help \"Certificate error\" meta))))\n        (gemini::malformed-response (e)\n          (error-help\n           \"Malformed response\"\n           (format nil \"The response for the URL you're requesting (~s) is malformed:~2%~a\" url e)))\n        (condition (condition)\n          (error-help \"Unknown error\"\n                      (format nil \"Original text of ~a:~%~a\" (type-of condition) condition))))))\n"
  },
  {
    "path": "source/mode/spell-check.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/mode/spell-check\n  (:documentation \"Package for `spell-check-mode' to spell-check text in buffers.\n\nIt leverages `enchant' under the hood.\"))\n(in-package :nyxt/mode/spell-check)\n\n(define-mode spell-check-mode ()\n  \"Enable spell checking of various text sources.\"\n  ((visible-in-status-p nil)\n   (spell-check-language\n    \"en_US\"\n    :documentation \"Spell check language used by Nyxt.\nFor a list of more languages available, see `spell-check-list-languages'.\")\n   (keyscheme-map\n    (define-keyscheme-map \"spell-check-mode\" ()\n      keyscheme:cua\n      (list\n       \"f7\" 'spell-check-word)\n      keyscheme:emacs\n      (list\n       \"M-$\" 'spell-check-word)\n      keyscheme:vi-normal\n      (list\n       \"z =\" 'spell-check-word)))))\n\n(define-configuration document-buffer\n  ((default-modes (cons 'spell-check-mode %slot-value%))))\n\n(defmacro with-spell-check ((variable) &body body)\n  `(enchant:with-dict (,variable (spell-check-language\n                                  (find-submode 'spell-check-mode)))\n     ,@body))\n\n(defun spell-dict-check-p (word)\n  \"Spell check `word' and return if correct or not.\"\n  (with-spell-check (lang)\n    (enchant:dict-check lang word)))\n\n(define-command spell-check-word (&key (word nil word-supplied-p))\n  \"Spell check a word.\"\n  (let ((word (or (and word-supplied-p word)\n                  (prompt1\n                   :prompt \"Spell check word\"\n                   :sources 'prompter:raw-source))))\n    (if (spell-dict-check-p word)\n        (echo \"~s is spelled correctly.\" word)\n        (echo \"~s is NOT correctly spelled.\" word))))\n\n(define-command spell-check-highlighted-word ()\n  \"Spell check a highlighted word. If a word is incorrectly spelled,\npull up a prompt of suggestions.\"\n  (let ((word (ffi-buffer-copy (current-buffer))))\n    (if (str:blankp word)\n        (echo \"No word highlighted to spell check!\")\n        (spell-check-prompt word))))\n\n(defun spell-check-prompt (word)\n  \"Spell check `word', if incorrectly spelled, prompt the user with\nsuggestions.\"\n  (if (spell-dict-check-p word)\n      (echo \"Word ~s spelled correctly.\" word)\n      (progn\n        (echo \"Word ~s spelled incorrectly.\" word)\n        (spell-check-suggest-word :word word))))\n\n(define-command spell-check-word-at-cursor ()\n  \"Spell check the word at the cursor.\"\n  (nyxt/mode/input-edit:with-input-area (contents cursor-pos)\n    (nyxt/mode/input-edit:with-text-buffer (text-buffer cursor contents cursor-pos)\n      (spell-check-prompt (text-buffer::word-at-cursor cursor)))))\n\n(define-command spell-check-suggest-word (&key word)\n  \"Suggest a spelling for a given word.\"\n  (let ((selected-word (prompt1\n                        :prompt \"Suggest spelling (3+ characters)\"\n                        :input word\n                        :sources 'enchant-source)))\n    (trivial-clipboard:text selected-word)\n    (echo \"Word saved to clipboard.\")))\n\n(define-class enchant-source (prompter:source)\n  ((case-sensitive-p nil)\n   (prompter:name \"Enchant\")\n   (prompter:filter nil)\n   (prompter:filter-preprocessor\n    (lambda (preprocessed-suggestions source input)\n      (declare (ignore preprocessed-suggestions source))\n      (when (> (length input) 2)\n        (with-spell-check (lang)\n          (enchant:dict-suggest lang input)))))))\n\n(define-command spell-check-list-languages ()\n  \"List all languages supported on your machine.\"\n  (echo \"Supported languages: ~s\"\n        (mapcar #'first (enchant:with-broker bkr\n                          (enchant:broker-list-dicts bkr)))))\n\n(defun spell-check-and-suggest (word)\n  \"Only suggest if `word' is incorrect.\"\n  (with-spell-check (lang)\n    (let ((result (enchant:dict-check lang word)))\n      (or result\n          (enchant:dict-suggest lang word)))))\n"
  },
  {
    "path": "source/mode/style.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/mode/style\n  (:documentation \"Package for `style-mode', a customizable document styling\nfacility.\nIt also hosts the submode `dark-mode'.\"))\n(in-package :nyxt/mode/style)\n\n(define-mode style-mode ()\n  \"A mode for styling documents.\nStyle can be set by one of the `style', `style-file' or `style-url' slots.\"\n  ((visible-in-status-p nil)\n   (style-file\n    nil\n    :type (or null string pathname)\n    :documentation \"Local CSS file.\nIf supplied, sets `style' to the content of the file.\nOtherwise, looks for CSS in `style-url'.\")\n   (style\n    nil\n    :type (or null string)\n    :documentation \"Style as a CSS string.\nIf nil, look for CSS in `style-file' or `style-url'.\")))\n\n(defmethod enable ((mode style-mode) &key)\n  (unless (style mode)\n    (setf (style mode)\n          (ignore-errors\n           (uiop:read-file-string\n            (style-file mode)))))\n  (apply-style mode))\n\n(defmethod apply-style ((mode style-mode))\n  (when (style mode)\n    (nyxt::html-set-style (style mode) (buffer mode))))\n\n(defmethod nyxt:on-signal-load-finished ((mode style-mode) url title)\n  (declare (ignore url title))\n  (apply-style mode))\n\n(define-mode dark-mode (style-mode)\n  \"A `style-mode' for styling documents with a dark background.\nUnlike other modes, to effectively disable `dark-mode' you must also reload the\nbuffer.\"\n  ((visible-in-status-p nil)))\n\n(defmethod apply-style ((mode dark-mode))\n  (if (style mode)\n      (nyxt::html-set-style (style mode) (buffer mode))\n      (nyxt/mode/bookmarklets:darken (buffer mode))))\n"
  },
  {
    "path": "source/mode/user-script.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/mode/user-script\n  (:documentation \"Package for `user-script-mode' to load so-called 'user scripts', such as GreaseMonkey scripts.\"))\n(in-package :nyxt/mode/user-script)\n\n(defun inject-user-scripts (scripts buffer)\n  (mapcar (lambda (script) (ffi-buffer-add-user-script buffer script)) scripts))\n\n(defun de-inject-user-scripts (scripts buffer)\n  (mapcar (lambda (script) (ffi-buffer-remove-user-script buffer script)) scripts))\n\n(defun inject-user-styles (styles buffer)\n  (mapcar (lambda (style) (ffi-buffer-add-user-style buffer style)) styles))\n\n(defun de-inject-user-styles (styles buffer)\n  (mapcar (lambda (style) (ffi-buffer-remove-user-style buffer style)) styles))\n\n(eval-always\n  (define-class user-script (renderer-user-script files:data-file nyxt-remote-file)\n    ((code \"\" :type (maybe string))\n     (version \"\")\n     (description \"\")\n     (namespace \"\")\n     (world-name\n      nil\n      :type (maybe string)\n      :documentation \"The JavaScript world to run the `code' in.\")\n     (requires\n      nil\n      :type (maybe hash-table))\n     (include\n      '(\"http://*/*\" \"https://*/*\")\n      :type (list-of string))\n     (exclude\n      '()\n      :type (list-of string))\n     (all-frames-p\n      t\n      :type boolean\n      :documentation \"Whether to run on both top-level frame and all the subframes.\nIf false, runs on the toplevel frame only.\")\n     (run-at\n      :document-end\n      :type (member :document-start :document-end :document-idle)\n      :documentation \"When to run the script.\nPossible values:\n- `:document-start' (page started loading).\n- `:document-end' (page loaded, resources aren't).\n- `:document-idle' (page and resources are loaded).\"))\n    (:export-class-name-p t)\n    (:export-accessor-names-p t)\n    (:documentation \"The Nyxt-internal representation of user scripts to bridge with the renderer.\")\n    (:metaclass user-class)))\n\n(-> get-script-url\n    (string (maybe nyxt::url-designator pathname))\n    (values (maybe quri:uri) boolean))\n(defun get-script-url (script original-url)\n  \"A helper to get the URL to a SCRIPT string.\nReturn:\n- a final URL;\n- T when it's a file URL, NIL otherwise.\"\n  (cond\n    ((valid-url-p script)\n     (let ((script (quri:uri script)))\n       (if (quri:uri-file-p script)\n           (values script t)\n           (values script nil))))\n    ((and (uiop:file-pathname-p script)\n          (uiop:file-exists-p script)\n          (uiop:absolute-pathname-p script))\n     (values (quri.uri.file:make-uri-file :path script) t))\n    ((and (uiop:file-pathname-p script) original-url)\n     (let ((full-url (quri:merge-uris (quri:uri script)\n                                      (typecase original-url\n                                        (pathname (quri.uri.file:make-uri-file :path original-url))\n                                        (nyxt::url-designator (url original-url))))))\n       (if (and (quri:uri-file-p full-url)\n                (uiop:file-exists-p (quri:uri-path full-url)))\n           (values full-url t)\n           (values full-url nil))))\n    (t (values nil nil))))\n\n(-> get-script-text\n    ((or string nyxt::url-designator pathname)\n     &optional (maybe nyxt::url-designator pathname))\n    (values (maybe string) &optional))\n(defun get-script-text (script &optional original-url)\n  (etypecase script\n    (pathname\n     (files:content (make-instance 'user-script :base-path script)))\n    (quri:uri\n     (files:content\n      (if (quri:uri-file-p script)\n          (make-instance 'user-script :base-path (quri:uri-path script))\n          (make-instance 'user-script :url script :base-path #p\"\"))))\n    (string\n     (multiple-value-bind (url file-p)\n         (get-script-url script original-url)\n       (cond\n         ((and url file-p)\n          (files:content (make-instance 'user-script :base-path (quri:uri-path url))))\n         ((and url (not file-p))\n          (files:content (make-instance 'user-script :url (quri:uri script) :base-path #p\"\")))\n         ;; No URL. No need to download anything.\n         ;; It's just code (hopefully).\n         (t script))))))\n\n(defmethod files:write-file ((profile nyxt-profile) (script user-script) &key destination)\n  \"Persist the script body if it has a URL and associated content.\"\n  (unless (uiop:emptyp (files:url-content script))\n    (alex:write-string-into-file (files:url-content script) destination :if-exists :supersede)))\n\n(defmethod parse-user-script ((script user-script))\n  (let ((code (if (uiop:emptyp (code script))\n                  (files:content script)\n                  (code script))))\n    (or\n     (and-let* ((start-position (search \"// ==UserScript==\" code))\n                (end-position (search \"// ==/UserScript==\" code))\n                (meta (subseq code\n                              (+ (1+ (length \"// ==UserScript==\")) start-position)\n                              end-position)))\n       (flet ((getprop (prop)\n                (when-let* ((regex (str:concat \"// @\" prop \"\\\\s*(.*)\"))\n                            (raw-props (ppcre:all-matches-as-strings regex meta)))\n                  (mapcar (lambda (raw-prop)\n                            (multiple-value-bind (begin end reg-starts reg-ends)\n                                (ppcre:scan regex raw-prop)\n                              (declare (ignore end))\n                              (when begin\n                                (subseq raw-prop (elt reg-starts 0) (elt reg-ends 0)))))\n                          raw-props))))\n\n         (let ((code-with-requires (format nil \"~{~a;~&~}~a\"\n                                           (mapcar (lambda (require)\n                                                     (get-script-text\n                                                      require\n                                                      (get-script-url require nil)))\n                                                   (getprop \"require\"))\n                                           code)))\n           (setf\n            (files:name script) (or (first (getprop \"name\")) (alex:required-argument 'name))\n            (version script) (first (getprop \"version\"))\n            (description script) (first (getprop \"description\"))\n            (namespace script) (first (getprop \"namespace\"))\n            (all-frames-p script) (not (first (getprop \"noframes\")))\n            (code script) code-with-requires\n            (include script) (let ((includes (append (getprop \"include\") (getprop \"match\"))))\n                               (cond\n                                 ((and (sera:single includes)\n                                       (equal \"http*\" (first includes)))\n                                  '(\"http://*/*\" \"https://*/*\"))\n                                 ((and (sera:single includes)\n                                       (equal \"https*\" (first includes)))\n                                  '(\"https://*/*\"))\n                                 ((and (sera:single includes)\n                                       (equal \"*\" (first includes)))\n                                  '(\"*://*/*\"))\n                                 (t includes)))\n            (exclude script) (getprop \"exclude\")\n            (run-at script) (str:string-case (first (getprop \"run-at\"))\n                              (\"document-start\" :document-start)\n                              (\"document-end\" :document-end)\n                              (\"document-idle\" :document-idle)\n                              (otherwise :document-end)))\n           code-with-requires)))\n     (setf (code script) code))))\n\n(defmethod customize-instance :after ((script user-script) &key)\n  (parse-user-script script))\n\n(export-always 'renderer-user-style)\n(defclass renderer-user-style ()\n  ()\n  (:metaclass interface-class)\n  (:documentation \"The basis for renderer-specific user style extensions.\nShould be redefined by the renderer.\"))\n\n(eval-always\n  (define-class user-style (renderer-user-style files:data-file nyxt-remote-file)\n    ((code \"\" :type (maybe string))\n     (world-name\n      nil\n      :type (maybe string)\n      :documentation \"The JavaScript world to inject the style in.\")\n     (include\n      '(\"http://*/*\" \"https://*/*\")\n      :type (list-of string))\n     (exclude\n      '()\n      :type (list-of string))\n     (all-frames-p\n      t\n      :type boolean\n      :documentation \"Whether to run on both top-level frame and all the subframes.\nIf false, runs on the toplevel frame only.\")\n     (level\n      :user\n      :type (member :user :author)\n      :documentation \"The level of authority (:USER > :AUTHOR) with which to inject the style.\n:USER styles override everything else.\"))\n    (:export-class-name-p t)\n    (:export-accessor-names-p t)\n    (:documentation \"The Nyxt-internal representation of user styles to bridge with the renderer.\")\n    (:metaclass user-class)))\n\n(defmethod files:write-file ((profile nyxt-profile) (style user-style) &key destination)\n  \"Persist the script body if it has a URL and associated content.\"\n  (unless (uiop:emptyp (files:url-content style))\n    (alex:write-string-into-file (files:url-content style) destination :if-exists :supersede)))\n\n(defmethod customize-instance :after ((style user-style) &key)\n  (when (uiop:emptyp (code style))\n    (setf (code style) (files:content style))))\n\n(define-mode user-script-mode ()\n  \"Mode to manage user scripts such as GreaseMonkey scripts.\nThe mode can manage multiple scripts.  Each `user-script' behaves following to\nits own independent settings.\"\n  ((user-scripts\n    nil\n    :reader user-scripts\n    :type (list-of user-script)\n    :documentation \"List of `user-script'-s to attach via renderer-specific mechanisms.\")\n   (user-styles\n    nil\n    :reader user-styles\n    :type (list-of user-style)\n    :documentation \"List of `user-style'-s to attach via renderer-specific mechanisms.\")))\n\n(defmethod enable ((mode user-script-mode) &key)\n  (inject-user-scripts (user-scripts mode) (buffer mode))\n  (inject-user-styles (user-styles mode) (buffer mode)))\n\n(defmethod disable ((mode user-script-mode) &key)\n  (de-inject-user-scripts (user-scripts mode) (buffer mode))\n  (de-inject-user-styles (user-styles mode) (buffer mode)))\n\n(export-always 'user-scripts)\n(defmethod (setf user-scripts) (new-value (mode user-script-mode))\n  (inject-user-scripts (slot-value mode 'user-scripts) (buffer mode))\n  (inject-user-scripts new-value (buffer mode))\n  (setf (slot-value mode 'user-scripts) new-value))\n\n(export-always 'user-styles)\n(defmethod (setf user-styles) (new-value (mode user-script-mode))\n  (de-inject-user-styles (slot-value mode 'user-styles) (buffer mode))\n  (inject-user-styles new-value (buffer mode))\n  (setf (slot-value mode 'user-styles) new-value))\n\n(export-always 'renderer-user-script)\n(defclass renderer-user-script ()\n  ()\n  (:metaclass interface-class)\n  (:documentation \"The basis for renderer-specific user scripts.\nShould be redefined by the renderer.\"))\n"
  },
  {
    "path": "source/mode/vi.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/mode/vi\n  (:documentation \"Package for `vi-normal-mode' and `vi-insert-mode',\nwhich provide VI-style bindings.\"))\n(in-package :nyxt/mode/vi)\n\n(define-mode vi-normal-mode (nyxt/mode/keyscheme:keyscheme-mode)\n  \"Enable VI-style modal bindings (normal mode).\nTo enable these bindings by default, add the mode to the list of default modes\nin your configuration file.\n\nExample:\n\n\\(define-configuration buffer\n  ((default-modes (append '(vi-normal-mode) %slot-value%))))\n\nIn `vi-insert-mode', CUA bindings are still available unless\n`passthrough-mode-p' is non-nil in `vi-insert-mode'.\nYou can also enable `passthrough-mode' manually to forward all keybindings to\nthe web page.\n\nSee also `vi-insert-mode'.\"\n  ((glyph \"vi:N\")\n   (keyscheme keyscheme:vi-normal)\n   (keyscheme-map\n    (define-keyscheme-map \"vi-normal-mode\" ()\n      keyscheme:vi-normal\n      (list\n       \"i\" 'vi-insert-mode\n       \"q\" 'unfocus-current-element\n       \"v\" 'nyxt/mode/visual:visual-mode)))))\n\n(define-command unfocus-current-element ()\n  \"Unfocus the currently selected HTML element.\"\n  (ps-eval\n    (let ((el (ps:chain document active-element)))\n      (when el\n        (ps:chain el (blur))))))\n\n(define-mode vi-insert-mode (nyxt/mode/keyscheme:keyscheme-mode)\n  \"Enable VI-style modal bindings (insert mode).\nSee `vi-normal-mode'.\"\n  ((glyph \"vi:I\")\n   (keyscheme keyscheme:vi-insert)\n   (previous-vi-normal-mode nil\n    :type (or vi-normal-mode null)\n    :documentation \"The `vi-normal-mode' that this insert mode is tied to.\")\n   (keyscheme-map\n    (define-keyscheme-map \"vi-insert-mode\" ()\n      keyscheme:vi-insert\n      (list\n       \"C-z\" 'nyxt/mode/passthrough:passthrough-mode\n       \"escape\" 'switch-to-vi-normal-mode)))\n   (passthrough-mode-p nil\n                       :type boolean\n                       :documentation \"Whether to default to `passthrough-mode'\n                       when entering `vi-insert-mode'.\")))\n\n(defmethod enable ((mode vi-normal-mode) &key)\n  (with-accessors ((buffer buffer)) mode\n    (let ((vi-insert (find-submode 'vi-insert-mode buffer)))\n      (setf (nyxt/mode/keyscheme:previous-keyscheme mode)\n            (if vi-insert\n                (nyxt/mode/keyscheme:previous-keyscheme vi-insert)\n                (keyscheme buffer)))\n      (when vi-insert\n        ;; Destroy vi-normal mode after setting previous-keyscheme, or else we\n        ;; can't save the previous keyscheme.\n        (disable vi-insert)))\n    (setf (forward-input-events-p buffer) nil)))\n\n(defmethod disable ((mode vi-normal-mode) &key)\n  (setf (forward-input-events-p (buffer mode)) t))\n\n(define-command switch-to-vi-normal-mode\n    (&optional (mode (find-submode 'vi-insert-mode\n                                   (or (current-prompt-buffer)\n                                       (current-buffer)))))\n  \"Switch to the mode remembered to be the matching VI-normal one for this MODE.\nSee also `vi-normal-mode' and `vi-insert-mode'.\"\n  (when mode\n    (enable-modes* (list (or (and (previous-vi-normal-mode mode)\n                                  (sera:class-name-of\n                                   (previous-vi-normal-mode mode)))\n                             'vi-normal-mode))\n                   (buffer mode))))\n\n(defmethod enable ((mode vi-insert-mode) &key)\n  (with-accessors ((buffer buffer)) mode\n    (let ((vi-normal (find-submode 'vi-normal-mode buffer)))\n      (setf (nyxt/mode/keyscheme:previous-keyscheme mode)\n            (if vi-normal\n                (nyxt/mode/keyscheme:previous-keyscheme vi-normal)\n                (keyscheme buffer))\n            (previous-vi-normal-mode mode)\n            vi-normal)\n      (when vi-normal\n        (disable vi-normal)))\n    ;; Somehow use inheritance instead?\n    (when (passthrough-mode-p mode)\n      (enable-modes* 'nyxt/mode/passthrough:passthrough-mode buffer))))\n\n(defun vi-insert-on-input-fields (buffer)\n  (cond\n    ((j:and (find-submode 'vi-normal-mode buffer)\n            (ps-eval :buffer buffer\n              (and (nyxt/ps:active-element document)\n                   (nyxt/ps:element-editable-p\n                    (nyxt/ps:active-element document)))))\n     (enable-modes* 'vi-insert-mode buffer))\n    ((j:and (find-submode 'vi-insert-mode buffer)\n            (j:not (ps-eval :buffer buffer\n                     (and (nyxt/ps:active-element document)\n                          (nyxt/ps:element-editable-p\n                           (nyxt/ps:active-element document))))))\n     (enable-modes* 'vi-normal-mode buffer))))\n\n(defmethod on-signal-load-finished ((mode vi-insert-mode) url title)\n  (declare (ignore url title))\n  (enable-modes* 'vi-normal-mode (buffer mode))\n  (vi-insert-on-input-fields (buffer mode)))\n\n(defmethod on-signal-button-press ((mode vi-normal-mode) button-key)\n  (when (string= \"button1\" (keymaps:key-value button-key))\n    (vi-insert-on-input-fields (buffer mode))))\n\n(defmethod on-signal-button-press ((mode vi-insert-mode) button-key)\n  (when (string= \"button1\" (keymaps:key-value button-key))\n    (vi-insert-on-input-fields (buffer mode))))\n\n(defmethod on-signal-key-press ((mode vi-normal-mode) (key keymaps:key))\n  (when (equal \"tab\" (keymaps:key-value key))\n    (vi-insert-on-input-fields (buffer mode))))\n\n(defmethod nyxt/dom:focus-select-element :after ((element plump:element))\n  (vi-insert-on-input-fields (current-buffer)))\n"
  },
  {
    "path": "source/mode/visual.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/mode/visual\n  (:documentation \"Package for `visual-mode', which enables keyboard-driven HTML\nselection.\"))\n(in-package :nyxt/mode/visual)\n\n(define-mode visual-mode (nyxt/mode/hint:hint-mode)\n  \"Browse the page with a cursor to select text.\"\n  ((nyxt/mode/hint:hints-selector\n    \"a, b, p, del, h1, h2, h3, h4, h5, h6, i, option, strong, sub, sup, listing,\nxmp, plaintext, basefont, big, blink, center, font, marquee, multicol, nobr, s,\nspacer, strike, tt, u, wbr, code, cite, pre\"\n    :type string)\n   (keyscheme-map\n    (define-keyscheme-map \"visual-mode\" ()\n      keyscheme:cua\n      (list\n       \"up\" 'backward-line\n       \"down\" 'forward-line\n       \"left\" 'backward-char\n       \"right\" 'forward-char\n       \"backspace\" 'backward-char\n       \"space\" 'forward-char\n       \"shift-left\" 'backward-char-with-selection\n       \"shift-right\" 'forward-char-with-selection\n       \"C-shift-left\" 'backward-word-with-selection\n       \"C-shift-right\" 'forward-word-with-selection\n       \"keypadhome\" 'beginning-line\n       \"keypadend\" 'end-line\n       \"shift-up\" 'backward-line-with-selection\n       \"shift-down\" 'forward-line-with-selection\n       \"C-shift-up\" 'beginning-line-with-selection\n       \"C-shift-down\" 'end-line-with-selection\n       \"escape\" 'visual-mode\n       \"delete\" 'clear-selection\n       \"C-c\" 'visual-mode)\n      keyscheme:emacs\n      (list\n       \"C-p\" 'backward-line\n       \"C-n\" 'forward-line\n       \"C-b\" 'backward-char\n       \"C-f\" 'forward-char\n       \"M-b\" 'backward-word\n       \"M-f\" 'forward-word\n       \"M-a\" 'backward-sentence\n       \"M-e\" 'forward-sentence\n       \"M-{\" 'backward-paragraph\n       \"M-}\" 'forward-paragraph\n       \"C-h\" 'select-paragraph\n       \"M-<\" 'backward-document\n       \"M->\" 'forward-document\n       \"C-a\" 'beginning-line\n       \"C-e\" 'end-line\n       \"C-g\" 'visual-mode\n       \"shift-space\" 'toggle-mark)\n      ;; vi keybindings only enable use of vim's plain \"visual\" mode for now\n      keyscheme:vi-normal\n      (list\n       \"c\" 'clear-selection\n       \"h\" 'backward-char\n       \"l\" 'forward-char\n       \"p\" 'query-selection-in-search-engine\n       \"k\" 'backward-line\n       \"j\" 'forward-line\n       \"b\" 'backward-word\n       \"w\" 'forward-word\n       \"(\" 'backward-sentence\n       \")\" 'forward-sentence\n       \"{\" 'backward-paragraph\n       \"}\" 'forward-paragraph\n       \"escape\" 'visual-mode\n       \"g g\" 'backward-document\n       \"G\" 'forward-document\n       \"0\" 'beginning-line\n       \"$\" 'end-line\n       \"v\" 'toggle-mark\n       \"y\" 'nyxt/mode/document:copy\n       \"C-c\" 'visual-mode)))\n   (mark-set nil)))\n\n(defmethod enable ((mode visual-mode) &key)\n  (make-page-editable)\n  (block-page-keypresses)\n  (select-paragraph mode)\n  ;; imitating visual mode in vim\n  (when (equal (keyscheme (buffer mode)) keyscheme:vi-normal)\n    (setf (mark-set mode) t)))\n\n(defmethod disable ((mode visual-mode) &key)\n  (make-page-uneditable)\n  (unlock-page-keypresses)\n  (setf (mark-set mode) nil))\n\n(defmethod prompter:object-attributes ((element nyxt/dom:text-element)\n                                       (source prompter:source))\n  `((\"Hint\" ,(plump:attribute element \"nyxt-hint\"))\n    (\"Text\" ,(plump:text element))))\n\n(defmethod %follow-hint ((element nyxt/dom:text-element))\n  (nyxt/dom:set-caret-on-start element))\n\n(defmethod caret-action ((mode visual-mode))\n  (if (mark-set mode)\n      :extend\n      :move))\n\n(define-parenscript block-page-keypresses ()\n  (setf (ps:@ window block-keypresses)\n        (lambda (event)\n          (ps:chain event (prevent-default))))\n  (ps:chain window\n            (add-event-listener \"keydown\"\n                                (ps:@ window block-keypresses)\n                                false)))\n\n(define-parenscript unlock-page-keypresses ()\n  (ps:chain window\n            (remove-event-listener \"keydown\"\n                                   (ps:@ window block-keypresses)\n                                   false)))\n\n(define-parenscript make-page-editable ()\n  (setf (ps:@ document body content-editable) \"true\"))\n\n(define-parenscript make-page-uneditable ()\n  (setf (ps:@ document body content-editable) \"false\"))\n\n(define-command select-paragraph (&optional (mode (find-submode 'visual-mode)))\n  \"Add hints to text elements on the page and query them.\"\n  (nyxt/mode/hint:query-hints \"Set caret on element\"\n                              (lambda (results) (%follow-hint (first results)))\n                              :selector (nyxt/mode/hint:hints-selector mode)))\n\n(define-parenscript collapsed-p ()\n  \"Return T if mark's start and end are the same value, nil otherwise.\"\n  (defun collapsed-p ()\n    (let ((sel (ps:chain window (get-selection))))\n      (ps:@ sel is-collapsed)))\n  (collapsed-p))\n\n(define-parenscript collapse-to-focus ()\n  \"Collapse the selection\"\n  (let ((sel (ps:chain window (get-selection))))\n    (ps:chain sel\n              (collapse (ps:@ sel focus-node)\n                        (ps:@ sel focus-offset)))))\n\n(define-command toggle-mark ()\n  \"Toggle the mark.\"\n  (let ((mode (find-submode 'visual-mode)))\n    (if (collapsed-p)\n        (progn\n          (setf (mark-set mode) (not (mark-set mode)))\n          (if (mark-set mode)\n              (echo \"Mark set\")\n              (echo \"Mark deactivated\")))\n        (progn\n          (collapse-to-focus)\n          (echo \"Mark set\")))))\n\n(define-command clear-selection ()\n  \"Clear the selection and unset the mark.\"\n  (let ((mode (find-submode 'visual-mode)))\n    (unless (collapsed-p) (collapse-to-focus))\n    (setf (mark-set mode) nil)\n    (echo \"Mark deactivated\")))\n\n(define-parenscript caret-move (&key action direction scale (n 1))\n  (let ((sel (ps:chain window (get-selection)))\n        (parent-el (ps:chain window (get-selection) focus-node parent-element)))\n    (ps:chain parent-el (scroll-into-view (ps:create block \"nearest\")))\n    (dotimes (i (ps:lisp n))\n      (ps:chain sel (modify (ps:lisp action)\n                            (ps:lisp direction)\n                            (ps:lisp scale))))))\n\n(define-command forward-char ()\n  \"Move caret forward by a character.\"\n  (let ((mode (find-submode 'visual-mode)))\n    (caret-move :action (caret-action mode)\n                :direction :forward\n                :scale :character)))\n\n(define-command backward-char ()\n  \"Move caret backward by a character.\"\n  (let ((mode (find-submode 'visual-mode)))\n    (caret-move :action (caret-action mode)\n                :direction :backward\n                :scale :character)))\n\n(define-command forward-word ()\n  \"Move caret forward by a word.\"\n  (let ((mode (find-submode 'visual-mode)))\n    (caret-move :action (caret-action mode)\n                :direction :forward\n                :scale :word)))\n\n(define-command backward-word ()\n  \"Move caret backward by a word.\"\n  (let ((mode (find-submode 'visual-mode)))\n    (caret-move :action (caret-action mode)\n                :direction :backward\n                :scale :word)))\n\n(define-command forward-line ()\n  \"Move caret forward by a line.\"\n  (let ((mode (find-submode 'visual-mode)))\n    (caret-move :action (caret-action mode)\n                :direction :forward\n                :scale :line)))\n\n(define-command backward-line ()\n  \"Move caret backward by a line.\"\n  (let ((mode (find-submode 'visual-mode)))\n    (caret-move :action (caret-action mode)\n                :direction :backward\n                :scale :line)))\n\n(define-command beginning-line ()\n  \"Move caret to the beginning of the line.\"\n  (let ((mode (find-submode 'visual-mode)))\n    (caret-move :action (caret-action mode)\n                :direction :backward\n                :scale :lineboundary)))\n\n(define-command end-line ()\n  \"Move caret to the end of the line.\"\n  (let ((mode (find-submode 'visual-mode)))\n    (caret-move :action (caret-action mode)\n                :direction :forward\n                :scale :lineboundary)))\n\n(define-command forward-sentence ()\n  \"Move caret forward to next end of sentence.\"\n  (let ((mode (find-submode 'visual-mode)))\n    (caret-move :action (caret-action mode)\n                :direction :forward\n                :scale :sentence)))\n\n(define-command backward-sentence ()\n  \"Move caret backward to start of sentence.\"\n  (let ((mode (find-submode 'visual-mode)))\n    (caret-move :action (caret-action mode)\n                :direction :backward\n                :scale :sentence)))\n\n(define-command forward-paragraph ()\n  \"Move caret forward by a paragraph.\"\n  (let ((mode (find-submode 'visual-mode)))\n    (caret-move :action (caret-action mode)\n                :direction :forward\n                :scale :paragraph)))\n\n(define-command backward-paragraph ()\n  \"Move caret backward by a paragraph.\"\n  (let ((mode (find-submode 'visual-mode)))\n    (caret-move :action (caret-action mode)\n                :direction :backward\n                :scale :paragraph)))\n\n(define-command forward-document ()\n  \"Move caret forward to the end of the document.\"\n  (let ((mode (find-submode 'visual-mode)))\n    (caret-move :action (caret-action mode)\n                :direction :forward\n                :scale :documentboundary)))\n\n(define-command backward-document ()\n  \"Move caret backward to the beginning of the document.\"\n  (let ((mode (find-submode 'visual-mode)))\n    (caret-move :action (caret-action mode)\n                :direction :backward\n                :scale :documentboundary)))\n\n(defmacro define-command-with-selection (name args &body body)\n  (declare (ignore args))\n  (alex:with-gensyms (mode)\n    (multiple-value-bind (body decls doc)\n      (alex:parse-body body :documentation t)\n        `(define-command ,name ()\n           ,@decls ,@(sera:unsplice doc)\n           (let ((,mode (find-submode 'visual-mode)))\n             (setf (mark-set ,mode) t)\n             ,@body)))))\n\n(define-command-with-selection forward-line-with-selection ()\n  \"Set mark and move caret forward by a line.\"\n  (forward-line))\n\n(define-command-with-selection backward-line-with-selection ()\n  \"Set mark and move caret backward by a line.\"\n  (backward-line))\n\n(define-command-with-selection forward-char-with-selection ()\n  \"Set mark and move caret forward by a character.\"\n  (forward-char))\n\n(define-command-with-selection backward-char-with-selection ()\n  \"Set mark and move caret backward by a character.\"\n  (backward-char))\n\n(define-command-with-selection forward-word-with-selection ()\n  \"Set mark and move caret forward by a word.\"\n  (forward-word))\n\n(define-command-with-selection backward-word-with-selection ()\n  \"Set mark and move caret backward by a word.\"\n  (backward-word))\n\n(define-command-with-selection beginning-line-with-selection ()\n  \"Set mark and move caret to the beginning of the line.\"\n  (beginning-line))\n\n(define-command-with-selection end-line-with-selection ()\n  \"Set mark and move caret to the end of line.\"\n  (end-line))\n"
  },
  {
    "path": "source/mode/watch.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/mode/watch\n  (:documentation \"Package for `watch-mode', which reloads buffers at regular\ntime intervals.\"))\n(in-package :nyxt/mode/watch)\n\n(defun seconds-from-user-input ()\n  \"Query the numerical time inputs and collate them into seconds.\"\n  (let* ((time-units '(\"days\" \"hours\" \"minutes\" \"seconds\"))\n         (to-seconds-alist (pairlis time-units '(86400 3600 60 1)))\n         (active-time-units (prompt\n                             :prompt \"Time unit(s)\"\n                             :sources (make-instance 'prompter:source\n                                                     :name \"Units\"\n                                                     :constructor time-units\n                                                     :enable-marks-p t)))\n         (times (mapcar (lambda (unit)\n                          (parse-integer\n                           (prompt1\n                             :prompt (format nil \"Time interval (~a)\" unit)\n                             :sources 'prompter:raw-source)\n                           :junk-allowed t))\n                        active-time-units))\n         (to-seconds-multipliers\n           (mapcar\n            (lambda (elem) (rest (assoc elem to-seconds-alist\n                                        :test 'string-equal)))\n            active-time-units)))\n    (echo \"Refreshing every ~:@{~{~d ~}~a~:@}\"\n          (list times active-time-units))\n    (reduce #'+ (mapcar (lambda (time multiplier) (* time multiplier))\n                        times to-seconds-multipliers))))\n\n(define-mode watch-mode (nyxt/mode/repeat:repeat-mode)\n  \"Reload the current buffer every 5 minutes.\"\n  ((nyxt/mode/repeat:repeat-interval 300.0)\n   (nyxt/mode/repeat:repeat-action\n    (lambda (mode)\n      (ffi-buffer-reload (buffer mode)))\n    :type (maybe (function (nyxt/mode/repeat:repeat-mode))))))\n\n(defmethod enable ((mode watch-mode) &key)\n  (let ((interval (seconds-from-user-input)))\n    (setf (nyxt/mode/repeat:repeat-interval mode) interval))\n  (setf (nyxt/mode/repeat:repeat-action mode)\n        (lambda (mode)\n          (ffi-buffer-reload (buffer mode)))))\n"
  },
  {
    "path": "source/mode.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt)\n\n(defclass mode-class (user-class)\n  ((toggler-command-p                   ; TODO: Rename to `togglable-p'?\n    :initform (list t)\n    :initarg :toggler-command-p\n    :type (cons boolean null)\n    :documentation \"Whether to define a toggler command for the defined mode.\"))\n  (:documentation \"Metaclass for all the `mode's.\nOnly used to mandate whether the mode needs a toggler command:\n`toggler-command-p'.\"))\n(export-always 'mode-class)\n\n(defmethod closer-mop:validate-superclass ((class mode-class)\n                                           (superclass user-class))\n  t)\n\n(defun define-or-undefine-command-for-mode (class)\n  (let ((name (class-name class)))\n    ;; FIXME: SBCL `slot-value' returns a list, while CCL returns the boolean.  Why?\n    (if (alex:ensure-car (slot-value class 'toggler-command-p))\n        (sera:lret ((command (make-command\n                              name\n                              `(lambda (&rest args\n                                        &key (buffer (or (current-prompt-buffer) (current-buffer)))\n                                          (activate t activate-supplied-p)\n                                        &allow-other-keys)\n                                 ,(let ((*print-case* :downcase))\n                                    (format nil \"Toggle `~a'.\" name))\n                                 (declare (ignorable buffer activate activate-supplied-p))\n                                 (apply #'toggle-mode ',name args))\n                              :global)))\n          (setf (fdefinition name) command))\n        (delete-command name))))\n\n(defmethod initialize-instance :after ((class mode-class) &key)\n  (define-or-undefine-command-for-mode class))\n\n(defmethod reinitialize-instance :after ((class mode-class) &key)\n  (define-or-undefine-command-for-mode class))\n\n(define-class mode ()\n  ((buffer\n    nil\n    :type (maybe null buffer))\n   (visible-in-status-p\n    t\n    :documentation \"Whether the mode is visible in the `status-buffer'.\")\n   (glyph\n    nil\n    :type (maybe string)\n    :accessor nil\n    :documentation \"A `status-buffer' indicator that mode is enabled, when\n`glyph-mode-presentation-p' is non-nil.\")\n   (enabled-p\n    nil\n    :accessor t\n    :documentation \"Whether the mode is enabled in `buffer'.\")\n   (enable-hook\n    (make-instance 'hook-mode)\n    :type hook-mode\n    :documentation \"Hook run when enabling the mode, after the constructor.\nThe handlers take the mode as argument.\")\n   (disable-hook\n    (make-instance 'hook-mode)\n    :type hook-mode\n    :documentation \"Hook run when disabling the mode, before the destructor.\nThe handlers take the mode as argument.\")\n   (keyscheme-map\n    (make-hash-table :size 0)\n    :type keymaps:keyscheme-map))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:export-predicate-name-p t)\n  (:toggler-command-p nil)\n  (:metaclass mode-class)\n  (:documentation \"Representation of Nyxt mode.\nBelongs to `buffer', has `keyscheme-map' and is/isn't `rememberable-p'.\n\nWhen `visible-in-status-p', shows mode name (or `glyph', when\n`glyph-mode-presentation-p') in status buffer.\n\nDefine new modes with `define-mode'.\n\nSpecify `enable' and `disable' methods to include mode-specific\ninitialization/destruction or hook into `enable-hook' or `disable-hook' to know\nwhen it gets toggled.\"))\n\n(defmethod initialize-instance :after ((mode mode) &key)\n  (when (eq 'mode (sera:class-name-of mode))\n    (error \"Cannot initialize `mode', you must subclass it.\")))\n\n(defmethod name ((mode mode))\n  (sera:class-name-of mode))\n\n(export-always 'enable)\n(defgeneric enable (mode &key &allow-other-keys)\n  (:method-combination cascade)\n  (:method ((mode mode) &key &allow-other-keys)\n    mode)\n  (:documentation \"Run when enabling a mode.\nThe pre-defined `:after' method handles further setup.\nThis method is meant to be specialized for every mode.\nIt is not meant to be called directly, see `enable-modes*' instead.\n\nAll the parent modes' `enable' methods run after the exact mode one, cascading\nupwards to allow a more useful mode inheritance without duplicating the\nfunctionality. A `cascade' method combination is used for that.\n\nSee also `disable'.\"))\n\n(defmethod enable :before ((mode mode) &rest keys &key &allow-other-keys)\n  ;; Using class-direct-slots here because `enable' will cascade to parent modes anyway.\n  ;; FIXME: An easier way to initialize slots given initargs?\n  (loop with slot-defs = (closer-mop:class-direct-slots (class-of mode))\n        for (key value) on keys by #'cddr\n        do (when-let ((slot-name (loop for slot-def in slot-defs\n                                       when (member key (c2mop:slot-definition-initargs slot-def))\n                                         do (return (c2cl:slot-definition-name slot-def)))))\n             ;; TODO: Maybe use writer methods, if present? It implies a risk of\n             ;; runtime actions on not-yet-fully-initialized mode instances\n             ;; (because enable is a kind of initialization too).\n             (setf (slot-value mode slot-name) value))))\n\n(defmethod enable :around ((mode mode) &key &allow-other-keys)\n  (let* ((buffer (buffer mode))\n         (existing-instance (find (sera:class-name-of mode)\n                                  (remove-if (sera:eqs mode) (modes buffer))\n                                  :key #'sera:class-name-of)))\n    (if existing-instance\n        (log:debug \"Not enabling ~s since other ~s instance is already in buffer ~a\" mode existing-instance buffer)\n        (call-next-method))\n    mode))\n\n(defmethod enable :after ((mode mode) &key &allow-other-keys)\n  (setf (enabled-p mode) t)\n  (hooks:run-hook (enable-hook mode) mode)\n  (let ((buffer (buffer mode)))\n    ;; TODO: Should we move mode to the front on re-enable?\n    (unless (find mode (modes buffer))\n      (setf (modes buffer)\n            (cons mode (modes buffer))))\n    (hooks:run-hook (enable-mode-hook buffer) mode)\n    (when (and (prompt-buffer-p buffer)\n               (eq (first (active-prompt-buffers (window buffer)))\n                   buffer))\n      (render-prompt buffer))))\n\n(export-always 'disable)\n(defgeneric disable (mode &key &allow-other-keys)\n  (:method-combination cascade)\n  (:method ((mode mode) &key)\n    nil)\n  (:documentation \"Run when disabling a mode.\nThe pre-defined `:after' method handles further cleanup.\nThis method is meant to be specialized for every mode.\nIt is not meant to be called directly, see `disable-modes' instead.\n\nAll the parent modes' `disable' methods run after the exact mode one, cascading\nupwards to allow a more useful mode inheritance without duplicating the\nfunctionality. A `cascade' method combination is used for that.\n\nSee also `enable'.\"))\n\n(defmethod disable :around ((mode mode) &key &allow-other-keys)\n  (if (enabled-p mode)\n      (call-next-method)\n      (echo-warning \"~a is not enabled, cannot disable it.\" mode)))\n\n(defmethod disable :after ((mode mode) &key &allow-other-keys)\n  (setf (enabled-p mode) nil)\n  (hooks:run-hook (disable-hook mode) mode)\n  (let ((buffer (buffer mode)))\n    (hooks:run-hook (disable-mode-hook (buffer mode)) mode)\n    ;; TODO: Remove from list or not?\n    ;; (setf (modes buffer) (delete ,existing-instance (modes buffer)))\n    (when (and (prompt-buffer-p buffer)\n               (eq (first (active-prompt-buffers (window buffer)))\n                   buffer))\n      (render-prompt buffer))))\n\n(export-always 'define-mode)\n(defmacro define-mode (name direct-superclasses &body body)\n  \"Shorthand to define a mode.  It has the same syntax as `define-class' but\noptionally accepts a docstring after the superclass declaration.\nThe `mode' superclass is automatically added if not present.\"\n  (let* ((docstring (when (stringp (first body))\n                      (first body)))\n         (body (if docstring\n                   (rest body)\n                   body))\n         (direct-slots (first body))\n         (options (rest body)))\n    `(eval-always ; Important so that classes can be found from the same file at compile-time.\n       (define-class ,name (,@(append direct-superclasses\n                                      (unless (find 'mode direct-superclasses) '(mode))))\n         ,direct-slots\n         ,@(append options\n                   (when docstring\n                     `((:documentation ,docstring)))\n                   `((:export-class-name-p t)\n                     (:export-accessor-names-p t)\n                     (:export-predicate-name-p t)\n                     (:metaclass mode-class)))))))\n\n(hooks:define-hook-type mode (function (mode))\n  \"Hook acting on `mode's.\")\n\n(export-always 'glyph)\n(defmethod glyph ((mode mode))\n  \"Return the glyph for a mode.\nWhen unset, it corresponds to the mode name.\"\n  (or (slot-value mode 'glyph)\n      (princ-to-string mode)))\n\n(defmethod (setf glyph) (glyph (mode mode))\n  (setf (slot-value mode 'glyph) glyph))\n\n(defmethod print-object ((mode mode) stream)\n  (format stream \"~@(~a~)\"\n          (sera:drop-suffix \"-MODE\"\n                            (symbol-name (sera:class-name-of mode)))))\n\n(sym:define-symbol-type mode (class)\n  (when-let ((class (find-class sym:%symbol% nil)))\n    (mopu:subclassp class (find-class 'mode))))\n\n(defun mode-class (symbol)\n  (when (sym:mode-symbol-p symbol)\n    (find-class symbol)))\n\n(defun resolve-user-symbol (designator type &optional (packages (append (nyxt-packages)\n                                                                        (nyxt-user-packages)\n                                                                        (nyxt-extension-packages))))\n  \"`nsymbols:resolve-symbol' wrapper, only resolving strings, keywords, and NYXT-USER symbols.\nUseful for user configuration smarts, returns unaltered DESIGNATOR otherwise.\"\n  (etypecase designator\n    (string (sym:resolve-symbol designator type packages))\n    (keyword (sym:resolve-symbol designator type packages))\n    (symbol (if (eq (symbol-package designator) (find-package :nyxt-user))\n                (sym:resolve-symbol designator type packages)\n                designator))))\n\n;; NOTE: We define it here so that it's available in spinneret-tags.lisp.\n(export-always 'resolve-backtick-quote-links)\n(defun resolve-backtick-quote-links (string parent-package)\n  \"Return the STRING documentation with symbols surrounded by the (` ') pair\nturned into <a> links to their respective description page.\"\n  (labels ((resolve-as (symbol type)\n             (sym:resolve-symbol symbol type (list :nyxt :nyxt-user parent-package)))\n           (resolve-regex (target-string start end match-start match-end reg-starts reg-ends)\n             (declare (ignore start end reg-starts reg-ends))\n             ;; Excluding backtick & quote.\n             (let* ((name (subseq target-string (1+ match-start) (1- match-end)))\n                    (symbol (ignore-errors (uiop:safe-read-from-string\n                                            name :package parent-package :eof-error-p nil)))\n                    (function (and symbol\n                                   (fboundp symbol)\n                                   (resolve-as symbol :function)))\n                    (variable (when symbol\n                                (resolve-as symbol :variable)))\n                    (class (when symbol\n                             (resolve-as symbol :class)))\n                    ;; TODO: No way to determine the class reliably based on the slot name?\n                    ;; (slot (resolve-symbol name :slot (list :nyxt :nyxt-user *package*)))\n                    (url (cond\n                           ((and variable (not function) (not class))\n                            (nyxt-url 'describe-variable :variable variable))\n                           ((and class (not function) (not variable))\n                            (nyxt-url 'describe-class :class class))\n                           ((and function (not class) (not variable))\n                            (nyxt-url 'describe-function :fn function))\n                           (symbol\n                            (nyxt-url 'describe-any :input symbol))\n                           (t nil))))\n               (let ((*print-pretty* nil))\n                 ;; Disable pretty-printing to avoid spurious space insertion within links:\n                 ;; https://github.com/ruricolist/spinneret/issues/37#issuecomment-884740046\n                 (spinneret:with-html-string\n                   (if url\n                       (:a :href url (:code name))\n                       (:code name)))))))\n    (if (not (uiop:emptyp string))\n        ;; FIXME: Spaces are disallowed, but |one can use anything in a symbol|.\n        ;; Maybe allow it?  The problem then is that it increases the chances of\n        ;; false-positives when the \"`\" character is used for other reasons.\n        (ppcre:regex-replace-all \"`[^'\\\\s]+'\" string #'resolve-regex)\n        \"\")))\n\n(-> find-submode (sym:mode-symbol &optional buffer) (maybe mode))\n(export-always 'find-submode)\n(defun find-submode (mode-symbol &optional (buffer (current-buffer)))\n  \"Return the first submode instance of MODE-SYMBOL in BUFFER.\nAs a second value, return all matching submode instances.\nReturn nil if mode is not found.\"\n  (when (modable-buffer-p buffer)\n    (if-let ((class (mode-class mode-symbol)))\n      (let ((results (sera:filter\n                      (rcurry #'closer-mop:subclassp class)\n                      (enabled-modes buffer)\n                      :key #'class-of)))\n        (when (< 1 (length results))\n          ;; TODO: What's the best action on multiple mode match?\n          (log:debug \"Found multiple matching modes: ~a\" results))\n        (values (first results)\n                results))\n      ;; CCL catches the error at compile time but not all implementations do,\n      ;; hence the redundant error report here.\n      (error \"Mode ~a does not exist\" mode-symbol))))\n\n(-> current-mode ((or keyword string) &optional buffer) (maybe mode))\n(export-always 'current-mode)\n(defun current-mode (mode-designator &optional (buffer (current-buffer)))\n  \"Return mode instance of MODE-DESIGNATOR in BUFFER.\nReturn NIL if none.\nThe \\\"-mode\\\" suffix is automatically appended to MODE-KEYWORD if missing.\nThis is convenience function for interactive use.\nFor production code, see `find-submode' instead.\"\n  (let ((mode-designator (sera:ensure-suffix (string mode-designator) \"-MODE\")))\n    (find-submode (resolve-user-symbol mode-designator :mode)\n                  buffer)))\n\n(defun all-mode-symbols ()\n  \"Return the list of mode symbols.\"\n  (mapcar #'class-name (mopu:subclasses 'mode)))\n\n(defun make-mode-suggestion (mode &optional source input)\n  \"Return a `suggestion' wrapping around MODE. \"\n  (declare (ignore source input))\n  (make-instance 'prompter:suggestion\n                 :value mode\n                 :attributes `((\"Mode\" ,(string-downcase (symbol-name mode)))\n                               (\"Documentation\" ,(documentation-line mode 'type \"\"))\n                               (\"Package\" ,(string-downcase (package-name (symbol-package mode)))))))\n\n(define-class mode-source (prompter:source)\n  ((prompter:name \"Modes\")\n   (prompter:enable-marks-p t)\n   (prompter:constructor (sort (all-mode-symbols) #'string< :key #'symbol-name))\n   (prompter:filter-preprocessor #'prompter:filter-exact-matches)\n   (prompter:suggestion-maker 'make-mode-suggestion))\n  (:export-class-name-p t)\n  (:metaclass user-class)\n  (:documentation \"Source for all the existing modes.\"))\n\n(defmethod prompter:object-attributes ((mode mode) (source prompter:source))\n  (declare (ignore source))\n  `((\"Name\" ,mode)))\n\n(define-class active-mode-source (mode-source)\n  ((prompter:name \"Active modes\")\n   (buffers '())\n   (prompter:enable-marks-p t)\n   (prompter:constructor (lambda (source)\n                           (delete-duplicates\n                            (mapcar\n                             #'name\n                             (mappend\n                              #'enabled-modes\n                              (uiop:ensure-list (buffers source))))))))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:metaclass user-class)\n  (:documentation \"Source listing names of all the `enable'd modes in `buffers'.\"))\n\n(define-class inactive-mode-source (mode-source)\n  ((prompter:name \"Inactive modes\")\n   (buffers '())\n   (prompter:enable-marks-p t)\n   (prompter:constructor (lambda (source)\n                           (let ((common-modes\n                                   (reduce #'intersection\n                                           (mapcar (lambda (b)\n                                                     (mapcar #'name (enabled-modes b)))\n                                                   (uiop:ensure-list (buffers source))))))\n                             (set-difference (all-mode-symbols) common-modes)))))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:metaclass user-class)\n  (:documentation \"Source listing names of modes not yet `enable'd (or `disable'd) in `buffers'.\"))\n\n(export-always 'enable-modes*)\n(defgeneric enable-modes* (modes buffers &rest args &key &allow-other-keys)\n  ;; FIXME: Better type dispatching? The types used to be:\n  ;; (-> enable-modes* ((or sym:mode-symbol (list-of sym:mode-symbol))\n  ;;                    (or buffer (list-of buffer))\n  ;;                    &key &allow-other-keys) *)\n  ;; TODO: accept a list of mode objects as well as symbols?\n  (:method (modes buffers &rest args &key &allow-other-keys)\n    (let ((modes (uiop:ensure-list modes))\n          (buffers (uiop:ensure-list buffers)))\n      (dolist (mode modes)\n        (check-type mode sym:mode-symbol))\n      (dolist (buffer buffers)\n        (check-type buffer buffer))\n      (mapcar (lambda (buffer)\n                (mapcar (lambda (mode-sym)\n                          (apply #'enable (or (find mode-sym (modes buffer) :key #'name)\n                                              (make-instance mode-sym :buffer buffer))\n                                 args))\n                        modes)\n                buffer)\n              (sera:filter #'modable-buffer-p buffers))))\n  (:documentation \"Enable MODES in BUFFERS.\nARGS are the keyword arguments for `make-instance'/`enable' on MODES.\"))\n\n(define-command enable-modes (&key\n                              (modes nil modes-supplied-p)\n                              (buffers (current-buffer) buffers-supplied-p))\n  \"Enable MODES for BUFFERS prompting for either or both.\nMODES should be a list of mode symbols or a mode symbol.\nBUFFERS and MODES are automatically coerced into a list.\n\nIf BUFFERS is a list, return it.\nIf it's a single buffer, return it directly (not as a list).\"\n  ;; We allow NIL values for MODES and BUFFERS in case they are forms, in which\n  ;; case it's handy that this function does not error, it simply does nothing.\n  ;; REVIEW: But we wrap commands into `with-protect' for this, don't we?\n  (let* ((buffers (or buffers\n                      (unless buffers-supplied-p\n                        (prompt\n                         :prompt \"Enable mode(s) for buffer(s)\"\n                         :sources (make-instance 'buffer-source\n                                                 :enable-marks-p t\n                                                 :actions-on-return '())))))\n         (modes (or modes\n                    (unless modes-supplied-p\n                      (prompt\n                       :prompt \"Enable mode(s)\"\n                       :sources (make-instance 'inactive-mode-source\n                                               :buffers buffers))))))\n    (enable-modes* modes buffers))\n  buffers)\n\n(export-always 'disable-modes*)\n(defgeneric disable-modes* (modes buffers &rest args &key &allow-other-keys)\n  ;; FIXME: Better type dispatching?\n  (:method (modes buffers &rest args &key &allow-other-keys)\n    (declare (ignorable args))\n    (let ((modes (uiop:ensure-list modes))\n          (buffers (uiop:ensure-list buffers)))\n      (dolist (mode modes)\n        (check-type mode sym:mode-symbol))\n      (dolist (buffer buffers)\n        (check-type buffer buffer))\n      (mapcar (lambda (buffer)\n                (mapcar #'disable\n                        (delete nil (mapcar (lambda (mode) (find mode (enabled-modes buffer) :key #'name))\n                                            modes))))\n              buffers)))\n  (:documentation \"Disable MODES in BUFFERS.\"))\n\n(define-command disable-modes (&key (modes nil modes-supplied-p)\n                               (buffers (current-buffer) buffers-supplied-p))\n  \"Disable MODES for BUFFERS.\nMODES should be a list of mode symbols.\nBUFFERS and MODES are automatically coerced into a list.\n\nIf BUFFERS is a list, return it.\nIf it's a single buffer, return it directly (not as a list).\"\n  (let* ((buffers (or buffers\n                      (unless buffers-supplied-p\n                        (prompt\n                         :prompt \"Enable mode(s) for buffer(s)\"\n                         :sources (make-instance 'buffer-source\n                                                 :enable-marks-p t\n                                                 :actions-on-return '())))))\n         (modes (or modes\n                    (unless modes-supplied-p\n                      (prompt\n                       :prompt \"Disable mode(s)\"\n                       :sources (make-instance 'active-mode-source\n                                               :buffers buffers))))))\n    (disable-modes* modes buffers))\n  buffers)\n\n(define-command toggle-modes (&key (buffer (current-buffer)))\n  \"Enable marked modes, disable unmarked modes for BUFFER.\"\n  (let* ((modes-to-enable\n           (prompt\n            :prompt \"Mark modes to enable, unmark to disable\"\n            :sources (make-instance\n                      'mode-source\n                      :marks (mapcar #'sera:class-name-of (enabled-modes buffer)))))\n         (modes-to-disable (set-difference (all-mode-symbols) modes-to-enable\n                                           :test #'string=)))\n    (disable-modes* modes-to-disable buffer)\n    (enable-modes* modes-to-enable buffer))\n  buffer)\n\n;; TODO: Factor `toggle-mode' and `toggle-modes' somehow?\n;; TODO: Shall we have a function that returns the focused buffer?\n;; `focused-buffer'?  `current-buffer*'?  Rename `current-buffer' to\n;; `current-view-buffer' and add `current-buffer' for this task?\n(defun toggle-mode (mode-sym\n                    &rest args\n                    &key (buffer (or (current-prompt-buffer) (current-buffer)))\n                      (activate t activate-supplied-p)\n                    &allow-other-keys)\n  \"Enable MODE-SYM if not already enabled, disable it otherwise.\"\n  (when (modable-buffer-p buffer)\n    (let ((existing-instance (find mode-sym (modes buffer) :key #'sera:class-name-of)))\n      (unless activate-supplied-p\n        (setf activate (or (not existing-instance)\n                           (not (enabled-p existing-instance)))))\n      (if activate\n          ;; TODO: Shall we pass args to `make-instance' or `enable'?\n          ;; Have 2 args parameters?\n          (let ((mode (or existing-instance\n                          (apply #'make-instance mode-sym\n                                 :buffer buffer\n                                 args))))\n            (enable mode)\n            (echo \"~@(~a~) mode enabled.\" mode))\n          (when existing-instance\n            (disable existing-instance)\n            (echo \"~@(~a~) mode disabled.\" existing-instance))))))\n\n(export-always 'find-buffer)\n(defun find-buffer (mode-symbol)\n  \"Return first buffer matching MODE-SYMBOL.\"\n  (find-if (lambda (b)\n             (find-submode mode-symbol b))\n           (buffer-list)))\n\n(export-always 'keymap)\n(defmethod keymap ((mode mode))\n  \"Return the `nkeymaps:keymap' of MODE according to its buffer `nyxt/mode/keyscheme::keyscheme'.\nIf there is no corresponding keymap, return NIL.\"\n  (keymaps:get-keymap (if (buffer mode)\n                          (keyscheme (buffer mode))\n                          keyscheme:cua)\n                      (keyscheme-map mode)))\n\n(defmethod url-sources ((mode mode) actions-on-return)\n  (declare (ignore actions-on-return))\n  nil)\n\n(defmethod url-sources :around ((mode mode) actions-on-return)\n  (declare (ignore actions-on-return))\n  (alex:ensure-list (call-next-method)))\n\n(defmethod s-serialization:serializable-slots ((object mode))\n  \"Discard keymaps which can be quite verbose.\"\n  (delete 'keyscheme-map\n          (mapcar #'closer-mop:slot-definition-name\n                  (closer-mop:class-slots (class-of object)))))\n"
  },
  {
    "path": "source/package.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n;; Some compilers (e.g. SBCL) fail to reload the system with `defpackage' when\n;; exports are spread around.  `uiop:define-package' does not have this problem.\n\n#+sb-package-locks\n(serapeum:eval-always\n  (when (find-package :nyxt)\n    (sb-ext:unlock-package :nyxt)))\n\n(uiop:define-package :nyxt\n  (:use :cl)\n  (:use-reexport :nyxt/utilities :nyxt/types)\n  (:export #:use-nyxt-package-nicknames)\n  (:documentation \"The core package of Nyxt, the infinitely extensible browser.\n\nThis package should not be modified by the users.\n\nIt's recommended to use the `nyxt-user' package instead to create new functions,\nmodes, commands, etc.\"))\n#+sb-package-locks\n(sb-ext:lock-package :nyxt)\n\n(in-package :nyxt)\n(defvar *imports*\n  '((:alexandria #:compose #:curry #:mappend #:rcurry\n     #:if-let #:when-let #:when-let*\n     #:assoc-value)\n    (:trivia #:match #:multiple-value-match #:lambda-match #:guard)\n    (:nkeymaps #:define-key #:define-keyscheme-map)\n    (:serapeum #:export-always #:-> #:and-let* #:eval-always))\n  \"Default list of symbol imports used by `nyxt:define-package'.\")\n\n(loop :for (package . symbols) in *imports*\n      :do (import (mapcar (lambda (symbol) (intern (symbol-name symbol) package))\n                          symbols)\n                  :nyxt))\n\n(eval-when (:compile-toplevel :load-toplevel :execute)\n  (loop :for (nickname package) in\n        '((:alex :alexandria-2)\n          (:sera :serapeum)\n          (:time :local-time)\n          (:types :trivial-types)\n          (:lpara :lparallel)\n          (:hooks :nhooks)\n          (:files :nfiles)\n          (:j :njson/aliases)\n          (:keymaps :nkeymaps)\n          (:sym :nsymbols))\n        :do (trivial-package-local-nicknames:add-package-local-nickname\n             nickname package :nyxt)))\n\n(defmacro nyxt::use-nyxt-package-nicknames (&optional (package *package*))\n  \"Define package nicknames in PACKAGE for Nyxt-used libraries.\nEffectively makes programming in PACKAGE same as programming in `:nyxt'.\"\n  `(eval-when (:compile-toplevel :load-toplevel :execute)\n     (let ((*package* (find-package ,package)))\n       (dolist (pkgs (trivial-package-local-nicknames:package-local-nicknames\n                      :nyxt))\n         (trivial-package-local-nicknames:add-package-local-nickname\n          (first pkgs) (package-name (rest pkgs))\n          (find-package ,package))))))\n\n(defmacro without-package-locks (&body body)\n  \"Ignore package locks for the duration of the BODY.\nSame as `progn' on implementations that don't have package locks.\"\n  #+sb-package-locks\n  `(sb-ext:without-package-locks\n     ,@body)\n  #+(and ecl package-locks)\n  `(ext:without-package-locks\n     ,@body)\n  #-(or sb-package-locks package-locks)\n  `(progn ,@body))\n\n(serapeum:export-always 'define-class :nyxt)\n(defmacro define-class (name supers slots &rest options)\n  \"`nclasses:define-class' with automatic types and always-dashed predicates.\"\n  `(nclasses:define-class ,name ,supers ,slots\n     ,@(append\n        '((:automatic-types-p t)\n          (:accessor-name-package :slot-name)\n          (:predicate-name-transformer\n           'nclasses:always-dashed-predicate-name-transformer))\n       options)))\n\n(serapeum:export-always 'define-package :nyxt)\n(defmacro define-package (name &body options)\n  \"A helper around `uiop:define-package'.\n`:cl' and `:nyxt' are automatically used.\n`nyxt::*imports*' are automatically imported.\"\n  (let* ((uses (append (serapeum:keep :use options :key #'first)\n                       '((:use :cl :nyxt :nyxt/utilities))))\n         (imports (append (serapeum:keep :import-from options :key #'first)\n                          (mapcar (lambda (import) (cons :import-from import))\n                                  *imports*)))\n         (options (remove :use (remove :import-from options :key #'first)\n                          :key #'first)))\n    `(progn\n       (serapeum:eval-always\n         (without-package-locks\n           (uiop:define-package ,name\n             ,@uses\n             ,@imports\n             ,@options)))\n       (nyxt::use-nyxt-package-nicknames ',name)\n       #+sb-package-locks\n       (sb-ext:lock-package ',name))))\n\n(deftype class-symbol ()\n  `(and symbol (satisfies find-class)))\n\n(uiop:define-package :nyxt-user\n  (:use :cl :nyxt :nyxt/utilities)\n  (:import-from :nkeymaps #:define-key #:define-keyscheme-map)\n  (:documentation \"Package left for the user to fiddle with.  If the\nconfiguration file package is left unspecified, it defaults to this.  It's not\nrecommended to use `nyxt' itself to avoid clobbering internal symbols.\n\nBy default, the `:nyxt' and `:cl' packages are `:use'd.\n\nTo import more symbols, you can use the `import' function.\nFor instance, to access `match' directly (without having to prefix it with\n`trivia:', add this at the top of your configuration file:\n\n  (import 'trivia:match)\n\nYou can also use package local nicknames if you want to abbreviate package\nprefix.\nFor instance, to be able to use `alex:' and `sera:' in place of `alexandria:'\nand `serapeum:':\n\n  (trivial-package-local-nicknames:add-package-local-nickname\n    :alex :alexandria-2 :nyxt-user)\n  (trivial-package-local-nicknames:add-package-local-nickname\n    :sera :serapeum :nyxt-user)\"))\n\n(trivial-package-local-nicknames:add-package-local-nickname\n :hooks :nhooks :nyxt-user)\n(trivial-package-local-nicknames:add-package-local-nickname\n :files :nfiles :nyxt-user)\n(trivial-package-local-nicknames:add-package-local-nickname\n :keymaps :nkeymaps :nyxt-user)\n"
  },
  {
    "path": "source/parenscript-macro.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n;; `uiop:define-package' instead of `nyxt:define-package' since it does not\n;; depend on Nyxt.\n(uiop:define-package :nyxt/parenscript\n  (:nicknames :nyxt/ps)\n  (:use :cl :parenscript)\n  (:import-from :serapeum #:export-always))\n\n(in-package :nyxt/parenscript)\n(nyxt:use-nyxt-package-nicknames)\n\n(export-always 'qs)\n(defpsmacro qs (context selector)\n  \"Alias of context.querySelector()\"\n  `(chain ,context (query-selector ,selector)))\n\n(export-always 'qsa)\n(defpsmacro qsa (context selector)\n  \"Alias of context.querySelectorAll()\"\n  `(chain ,context (query-selector-all ,selector)))\n\n(export-always 'qs-id)\n(defpsmacro qs-id (context id)\n  \"Alias of context.getElementById()\"\n  `(chain ,context (get-element-by-id ,id)))\n\n(export-always 'qs-nyxt-id)\n(defpsmacro qs-nyxt-id (context id)\n  \"context.querySelector() tailored for Nyxt IDs.\"\n  `(chain ,context (query-selector (stringify \"[nyxt-identifier=\\\"\" ,id \"\\\"]\"))))\n\n(export-always 'rqs-nyxt-id)\n(defpsmacro rqs-nyxt-id (context id)\n  \"Recursive version of `qs-nyxt-id` which goes through Shadow DOMs if there's\nat least one.\"\n  `(flet ((recursive-query-selector (context selector)\n            (let ((node (qs context selector))\n                  (shadow-roots (chain *array (from (qsa context \"[nyxt-shadow-root]\"))))\n                  shadow-root)\n              (do ((i 0 (1+ i)))\n                  ((or node\n                       (>= i (chain shadow-roots length))))\n                (setf shadow-root (chain (elt shadow-roots i) shadow-root))\n                (chain shadow-roots push (apply shadow-roots (chain *array (from (qsa shadow-root \"[nyxt-shadow-root]\")))))\n                (setf node (qs shadow-root selector)))\n              node)))\n     (if (chain ,context (query-selector \"[nyxt-shadow-root]\"))\n         (recursive-query-selector ,context (stringify \"[nyxt-identifier=\\\"\" ,id \"\\\"]\"))\n         (qs-nyxt-id ,context ,id))))\n\n(export-always 'active-element)\n(defpsmacro active-element (context)\n  \"Shorthand for active element in CONTEXT.\"\n  `(@ ,context active-element))\n\n(defpsmacro get-caret ()\n  `(let* ((element (active-element document))\n          (tag-name (chain element tag-name)))\n     (cond\n      ((or (string= tag-name \"INPUT\") (string= tag-name \"TEXTAREA\"))\n       (list (chain element selection-start) (chain element selection-end)))\n      ((chain element is-content-editable)\n       (let ((range (chain window (get-selection) (get-range-at 0))))\n         (list (@ range start-offset) (@ range end-offset)))))))\n\n(defpsmacro set-caret (element &optional start end)\n  `(let* ((element ,element)\n          (tag-name (chain element tag-name))\n          (start ,start)\n          (end ,end))\n     (unless (active-element document)\n       (chain element (focus)))\n     (cond\n       ((or (string= tag-name \"INPUT\")\n            (string= tag-name \"TEXTAREA\"))\n        (setf (chain element selection-start) (or start nil)\n              (chain element selection-end) (or end start nil)))\n       ((chain element is-content-editable)\n        (let* ((selection (chain window (get-selection)))\n               (range (chain document (create-range))))\n          (when (and selection (chain selection (get-range-at 0)))\n            (chain selection (remove-all-ranges)))\n          (when start\n            (chain range (set-start element start))\n            (if end\n                (chain range (set-end element end))\n                (chain range (set-end element start)))\n            (chain window (get-selection) (add-range range))))))))\n\n(export-always 'insert-at)\n(defpsmacro insert-at (tag input-text)\n  \"Insert text at a tag.\"\n  `(let* ((element ,tag)\n          (caret (get-caret))\n          (origin (@ caret 0))\n          (end (or (@ caret 1) origin))\n          (tag-name (chain element tag-name)))\n     (cond\n       ((or (string= tag-name \"INPUT\")\n            (string= tag-name \"TEXTAREA\"))\n        (setf (chain element value)\n              (+ (chain element value (substring 0 origin))\n                 ,input-text\n                 (chain element value\n                        (substring end (chain element value length))))))\n       ((chain element is-content-editable)\n        ;; TODO: Implement caret movement, as in\n        ;; https://stackoverflow.com/questions/6249095/how-to-set-the-caret-cursor-position-in-a-contenteditable-element-div\n        (setf (chain element inner-text)\n              (+ (chain element inner-text (substring 0 origin))\n                 ,input-text\n                 (chain element inner-text\n                        (substring end\n                                   (chain element inner-text length)))))))\n     (set-caret\n      (if (= origin end)\n          (+ origin (chain ,input-text length))\n          origin)\n      (+ origin (chain ,input-text length)))))\n\n(export-always 'element-editable-p)\n(defpsmacro element-editable-p (element)\n  \"Whether ELEMENT is editable.\"\n  `(let ((tag (chain ,element tag-name)))\n     (if (or (and (string= tag \"INPUT\")\n                  ;; The list of all input types:\n                  ;; https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input\n                  (not (chain ([] \"hidden\" \"checkbox\" \"button\") (includes (chain ,element type))))\n                  (not (chain ,element disabled)))\n             (string= tag \"TEXTAREA\")\n             (chain ,element is-content-editable))\n         t f)))\n\n(export-always 'element-drawable-p)\n(defpsmacro element-drawable-p (element)\n  \"Whether ELEMENT is drawable.\"\n  `(if (or (chain ,element offset-width)\n           (chain ,element offset-height)\n           (chain ,element (get-client-rects) length))\n       t f))\n\n(export-always 'element-in-view-port-p)\n(defpsmacro element-in-view-port-p (element)\n  \"Whether ELEMENT is in viewport.\"\n  `(let* ((rect (chain ,element (get-bounding-client-rect)))\n          (computed-style (chain window (get-computed-style ,element))))\n     (if (and (>= (chain rect top) 0)\n              ;; a partially visible element is still visible\n              (<= (chain rect top) (- (chain window inner-height) 1))\n              (>= (chain rect left) 0)\n              ;; a partially visible element is still visible\n              (<= (chain rect left) (- (chain window inner-width) 1))\n              ;; some elements have top=bottom=left=right\n              (> (chain rect width) 3)\n              (> (chain rect height) 3)\n              (not (= (chain computed-style \"visibility\") \"hidden\"))\n              (not (= (chain computed-style \"display\") \"none\")))\n         t nil)))\n\n(export-always 'element-overlapped-p)\n(defpsmacro element-overlapped-p (element)\n  \"Whether ELEMENT is overlapped by another element.\"\n  ;; Inspired by the algorithm from saka-key, see:\n  ;; https://github.com/lusakasa/saka-key/blob/v1.26.3/src/modes/hints/client/findHints.js#L114\n  `(let* ((rect (chain ,element (get-bounding-client-rect)))\n          (computed-style (chain window (get-computed-style ,element)))\n          (coord-truncation-offset 2)\n          (radius (parse-float (chain computed-style border-top-left-radius)))\n          (rounded-border-offset (ceiling (* radius (- 1 (sin (/ pi 4))))))\n          (offset (max coord-truncation-offset rounded-border-offset))\n          (el (chain ,element (get-root-node) (element-from-point (+ (chain rect left) offset)\n                                                                  (+ (chain rect top) offset)))))\n     (if (or (>= offset (chain rect width))\n             (>= offset (chain rect height)))\n         t\n         (progn (loop while (and el (not (eq el element)))\n                      do (setf el (if (instanceof (chain el parent-node) *shadow-root)\n                                      (chain el parent-node host)\n                                      (chain el parent-node))))\n                (null el)))))\n\n(export-always 'element-invisible-p)\n(defpsmacro element-invisible-p (element)\n  \"Whether ELEMENT is invisible.\"\n  `(or (= (@ ,element offset-height)\n          0)\n       (= (chain window (get-computed-style ,element) \"visibility\")\n          \"hidden\")))\n\n(export-always 'add-class-nyxt-id)\n(defpsmacro add-class-nyxt-id (id class)\n  \"element.classList.add(class) tailored for Nyxt IDs.\"\n  `(let ((element (nyxt/ps:qs-nyxt-id document (ps:lisp ,id))))\n     (ps:chain element class-list (add ,class))))\n\n(export-always 'remove-class-nyxt-id)\n(defpsmacro remove-class-nyxt-id (id class)\n  \"element.classList.remove(class) tailored for Nyxt IDs.\"\n  `(let ((element (nyxt/ps:qs-nyxt-id document (ps:lisp ,id))))\n     (ps:chain element class-list (remove ,class))))\n\n(export-always 'rqsa)\n(defpsmacro rqsa (context selector)\n  \"Recursive version of context.querySelectorAll() which goes through\nShadow DOMs if there's at least one.\"\n  `(flet ((recursive-query-selector-all (context selector)\n            (ps:let ((tree-walker (ps:chain document (create-tree-walker context (ps:@ *Node +element_node+))))\n                     (results (array)))\n              (ps:loop while (ps:chain tree-walker (next-node))\n                 do (when (ps:chain tree-walker current-node (matches selector))\n                      (ps:chain results (push (ps:@ tree-walker current-node))))\n                    (let ((shadow-root (ps:@ tree-walker current-node shadow-root)))\n                      (when shadow-root\n                        (ps:chain *array prototype push (apply results (recursive-query-selector-all shadow-root selector))))))\n              results)))\n     (if (chain ,context (query-selector \"[nyxt-shadow-root]\"))\n         (recursive-query-selector-all ,context ,selector)\n         (qsa ,context ,selector))))\n"
  },
  {
    "path": "source/prompt-buffer.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt)\n\n(eval-always\n  (define-class prompt-buffer\n      (network-buffer input-buffer modable-buffer prompter:prompter)\n    ((window\n      nil\n      :type (or null window)\n      :export nil\n      :documentation \"The window in which the prompt buffer is showing.\")\n     (height\n      :default\n      :type (or keyword integer)\n      :writer nil\n      :reader height\n      :export t\n      :documentation \"The height occupied by the prompt buffer.\nThe options are:\n- `:default', which sets it to a third of the window's height;\n- an integer, which corresponds to the height in pixels.\")\n     (prompter:history\n      (prompt-buffer-generic-history *browser*)\n      ;; Both set to nil since it overrides the default value.\n      :accessor nil\n      :export nil\n      :documentation\n      \"Override `prompter:history' to use input history globally.\")\n     (invisible-input-p\n      nil\n      :documentation \"Whether to replace input by a placeholder character.\nThis is useful to conceal passwords.\")\n     (hide-suggestion-count-p\n      nil\n      :documentation \"Whether to hide the number of suggestions.\nAffects both the prompt and its sources.\")\n     (max-suggestions\n      0\n      :export nil\n      :documentation \"Maximum number of total suggestions that were listed at\nsome point.\")\n     (style\n      (theme:themed-css (theme *browser*)\n        '(:font-face :font-family \"public sans\" :font-style \"normal\"\n          :font-weight \"400\" :src\n          \"url('nyxt-resource:PublicSans-Regular.woff')\" \"format('woff')\")\n        '(:font-face :font-family \"public sans\" :font-style \"italic\"\n          :font-weight \"400\" :src\n          \"url('nyxt-resource:PublicSans-Italic.woff')\" \"format('woff')\")\n        '(:font-face :font-family \"public sans\" :font-style \"normal\"\n          :font-weight \"100\" :src\n          \"url('nyxt-resource:PublicSans-Thin.woff')\" \"format('woff')\")\n        '(:font-face :font-family \"public sans\" :font-style \"italic\"\n          :font-weight \"100\" :src\n          \"url('nyxt-resource:PublicSans-ThinItalic.woff')\" \"format('woff')\")\n        '(:font-face :font-family \"public sans\" :font-style \"normal\"\n          :font-weight \"200\" :src\n          \"url('nyxt-resource:PublicSans-ExtraLight.woff')\" \"format('woff')\")\n        '(:font-face :font-family \"public sans\" :font-style \"italic\"\n          :font-weight \"200\" :src\n          \"url('nyxt-resource:PublicSans-ExtraLightItalic.woff')\"\n          \"format('woff')\")\n        '(:font-face :font-family \"public sans\" :font-style \"normal\"\n          :font-weight \"300\" :src\n          \"url('nyxt-resource:PublicSans-Light.woff')\" \"format('woff')\")\n        '(:font-face :font-family \"public sans\" :font-style \"italic\"\n          :font-weight \"300\" :src\n          \"url('nyxt-resource:PublicSans-LightItalic.woff')\" \"format('woff')\")\n        '(:font-face :font-family \"public sans\" :font-style \"normal\"\n          :font-weight \"500\" :src\n          \"url('nyxt-resource:PublicSans-Medium.woff')\" \"format('woff')\")\n        '(:font-face :font-family \"public sans\" :font-style \"italic\"\n          :font-weight \"500\" :src\n          \"url('nyxt-resource:PublicSans-MediumItalic.woff')\" \"format('woff')\")\n        '(:font-face :font-family \"public sans\" :font-style \"normal\"\n          :font-weight \"600\" :src\n          \"url('nyxt-resource:PublicSans-SemiBold.woff')\" \"format('woff')\")\n        '(:font-face :font-family \"public sans\" :font-style \"italic\"\n          :font-weight \"600\" :src\n          \"url('nyxt-resource:PublicSans-SemiBoldItalic.woff')\" \"format('woff')\")\n        '(:font-face :font-family \"public sans\" :font-style \"normal\"\n          :font-weight \"700\" :src\n          \"url('nyxt-resource:PublicSans-Bold.woff')\" \"format('woff')\")\n        '(:font-face :font-family \"public sans\" :font-style \"italic\"\n          :font-weight \"700\" :src\n          \"url('nyxt-resource:PublicSans-BoldItalic.woff')\" \"format('woff')\")\n        '(:font-face :font-family \"public sans\" :font-style \"normal\"\n          :font-weight \"800\" :src\n          \"url('nyxt-resource:PublicSans-ExtraBold.woff')\" \"format('woff')\")\n        '(:font-face :font-family \"public sans\" :font-style \"italic\"\n          :font-weight \"800\" :src\n          \"url('nyxt-resource:PublicSans-ExtraBoldItalic.woff')\"\n          \"format('woff')\")\n        '(:font-face :font-family \"public sans\" :font-style \"normal\"\n          :font-weight \"900\" :src\n          \"url('nyxt-resource:PublicSans-Black.woff')\" \"format('woff')\")\n        '(:font-face :font-family \"public sans\" :font-style \"italic\"\n          :font-weight \"900\" :src\n          \"url('nyxt-resource:PublicSans-BlackItalic.woff')\" \"format('woff')\")\n        '(:font-face :font-family \"dejavu sans mono\" :src\n          \"url('nyxt-resource:DejaVuSansMono.ttf')\" \"format('ttf')\")\n        '(*\n          :font-size \"14px\"\n          :line-height \"18px\")\n        `(body\n          :background-color ,theme:background-color-\n          :font-family ,theme:font-family\n          :margin \"0\")\n        '(\"#root\"\n          :height \"100%\"\n          :display \"grid\"\n          :grid-template-rows \"auto 1fr\")\n        `(\"#prompt-area\"\n          :margin \"4px\"\n          :border-radius \"3px\"\n          :background-color ,theme:primary-color\n          :color ,theme:on-primary-color\n          :border-top \"2px solid\"\n          :border-bottom \"2px solid\"\n          :border-color ,theme:primary-color\n          :display \"grid\"\n          :grid-template-columns \"auto auto 1fr auto auto\")\n        `(\"#prompt\"\n          :background-color ,theme:primary-color\n          :color ,theme:on-primary-color\n          :padding-left \"10px\"\n          :line-height \"28px\"\n          :max-width \"40ch\"\n          :overflow \"hidden\"\n          :white-space \"nowrap\"\n          :text-overflow \"ellipsis\")\n        '(\"#prompt-input\"\n          :min-width \"10ch\"\n          :line-height \"28px\"\n          :padding-right \"10px\")\n        `(\"#prompt-extra\"\n          :font-family ,theme:monospace-font-family\n          :min-width \"12px\"\n          :background-color ,theme:primary-color\n          :color ,theme:on-primary-color\n          :line-height \"28px\"\n          :padding-right \"5px\")\n        `(\"#prompt-modes\"\n          :line-height \"28px\"\n          :padding-left \"3px\"\n          :padding-right \"3px\")\n        `(\"#close-button\"\n          :text-align \"right\"\n          :padding-right \"7px\"\n          :background-color ,theme:primary-color\n          :min-width \"24px\"\n          :line-height \"28px\"\n          :font-weight \"bold\"\n          :font-size \"20px\")\n        '(button\n          :background \"transparent\"\n          :color \"inherit\"\n          :text-decoration \"none\"\n          :border \"none\"\n          :padding 0\n          :font \"inherit\"\n          :outline \"inherit\")\n        `(.button.action\n          :background-color ,theme:action-color\n          :color ,theme:on-action-color)\n        `((:and .button :hover)\n          :cursor \"pointer\"\n          :color ,theme:action-color)\n        `(\".button:hover svg path\"\n          :stroke ,theme:action-color-)\n        `((:and .button (:or :visited :active))\n          :color ,theme:background-color)\n        `(input\n          :font-family ,theme:monospace-font-family)\n        `(\"#input\"\n          :border-radius \"4px\"\n          :height \"28px\"\n          :background-color ,theme:background-color\n          :color ,theme:on-background-color\n          :border 2px solid ,theme:secondary-color\n          :outline \"none\"\n          :width \"100%\"\n          :autofocus \"true\")\n        `(\"#input:focus\"\n          :border-color\n          ,(cl-colors-ng:print-hex theme:action-color- :print-alpha 0.40))\n        '(\".source\"\n          :margin-left \"10px\"\n          :margin-top \"15px\")\n        `(\".source-name\"\n          :padding-left \"4px\"\n          :background-color ,theme:secondary-color\n          :color ,theme:on-secondary-color\n          :display \"flex\"\n          :justify-content \"space-between\"\n          :align-items \"stretch\"\n          :border-radius \"3px\")\n        '(\".source-name > div\"\n          :line-height \"26px\")\n        '(\".source-name > div > button\"\n          :padding \"5px 5px 5px 0px\"\n          :min-height \"100%\")\n        '(\"#next-source > svg, #previous-source > svg\"\n          :margin-bottom \"2px\"\n          :height \"5px\")\n        '(\"#previous-source\"\n          :padding 0)\n        '(\"#next-source\"\n          :padding 0)\n        `(\"#suggestions\"\n          :color ,theme:on-background-color\n          :margin-right \"3px\"\n          :overflow \"hidden\")\n        `(\".suggestion-and-mark-count\"\n          :font-family ,theme:monospace-font-family)\n        `(\".source-content\"\n          :box-sizing \"border-box\"\n          :padding-left \"16px\"\n          :margin-left \"2px\"\n          :width \"100%\"\n          :table-layout \"fixed\"\n          (td\n           :color ,theme:on-background-color\n           :overflow \"hidden\"\n           :text-overflow \"ellipsis\"\n           :border-radius \"2px\"\n           :white-space \"nowrap\"\n           :height \"20px\"\n           :padding-left \"4px\")\n          (\"tr:not(:first-child)\"\n           :font-family ,theme:monospace-font-family)\n          (\"tr:hover\"\n           :background-color ,theme:action-color-\n           :color ,theme:on-action-color\n           :cursor \"pointer\")\n          (th\n           :background-color ,theme:secondary-color+\n           :color ,theme:on-secondary-color\n           :font-weight \"normal\"\n           :padding-left \"4px\"\n           :border-radius \"2px\"\n           :text-align \"left\"))\n        `(\"#selection\"\n          :background-color ,theme:action-color\n          :color ,theme:on-action-color)\n        `(.marked\n          :background-color ,theme:secondary-color\n          :color ,theme:on-secondary-color\n          :font-weight \"bold\")\n        `(.selected\n          :background-color ,theme:primary-color\n          :color ,theme:on-primary-color))\n      :documentation \"The CSS applied to prompt buffer.\"))\n    (:export-class-name-p t)\n    (:export-accessor-names-p t)\n    (:export-predicate-name-p t)\n    (:documentation \"The prompt buffer is the interface for user interactions.\nEach prompt spawns a new object: this makes it possible to nest prompts, such as\ninvoking `prompt-buffer:history'.\n\nSee `prompt' for how to invoke prompts.\")\n    (:metaclass user-class)))\n\n(defmethod customize-instance :after ((prompt-buffer prompt-buffer)\n                                      &key extra-modes &allow-other-keys)\n  (hooks:run-hook (prompt-buffer-make-hook *browser*) prompt-buffer)\n  (enable-modes* (append (default-modes prompt-buffer)\n                         (uiop:ensure-list extra-modes))\n                 prompt-buffer))\n\n(defmethod (setf height) (value (prompt-buffer prompt-buffer))\n  (setf (ffi-height prompt-buffer)\n        (case value\n          (:default (round (/ (ffi-height (window prompt-buffer)) 4)))\n          (t value)))\n  (setf (slot-value prompt-buffer 'height) value))\n\n(export-always 'current-source)\n(defun current-source (&optional (prompt-buffer (current-prompt-buffer)))\n  \"Current PROMPT-BUFFER `prompter:source'.\nIf PROMPT-BUFFER is not provided, use `current-prompt-buffer'.\"\n  (prompter:current-source prompt-buffer))\n\n(export-always 'current-suggestion-value)\n(defun current-suggestion-value\n    (&optional (prompt-buffer (current-prompt-buffer)))\n  \"Return selected PROMPT-BUFFER `prompter:suggestion' `prompter:value'.\nReturn `prompter:source' as second value.\nTo access the suggestion instead, see `prompter:%current-suggestion'.\"\n  (multiple-value-bind (suggestion source)\n      (prompter:%current-suggestion prompt-buffer)\n    (values (when suggestion (prompter:value suggestion)) source)))\n\n(defmethod show-prompt-buffer ((prompt-buffer prompt-buffer))\n  (with-slots (window) prompt-buffer\n    (push prompt-buffer (active-prompt-buffers window))\n    (calispel:! (prompt-buffer-ready-channel window) prompt-buffer))\n  (prompt-render-skeleton prompt-buffer)\n  (prompt-render-focus prompt-buffer)\n  (setf (height prompt-buffer) (slot-value prompt-buffer 'height))\n  (ffi-focus-buffer prompt-buffer)\n  (update-prompt-input prompt-buffer)\n  (hooks:run-hook (prompt-buffer-ready-hook *browser*) prompt-buffer))\n\n(defmethod hide-prompt-buffer ((prompt-buffer prompt-buffer))\n  \"Hide PROMPT-BUFFER and display the next active one, if any.\"\n  (with-slots (window) prompt-buffer\n    (alex:deletef (active-prompt-buffers window) prompt-buffer)\n    ;; The channel values are irrelevant, so is the element order:\n    (calispel:? (prompt-buffer-ready-channel window) 0)\n    (ffi-buffer-delete prompt-buffer)\n    (if (active-prompt-buffers window)\n        (show-prompt-buffer (first (active-prompt-buffers window)))\n        (ffi-window-set-buffer window (active-buffer window) :focus t))))\n\n(defun suggestion-and-mark-count (prompt-buffer suggestions marks &key enable-marks-p)\n  \"Return a formatted string with counts of marks (if any) and suggestions.\"\n  (alex:maxf (max-suggestions prompt-buffer)\n             (length suggestions))\n  (unless (hide-suggestion-count-p prompt-buffer)\n    (let ((marks-str (when (or marks enable-marks-p)\n                       (format nil \"~2,' d/\" (length marks)))))\n      (format nil \"[~a~3,' d]\" (or marks-str \"\") (length suggestions)))))\n\n(defmethod render-prompt ((prompt-buffer prompt-buffer))\n  (ps-eval :async t :buffer prompt-buffer\n    (setf (ps:@ (nyxt/ps:qs document \"#prompt-extra\") |innerHTML|)\n          (ps:lisp\n           (suggestion-and-mark-count\n            prompt-buffer\n            (prompter:all-suggestions prompt-buffer)\n            (prompter:all-marks prompt-buffer)\n            :enable-marks-p (some #'prompter:enable-marks-p\n                                  (prompter:sources prompt-buffer)))))\n    (setf (ps:@ (nyxt/ps:qs document \"#prompt-modes\") |innerHTML|)\n          (ps:lisp\n           (str:join\n            \" \"\n            (mapcar (curry #'mode-status\n                           (status-buffer (current-window)))\n                    (sort-modes-for-status (enabled-modes prompt-buffer))))))))\n\n(defmethod attribute-widths ((source prompter:source))\n  \"Return the widths of SOURCE's attribute columns (as ratios).\"\n  ;; In a proportion a:b, a is the \"mean\" and b is the \"extreme\".\n  (let* ((means\n           (mapcar (lambda (attr) (getf (third attr) ':width))\n                   (prompter:active-attributes\n                    (first (prompter:suggestions source))\n                    :source source)))\n         (extreme (ignore-errors (reduce #'+ means))))\n    (if extreme\n        (mapcar (lambda (ratio) (/ ratio extreme)) means)\n        (let ((len (length means)))\n          (log:debug \"Fallback uniform width distribution: no allocation on ~a.\"\n                     source)\n          (make-list len :initial-element (/ 1 len))))))\n\n(defun render-attributes (source prompt-buffer)\n  (spinneret:with-html\n    (when (prompter:suggestions source)\n      (:table :class \"source-content\"\n              (:colgroup\n               (when (prompter:enable-marks-p source)\n                 (:col :style \"width: 25px\"))\n               (dolist (width (attribute-widths source))\n                 (:col :style (format nil \"width: ~,2f%\" (* 100 width)))))\n              (:tr\n               :style (if (sera:single (prompter:active-attributes-keys source))\n                          \"display:none;\"\n                          \"display:revert;\")\n               (when (prompter:enable-marks-p source) (:th \" \"))\n               (loop for attribute-key in (prompter:active-attributes-keys source)\n                     collect (:th (spinneret:escape-string attribute-key))))\n              (loop\n                ;; TODO: calculate how many lines fit in the prompt buffer\n                with max-suggestion-count = 8\n                repeat max-suggestion-count\n                with cursor-index = (prompter:current-suggestion-position prompt-buffer)\n                for suggestion-index from (max 0 (- cursor-index (- (/ max-suggestion-count 2) 1)))\n                for suggestion in (nthcdr suggestion-index (prompter:suggestions source))\n                collect\n                (let ((suggestion-index suggestion-index))\n                  (:tr :id (when (equal (list suggestion source)\n                                        (multiple-value-list\n                                         (prompter:%current-suggestion\n                                          prompt-buffer)))\n                             \"selection\")\n                       :class (when (prompter:marked-p source (prompter:value suggestion))\n                                \"marked\")\n                       (when (prompter:enable-marks-p source)\n                         (:td\n                          (:input\n                           :type \"checkbox\"\n                           :checked (prompter:marked-p\n                                     source (prompter:value suggestion))\n                           :onchange (ps:ps\n                                       (nyxt/ps:lisp-eval\n                                        (:title \"unmark-this-suggestion\"\n                                         :buffer prompt-buffer)\n                                        (prompter::set-current-suggestion-by-class-and-index\n                                         prompt-buffer\n                                         (class-name (class-of source))\n                                         suggestion-index)\n                                        (prompter:toggle-mark prompt-buffer)\n                                        (prompt-render-suggestions prompt-buffer))))))\n                       (loop for (nil attribute)\n                               in (prompter:active-attributes\n                                   suggestion :source source)\n                             collect\n                             (:td\n                              :title attribute\n                              :onclick\n                              (ps:ps\n                                (cond\n                                  ((ps:chain window event ctrl-key)\n                                   (nyxt/ps:lisp-eval\n                                    (:title \"mark-this-suggestion\"\n                                     :buffer prompt-buffer)\n                                    (prompter::set-current-suggestion-by-class-and-index\n                                     prompt-buffer\n                                     (class-name (class-of source))\n                                     suggestion-index)\n                                    (prompter:toggle-mark prompt-buffer)\n                                    (prompt-render-suggestions prompt-buffer)))\n                                  (t\n                                   (nyxt/ps:lisp-eval\n                                    (:title \"return-this-suggestion\"\n                                     :buffer prompt-buffer)\n                                    (prompter::set-current-suggestion-by-class-and-index\n                                     prompt-buffer\n                                     (class-name (class-of source))\n                                     suggestion-index)\n                                    (prompter:run-action-on-return\n                                     (nyxt::current-prompt-buffer))))))\n                              attribute)))))))))\n\n(export 'prompt-render-suggestions)\n(defmethod prompt-render-suggestions ((prompt-buffer prompt-buffer))\n  \"Refresh the rendering of the suggestion list in PROMPT-BUFFER.\"\n  (let* ((sources (prompter:sources prompt-buffer))\n         (current-source-index (position (current-source prompt-buffer) sources))\n         (last-source-index (1- (length sources))))\n    (flet ((source->html (source)\n             (spinneret:with-html-string\n               (:div.source\n                (:div.source-name\n                 (:div\n                  (:nbutton\n                    :id \"next-source\"\n                    :text (:raw (gethash \"down.svg\" *static-data*))\n                    :title (format nil \"Next source (~a)\"\n                                   (binding-keys (sym:resolve-symbol\n                                                  :next-source :command)\n                                                 :modes (enabled-modes\n                                                         prompt-buffer)))\n                    :buffer prompt-buffer\n                    '(funcall (sym:resolve-symbol :next-source :command)))\n                  (:nbutton\n                    :id \"previous-source\"\n                    :text (:raw (gethash \"up.svg\" *static-data*))\n                    :title (format nil \"Previous source (~a)\"\n                                   (binding-keys (sym:resolve-symbol\n                                                  :previous-source :command)\n                                                 :modes (enabled-modes\n                                                         prompt-buffer)))\n                    :buffer prompt-buffer\n                    '(funcall (sym:resolve-symbol :previous-source :command)))\n                  (prompter:name source)\n                  (:span\n                   :class \"suggestion-and-mark-count\"\n                   ;; To hide the suggestion count for the source, subclass\n                   ;; `prompter:source' and handle the condition.  Note that\n                   ;; `suggestion-and-mark-count' relies on the global prompt\n                   ;; value `hide-suggestion-count-p'.\n                   (suggestion-and-mark-count\n                    prompt-buffer\n                    (prompter:suggestions source)\n                    (prompter:marks source)\n                    :enable-marks-p (prompter:enable-marks-p source)))\n                  (when (not (prompter:ready-p source)) \"(In progress...)\"))\n                 (:div\n                  (:nbutton\n                    :id \"toggle-attributes\"\n                    :text (:raw (gethash \"plus-minus.svg\" *static-data*))\n                    :title (format nil \"Toggle attributes display (~a)\"\n                                   (binding-keys\n                                    (sym:resolve-symbol\n                                     'toggle-attributes-display :command)\n                                    :modes (enabled-modes prompt-buffer)))\n                    :buffer prompt-buffer\n                    `(funcall (sym:resolve-symbol\n                               :toggle-attributes-display :command)\n                              :source ,source))))\n                (render-attributes source prompt-buffer)))))\n      (ps-eval :async t :buffer prompt-buffer\n        (setf (ps:@ (nyxt/ps:qs document \"#suggestions\") |innerHTML|)\n              (ps:lisp\n               (sera:string-join\n                (loop for i from current-source-index to last-source-index\n                      for source = (nth i sources)\n                      unless (null (prompter:suggestions source))\n                        collect (source->html source))\n                +newline+)))))\n    (render-prompt prompt-buffer)))\n\n(defun prompt-render-skeleton (prompt-buffer)\n  (html-write (spinneret:with-html-string\n                (:head (:nstyle (style prompt-buffer)))\n                (:body\n                 (:div\n                  :id \"root\"\n                  (:div\n                   :id \"prompt-area\"\n                   (:div :id \"prompt\" (prompter:prompt prompt-buffer))\n                   (:div :id \"prompt-extra\" :class \"arrow-right\" \"[?/?]\")\n                   (:div :id \"prompt-input\"\n                         (:input :type (if (invisible-input-p prompt-buffer)\n                                           \"password\"\n                                           \"text\")\n                                 :id \"input\"\n                                 :value (prompter:input prompt-buffer)))\n                   (:div :id \"prompt-modes\" :class \"arrow-left\" \"\")\n                   (:div :id \"close-button\" :class \"arrow-left\"\n                         (:nbutton\n                           :text \"×\"\n                           :title \"Close prompt\"\n                           :buffer prompt-buffer\n                           '(funcall (sym:resolve-symbol\n                                      :quit-prompt-buffer :command)))))\n                  (:div :id \"suggestions\"\n                        :style (if (invisible-input-p prompt-buffer)\n                                   \"visibility:hidden;\"\n                                   \"visibility:visible;\")))))\n              prompt-buffer))\n\n(defun prompt-render-focus (prompt-buffer)\n  (ps-eval :async t :buffer prompt-buffer\n    (let ((input (ps:chain (nyxt/ps:qs document \"#input\"))))\n      (ps:chain input (focus))\n      (ps:chain input (select)))))\n\n(defun update-prompt-input (prompt-buffer &optional input)\n  \"This blocks and updates the view.\nINPUT is an implementation detail, don't rely on it.\nIf you want to set the input, see `set-prompt-buffer-input'.\"\n  ;; TODO: This function is not thread-safe, add a lock?\n  (let ((input (or input\n                   (ps-eval :buffer prompt-buffer\n                     (ps:chain (nyxt/ps:qs document \"#input\") value)))))\n    (setf (prompter:input prompt-buffer) input)\n    ;; TODO: Stop loop when prompt-buffer is no longer current.\n    (labels ((maybe-update-view ()\n               (let ((next-source (when (find prompt-buffer\n                                              (active-prompt-buffers\n                                               (window prompt-buffer)))\n                                    (prompter:next-ready-p prompt-buffer))))\n                 (cond\n                   ;; Nothing to do:\n                   ((eq t next-source)\n                    ;; The renderer might have taken been too long to render the prompt\n                    ;; buffer and its HTML input, causing the latter to not be in sync with\n                    ;; what was send as input to the prompter sources.  Thus when we are done\n                    ;; watching, check if we are in sync; if not, try again.\n                    (let ((input (ps-eval :buffer prompt-buffer\n                                   (ps:chain (nyxt/ps:qs document \"#input\") value))))\n                      (unless (string= input (prompter:input prompt-buffer))\n                        (update-prompt-input prompt-buffer input)))\n                    t)\n                   ((null next-source) nil)\n                   (t ;; At least one source got updated.\n                    (prompt-render-suggestions prompt-buffer)\n                    (maybe-update-view))))))\n      (maybe-update-view))))\n\n(export-always 'set-prompt-buffer-input)\n(defun set-prompt-buffer-input\n    (input &optional (prompt-buffer (current-prompt-buffer)))\n  \"Set HTML INPUT in PROMPT-BUFFER.\nSee `update-prompt-input' to update the changes visually.\"\n  (ps-eval :async t :buffer prompt-buffer\n    (setf (ps:@ (nyxt/ps:qs document \"#input\") value)\n          (ps:lisp input)))\n  (update-prompt-input prompt-buffer input))\n\n(defun wait-on-prompt-buffer (prompt-buffer) ; TODO: Export?  Better name?\n  \"Block and return PROMPT-BUFFER results.\"\n  (when (prompt-buffer-p prompt-buffer)\n    (show-prompt-buffer prompt-buffer)\n    (calispel:fair-alt\n      ((calispel:? (prompter:result-channel prompt-buffer) results)\n       (hide-prompt-buffer prompt-buffer)\n       results)\n      ((calispel:? (prompter:interrupt-channel prompt-buffer))\n       (hide-prompt-buffer prompt-buffer)\n       (error 'prompt-buffer-canceled)))))\n\n(eval-always\n  (defvar %prompt-args\n    (delete-duplicates (append\n                        (mopu:direct-slot-names 'prompt-buffer)\n                        (mopu:direct-slot-names 'prompter:prompter)\n                        ;; `customize-instance' `:after' arguments:\n                        '(extra-modes)))))\n(export-always 'prompt)\n(eval-always\n  (defun prompt #.(append '(&rest args) `(&key ,@%prompt-args))\n    \"Open the prompt buffer, ready for user input.\nPROMPTER and PROMPT-BUFFER are plists of keyword arguments passed to the\nprompt-buffer constructor.\n\nExample use:\n\n\\(prompt :prompt \\\"Test prompt\\\"\n         :sources (make-instance 'prompter:source :name \\\"Test\\\"\n                                                  :constructor '(\\\"foo\\\" \\\"bar\\\")))\n\nSee the documentation of `prompt-buffer' to know more about the options.\"\n    (declare #.(cons 'ignorable %prompt-args))\n    (when-let ((prompt-text (getf args :prompt)))\n      (when (str:ends-with-p \":\" prompt-text)\n        (log:warn \"Prompt text ~s should not end with a ':'.\" prompt-text)\n        (setf (getf args :prompt) (string-right-trim\n                                   (uiop:strcat \":\" serapeum:whitespace)\n                                   prompt-text))))\n    (let ((prompt-object-channel (make-channel 1)))\n      (ffi-within-renderer-thread\n       (lambda ()\n         (let ((prompt-buffer\n                 (apply #'make-instance\n                        'prompt-buffer\n                        (append args\n                                (list :window (current-window)\n                                      :result-channel (make-channel)\n                                      :interrupt-channel (make-channel))))))\n           (calispel:! prompt-object-channel prompt-buffer))))\n      (let ((new-prompt (calispel:? prompt-object-channel)))\n        (wait-on-prompt-buffer new-prompt)))))\n\n(export-always 'prompt1)\n(eval-always\n  (defun prompt1 #.(append '(&rest args) `(&key ,@%prompt-args))\n    \"Return the first result of a prompt.\"\n    (declare #.(cons 'ignorable %prompt-args))\n    (first (apply #'prompt args))))\n\n(defmethod prompter:object-attributes\n    ((prompt-buffer prompt-buffer) (source prompter:source))\n  (declare (ignore source))\n  `((\"Prompt\" ,(prompter:prompt prompt-buffer))\n    (\"Input\" ,(prompter:input prompt-buffer))))\n"
  },
  {
    "path": "source/recent-buffers.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt)\n\n(defmethod deleted-buffers ((browser browser))\n  (remove-if-not #'dead-buffer-p\n                 (cl-containers:container->list (recent-buffers browser))))\n\n(defmethod last-deleted-buffer ((browser browser))\n  \"Return the last deleted buffer if exists, otherwise NIL.\"\n  (first (deleted-buffers browser)))\n\n(defmethod reopen-dead-buffer ((buffer modable-buffer))\n  (when (dead-buffer-p buffer)\n    (ffi-buffer-initialize-foreign-object buffer)\n    (add-to-recent-buffers buffer)\n    (buffer-set (id buffer) buffer)\n    (ffi-buffer-load buffer (url buffer))))\n\n(define-class recent-buffer-source (prompter:source)\n  ((prompter:name \"Deleted buffers\")\n   (prompter:enable-marks-p t)\n   (prompter:constructor\n    (deleted-buffers *browser*))\n   (prompter:actions-on-return\n    (list\n     (lambda-command reopen-dead-buffer-focus (buffer-list)\n       \"Reopen BUFFER and switch to it.\"\n       (mapc #'reopen-dead-buffer buffer-list)\n       (set-current-buffer\n        (or (first (prompter:marks (current-source)))\n            (current-suggestion-value (current-prompt-buffer)))))\n     (lambda-mapped-command reopen-dead-buffer)))\n   (prompter:filter-preprocessor #'prompter:filter-exact-matches)))\n\n(define-command reopen-buffer ()\n  \"Reopen queried deleted buffer(s).\"\n  (prompt :prompt \"Reopen buffer(s)\" :sources 'recent-buffer-source))\n\n(define-command reopen-last-buffer ()\n  \"Open a new buffer with the URL of the most recently deleted buffer.\"\n  (alex:if-let ((buffer (last-deleted-buffer *browser*)))\n    (progn (reopen-dead-buffer buffer)\n           (set-current-buffer buffer))\n    (echo \"There are no recently-deleted buffers.\")))\n"
  },
  {
    "path": "source/renderer/electron.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/renderer/electron\n  (:documentation \"Electron renderer.\"))\n(in-package :nyxt/renderer/electron)\n\n(define-class electron-renderer (renderer)\n  ((name \"Electron\"))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:export-predicate-name-p t)\n  (:metaclass user-class)\n  (:documentation \"Electron renderer class.\"))\n\n(setf nyxt::*renderer* (make-instance 'electron-renderer))\n(pushnew :nyxt-electron *features*)\n\n(defmethod install ((renderer electron-renderer))\n  (flet ((set-superclasses (renderer-class-sym+superclasses)\n           (closer-mop:ensure-finalized\n            (closer-mop:ensure-class (first renderer-class-sym+superclasses)\n                                     :direct-superclasses (rest renderer-class-sym+superclasses)\n                                     :metaclass 'interface-class))))\n    (mapc #'set-superclasses '((renderer-browser electron-browser)\n                               (renderer-scheme electron-scheme)\n                               (renderer-window electron-window)\n                               (renderer-buffer electron-buffer)\n                               (nyxt/mode/download:renderer-download electron-download)))))\n\n(defmethod uninstall ((renderer electron-renderer))\n  (flet ((remove-superclasses (renderer-class-sym)\n           (closer-mop:ensure-finalized\n            (closer-mop:ensure-class renderer-class-sym\n                                     :direct-superclasses '()\n                                     :metaclass 'interface-class))))\n    (mapc #'remove-superclasses '(renderer-browser\n                                  renderer-scheme\n                                  renderer-window\n                                  renderer-buffer))))\n\n\n(define-class electron-download ()\n  ((remote-object))\n  (:documentation \"Electron download class.\"))\n\n(defmethod update-status ((electron-download electron-download))\n  (setf (nyxt/mode/download:status electron-download)\n        (match (electron:state (remote-object electron-download))\n          (\"completed\" :finished)\n          (\"progressing\" :loading)\n          (\"cancelled\" :canceled)\n          (\"interrupted\" :failed))))\n\n(define-class electron-scheme (electron:protocol)\n  ()\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:metaclass user-class)\n  (:documentation \"Electron scheme class.\"))\n\n(defmethod initialize-instance :after ((scheme electron-scheme) &key)\n  ;; Set the scheme name from child (`scheme') to parent class\n  ;; (`electron-scheme').\n  (setf (slot-value scheme 'electron:scheme-name)\n        (name scheme)))\n\n(defmethod ffi-register-custom-scheme ((scheme electron-scheme))\n  (electron:handle-callback scheme (callback scheme)))\n\n(define-class electron-browser (electron:interface)\n  ((adblocking-enabled-p t))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:metaclass user-class)\n  (:documentation \"Electron browser class.\"))\n\n(defmethod ffi-initialize ((browser electron-browser) urls startup-timestamp)\n  (declare (ignore urls startup-timestamp))\n  (log:debug \"Initializing Electron Interface\")\n  (setf electron:*interface* (make-instance 'electron:interface))\n  ;; Schemes' privileges (security settings) need to be set before launch.\n  (setf (electron:protocols electron:*interface*)\n        (list (make-instance 'electron:protocol\n                             :scheme-name \"nyxt\"\n                             :privileges \"{}\")\n              (make-instance 'electron:protocol\n                             :scheme-name \"nyxt-resource\"\n                             :privileges \"{secure:true}\")\n              (make-instance 'electron:protocol\n                             :scheme-name \"lisp\"\n                             :privileges \"{supportFetchAPI:true,corsEnabled:true}\")\n              (make-instance 'electron:protocol\n                             :scheme-name \"view-source\"\n                             :privileges \"{}\")\n              (make-instance 'electron:protocol\n                             :scheme-name \"gopher\"\n                             :privileges \"{}\")\n              (make-instance 'electron:protocol\n                             :scheme-name \"gemini\"\n                             :privileges \"{}\")))\n  (setf (electron:launch-options electron:*interface*)\n        (cl-ppcre:split \"\\\\s+\"  (getf *options* :electron-opts)))\n  (electron:launch electron:*interface*)\n  (when (adblocking-enabled-p browser)\n    (let ((adblocker (make-instance 'electron:adblocker-electron)))\n      (electron:default-block adblocker)))\n  (maphash (lambda (scheme-name callbacks)\n             (ffi-register-custom-scheme (make-instance 'scheme\n                                                        :name scheme-name\n                                                        :callback (first callbacks)\n                                                        :error-callback (second callbacks))))\n           nyxt::*schemes*)\n  (let ((session (electron:default-session electron:*interface*)))\n    (electron:add-listener session :download-item-updated\n                           (lambda (session item)\n                             (declare (ignore session))\n                             (download-item-updated item))))\n  (call-next-method)\n  (unless nyxt::*run-from-repl-p*\n    (uiop:wait-process (electron:process electron:*interface*))\n    (uiop:quit (nyxt:exit-code browser) #+bsd nil)))\n\n#+sbcl (pushnew 'electron:terminate sb-ext:*exit-hooks*)\n\n(defun download-item-updated (download-item)\n  (let ((download (find download-item (downloads *browser*) :key #'remote-object)))\n    (if download\n        (progn\n          (setf (url download)\n                (electron:url (remote-object download))\n                (nyxt/mode/download:bytes-downloaded download)\n                (electron:received-bytes (remote-object download))\n                (nyxt/mode/download:completion-percentage download)\n                (electron:percent-complete (remote-object download))\n                (nyxt/mode/download:destination-path download)\n                (electron:save-path (remote-object download)))\n          (update-status download))\n        (progn\n          (let ((download (make-instance 'nyxt/mode/download:download\n                                         :remote-object download-item\n                                         :url (electron:url download-item))))\n            (push download (downloads *browser*))\n            (setf (nyxt/mode/download::cancel-function download)\n                  (lambda ()\n                    (electron:cancel (remote-object download)))))))))\n\n(defmethod ffi-kill-browser ((browser electron-browser))\n  (declare (ignore browser))\n  (electron:terminate))\n\n(define-class electron-buffer (electron:view)\n  ((electron:options\n    \"\"\n    :export t\n    :reader t\n    :writer nil\n    :type string\n    :documentation \"A string that specifies the buffer's behavior.\")\n   (modifier-plist\n    '(:shift \"shift\"\n      :control \"control\"\n      :alt \"meta\"\n      :meta \"super\")\n    :type list\n    :documentation \"A map between Electron's and Nyxt's terminology for modifier keys.\nNote that by changing the default value, modifier keys can be remapped.\")\n   (set-height\n    :documentation \"The height the buffer has been requested to be set to.\nIt does not represent the current height as reported by the renderer, or\nthe default height.\"))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:metaclass user-class)\n  (:documentation \"Electron buffer class.\"))\n\n(defmethod customize-instance :after ((buffer electron-buffer)\n                                      &key &allow-other-keys)\n  ;; Otherwise the HTML document won't be set via JS.\n  (when (member (type-of buffer) '(status-buffer message-buffer prompt-buffer))\n    (electron:load-url buffer \"about:blank\"))\n  (initialize-listeners buffer)\n  (initialize-window-open-handler buffer))\n\n(defmethod initialize-window-open-handler ((buffer electron-buffer))\n  ;; When following a link with target=\"_blank\" or through JS window.open,\n  ;; we don't want to let electron open a new window, instead we want to\n  ;; put it into a new buffer\n  (electron:override-window-open-handler\n   (electron:web-contents buffer)\n   (lambda (details)\n     (let ((url (assoc-value details :url)))\n       (make-buffer-focus :url (quri:uri url))))))\n\n(defmethod initialize-listeners ((buffer electron-buffer))\n  (electron:add-listener buffer :before-input-event\n                         (lambda (buffer event)\n                           (on-signal-key-press-event buffer event)))\n  (when (web-buffer-p buffer)\n    (electron:add-listener (electron:web-contents buffer) :did-start-loading\n                           (lambda (_) (declare (ignore _))\n                             (setf (nyxt::status buffer) :loading)\n                             (on-signal-load-started buffer (ffi-buffer-url buffer))))\n    (electron:add-listener (electron:web-contents buffer) :did-redirect-navigation\n                           (lambda (_) (declare (ignore _))\n                             (let ((url (ffi-buffer-url buffer)))\n                               (setf (slot-value buffer 'url) url)\n                               (on-signal-load-redirected buffer url))))\n    (electron:add-listener (electron:web-contents buffer) :did-finish-load\n                           (lambda (_) (declare (ignore _))\n                             (setf (nyxt::status buffer) :finished)\n                             (let ((url (ffi-buffer-url buffer))\n                                   (title (ffi-buffer-title buffer)))\n                               (setf (url buffer) url)\n                               (on-signal-load-finished buffer url title))))\n    (electron:add-listener (electron:web-contents buffer) :page-title-updated\n                           (lambda (_) (declare (ignore _))\n                             (on-signal-notify-title buffer (ffi-buffer-title buffer))))\n    (unless (member (type-of buffer) '(status-buffer message-buffer prompt-buffer))\n      (electron:add-listener\n       (electron:web-contents buffer) :context-menu\n       (lambda (object params)\n         (declare (ignore object))\n         (print params)\n         (format nil  \"[{label: 'Backward', click: () => {~a.goBack()}},\n                        {label: 'Forward', click: () => {~a.goForward()}},\n                        {label: 'Reload', click: () => {~a.reload()}},\n                       ]\"\n                 (electron:remote-symbol (electron:web-contents buffer))\n                 (electron:remote-symbol (electron:web-contents buffer))\n                 (electron:remote-symbol (electron:web-contents buffer))))))))\n\n(defmethod ffi-buffer-initialize-foreign-object ((buffer electron-buffer))\n  (electron::message\n   buffer\n   (format nil \"~a = new WebContentsView(~a)\"\n           (electron:remote-symbol buffer) (electron:options buffer)))\n  (initialize-listeners buffer)\n  buffer)\n\n(defmethod ffi-buffer-delete ((buffer electron-buffer))\n  (electron:remove-view (current-window) buffer :kill-view-p t))\n\n(defmethod ffi-buffer-url ((buffer electron-buffer))\n  (quri:uri (electron:get-url buffer)))\n\n(defmethod ffi-buffer-title ((buffer electron-buffer))\n  (electron:get-title buffer))\n\n(defmethod ffi-buffer-load ((buffer electron-buffer) url)\n  (electron:load-url buffer url))\n\n(defmethod ffi-buffer-reload ((buffer electron-buffer))\n  (electron:reload (electron:web-contents buffer))\n  buffer)\n\n(defmethod ffi-buffer-zoom-ratio ((buffer electron-buffer))\n  (electron:get-zoom-factor (electron:web-contents buffer)))\n\n(defmethod (setf ffi-buffer-zoom-ratio) (value (buffer electron-buffer))\n  (if (and (floatp value) (plusp value))\n      (electron:set-zoom-factor (electron:web-contents buffer) value)\n      (echo-warning \"Zoom ratio must be a positive floating point number.\")))\n\n(defmethod ffi-buffer-evaluate-javascript ((buffer electron-buffer) javascript\n                                           &optional world-name)\n  ;; TODO world-name is used in user-script mode.\n  (declare (ignore world-name))\n  (electron:execute-javascript-synchronous (electron:web-contents buffer) javascript))\n\n(defmethod ffi-buffer-evaluate-javascript-async ((buffer electron-buffer) javascript\n                                                 &optional world-name)\n  (declare (ignore world-name))\n  (electron:execute-javascript (electron:web-contents buffer) javascript))\n\n(defmethod ffi-inspector-show ((buffer electron-buffer))\n  (electron:open-dev-tools buffer))\n\n(defmethod ffi-focused-p ((buffer electron-buffer))\n  (electron:is-focused buffer))\n\n;; ffi-buffer-load-alternate-html handles bogus URLs (https://bogusfoo.com/).\n;; (defmethod ffi-buffer-load-alternate-html ((buffer electron-buffer) html-content content-url url))\n\n(defmethod ffi-buffer-copy ((buffer electron-buffer) &optional (text nil text-provided-p))\n  (if text-provided-p\n      (trivial-clipboard:text text)\n      (progn\n        (electron:copy (electron:web-contents buffer))\n        (trivial-clipboard:content))))\n\n(defmethod ffi-buffer-paste ((buffer electron-buffer) &optional (text nil text-provided-p))\n  (if text-provided-p\n      (electron:insert-text (electron:web-contents buffer) text)\n      (electron:paste (electron:web-contents buffer))))\n\n(defmethod ffi-buffer-cut ((buffer electron-buffer))\n  (electron:cut (electron:web-contents buffer))\n  (trivial-clipboard:text))\n\n(defmethod ffi-buffer-select-all ((buffer electron-buffer))\n  (electron:select-all (electron:web-contents buffer)))\n\n(defmethod ffi-buffer-undo ((buffer electron-buffer))\n  ;; There is no way to check if an undo operation is possible. There exists a\n  ;; `context-menu' event that when invoked can check whether `canUndo' exists\n  ;; within the `editFLags' of the renderer, but we cannot manually trigger this\n  ;; event.\n  (electron:undo (electron:web-contents buffer)))\n\n(defmethod ffi-buffer-redo ((buffer electron-buffer))\n  ;; There is no way to check if a redo operation is possible. There exists a\n  ;; `context-menu' event that when invoked can check whether `canRedo' exists\n  ;; within the `editFLags' of the renderer, but we cannot manually trigger this\n  ;; event.\n  (electron:redo (electron:web-contents buffer)))\n\n;; TODO\n;; (defmethod ffi-buffer-cookie-policy ((buffer electron-buffer)))\n\n(defun update-active-buffer-bounds (window delta)\n  \"Recalculate the bounds of the active window to compensate for changes in the\nheight of the status/prompt/message buffer.\"\n  (let ((window-bounds (electron:get-bounds window)))\n    (electron:set-bounds (active-buffer window)\n                         :x 0\n                         :y 0\n                         :width (assoc-value window-bounds :width)\n                         :height (- (assoc-value window-bounds :height)\n                                    (+ delta\n                                       (ffi-height (status-buffer window))\n                                       (ffi-height (message-buffer window)))))))\n\n(defmethod ffi-height ((buffer electron-buffer))\n  (assoc-value (electron:get-bounds buffer) :height))\n\n(defmethod (setf ffi-height) ((height integer) (buffer electron-buffer))\n  (setf (set-height buffer) height)\n  (let ((bounds (electron:get-bounds buffer)))\n    (electron:set-bounds buffer\n                         :x (assoc-value bounds :x)\n                         :y (assoc-value bounds :y)\n                         :width (assoc-value bounds :width)\n                         :height height))\n  (update-active-buffer-bounds (window buffer) 0))\n\n(defmethod ffi-focus-buffer ((buffer electron-buffer))\n  (electron:focus buffer)\n  buffer)\n\n(defmethod (setf ffi-height) ((height integer) (prompt-buffer prompt-buffer))\n  (with-slots (window) prompt-buffer\n    (update-active-buffer-bounds window height)\n    (electron:add-bounded-view window\n                               prompt-buffer\n                               :window-bounds-alist-var bounds\n                               :x 0\n                               :y (- (assoc-value bounds :height)\n                                     (+ height\n                                        (set-height (status-buffer window))\n                                        (set-height (message-buffer window))))\n                               :width (assoc-value bounds :width)\n                               :height height)))\n\n(defmethod ffi-width ((buffer electron-buffer))\n  (assoc-value (electron:get-bounds buffer) :height))\n\n(defmethod (setf ffi-width) (width (buffer electron-buffer))\n  (let ((bounds (electron:get-bounds buffer)))\n    (electron:set-bounds buffer\n                         :x (assoc-value bounds :x)\n                         :y (assoc-value bounds :y)\n                         :width width\n                         :height (assoc-value bounds :height))))\n\n(defmethod ffi-buffer-sound-enabled-p ((buffer electron-buffer))\n  (not (electron:muted-p (electron:web-contents buffer))))\n(defmethod (setf ffi-buffer-sound-enabled-p) (value (buffer electron-buffer))\n  (electron:set-audio-muted (electron:web-contents buffer) (not value)))\n\n(defmethod ffi-buffer-download ((buffer electron-buffer) url)\n  (electron:download-url (electron:web-contents buffer) url))\n\n;; TODO Support user-script mode.\n;; (defmethod ffi-buffer-add-user-style ((buffer electron-buffer) style))\n;; (defmethod ffi-buffer-remove-user-style ((buffer electron-buffer) style))\n;; (defmethod ffi-buffer-add-user-script ((buffer electron-buffer) script))\n;; (defmethod ffi-buffer-remove-user-script ((buffer electron-buffer) script))\n\n\n;; TODO: Implement image / javascript disabling by deleting/recreating the view\n;; with the appropriate WebPreferences. It is not possible to enable/disable\n;; WebPreferences in real-time due to limitations in Electron.\n(defmethod ffi-buffer-auto-load-image-enabled-p ((buffer electron-buffer))\n  (echo \"Disabling images not supported by Electron back-end.\")\n  (error \"Disabling images not supported by Electron back-end.\"))\n(defmethod (setf ffi-buffer-auto-load-image-enabled-p) (value (buffer electron-buffer))\n  (declare (ignore buffer value))\n  (echo \"Disabling images not supported by Electron back-end.\")\n  (error \"Disabling images not supported by Electron back-end.\"))\n\n(defmethod ffi-buffer-javascript-markup-enabled-p ((buffer electron-buffer))\n  (echo \"Disabling JavaScript not supported by Electron back-end.\")\n  (error \"Disabling JavaScript not supported by Electron back-end.\"))\n(defmethod (setf ffi-buffer-javascript-markup-enabled-p) (value (buffer electron-buffer))\n  (declare (ignore buffer value))\n  (echo \"Disabling JavaScript not supported by Electron back-end.\")\n  (error \"Disabling JavaScript not supported by Electron back-end.\"))\n\n(defmethod ffi-buffer-webgl-enabled-p ((buffer electron-buffer))\n  (echo \"Disabling WebGL not supported by Electron back-end.\")\n  (error \"Disabling WebGL not supported by Electron back-end.\"))\n(defmethod (setf ffi-buffer-webgl-enabled-p) (value (buffer electron-buffer))\n  (declare (ignore buffer value))\n  (echo \"Disabling WebGL not supported by Electron back-end.\")\n  (error \"Disabling WebGL not supported by Electron back-end.\"))\n\n(defmethod ffi-buffer-proxy ((buffer electron-buffer))\n  (echo \"Setting Proxy per buffer not supported by the Electron back-end.\")\n  (error \"Setting Proxy per buffer not supported by the Electron back-end.\"))\n(defmethod (setf ffi-buffer-proxy) (value (buffer electron-buffer))\n  (declare (ignore buffer value))\n  (echo \"Setting Proxy per buffer not supported by the Electron back-end.\")\n  (error \"Setting Proxy per buffer not supported by the Electron back-end.\"))\n\n(define-class electron-window (electron:window)\n  ((electron:options\n    \"{autoHideMenuBar: true,\n      width: 1600,\n      height: 1200}\")\n   (current-view))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:metaclass user-class)\n  (:documentation \"Electron window class.\"))\n\n(defmethod initialize-instance :after ((window electron-window) &key)\n  (electron:remove-menu window)\n  (let ((message-buffer (message-buffer window))\n        (status-buffer (status-buffer window)))\n    (setf (set-height message-buffer) (height message-buffer))\n    (setf (set-height status-buffer) (height status-buffer))\n    (electron:add-bounded-view window\n                               message-buffer\n                               :window-bounds-alist-var bounds\n                               :x 0\n                               :y (- (assoc-value bounds :height)\n                                     (set-height message-buffer))\n                               :width (assoc-value bounds :width)\n                               :height (set-height message-buffer))\n    (electron:add-bounded-view window\n                               status-buffer\n                               :window-bounds-alist-var bounds\n                               :x 0\n                               :y (- (assoc-value bounds :height)\n                                     (+ (set-height status-buffer)\n                                        (set-height message-buffer)))\n                               :width (assoc-value bounds :width)\n                               :height (set-height status-buffer))\n    ;; TODO: Fix buffer deletion. We CANNOT hook on close to remove the view\n    ;; from the window because it is too late. Ergo, we must manually delete the\n    ;; currently viewed buffer without trying to mutate it via JS.\n    (electron:add-listener\n     window :close\n     (lambda (_) (declare (ignore _))\n       (when (current-view window)\n         (nyxt::buffer-delete (id (current-view window))))))))\n\n(defmethod ffi-window-delete ((window electron-window))\n  (when (current-view window)\n    (electron:remove-view window (current-view window) :kill-view-p nil))\n  (electron:kill window))\n\n(defmethod ffi-window-title ((window electron-window))\n  (electron:get-title window))\n(defmethod (setf ffi-window-title) (title (window electron-window))\n  (electron:set-title window title))\n\n(defmethod ffi-window-set-buffer ((window electron-window)\n                                  (buffer electron-buffer)\n                                  &key (focus t))\n  (when (current-view window)\n    (electron:remove-view window (current-view window) :kill-view-p nil))\n  (electron:add-bounded-view window\n                             buffer\n                             :window-bounds-alist-var bounds\n                             :x 0\n                             :y 0\n                             :width (assoc-value bounds :width)\n                             :height (- (assoc-value bounds :height)\n                                        (+ (ffi-height (status-buffer window))\n                                           (ffi-height (message-buffer window))\n                                           (if-let ((prompt (current-prompt-buffer)))\n                                             (ffi-height prompt)\n                                             0))))\n  (when focus (electron:focus buffer))\n  (setf (current-view window) buffer)\n  buffer)\n\n(defmethod ffi-height ((window electron-window))\n  (assoc-value (electron:get-bounds window) :height))\n\n(defmethod ffi-width ((window electron-window))\n  (assoc-value (electron:get-bounds window) :width))\n\n(defmethod ffi-window-fullscreen ((window electron-window) &key &allow-other-keys)\n  (electron:fullscreen window))\n\n(defmethod ffi-window-unfullscreen ((window electron-window) &key &allow-other-keys)\n  (electron:unfullscreen window))\n\n(defmethod ffi-window-maximize ((window electron-window) &key &allow-other-keys)\n  (electron:maximize window))\n\n(defmethod ffi-window-unmaximize ((window electron-window) &key &allow-other-keys)\n  (electron:unmaximize window))\n\n(defmethod ffi-window-active ((browser electron-browser))\n  (or (find-if #'electron:is-focused (window-list))\n      (call-next-method)))\n\n;; Input handling\n\n(defmethod input-modifier-translator ((buffer electron-buffer) input-event-modifier-state)\n  \"Return a list of modifier keys understood by `keymaps:make-key'.\"\n  (when-let ((state input-event-modifier-state))\n    (mapcar (lambda (modifier) (getf (modifier-plist buffer) modifier)) state)))\n\n(defun translate-code-string (code-string)\n  \"Return string representation of a keyval.\nReturn nil when key must be discarded, e.g. for modifiers.\"\n  (match code-string\n    ;; Compatibility layer between GDK keycode names and those of Browsers.\n    ;; https://gitlab.gnome.org/GNOME/gtk/-/blob/main/gdk/gdkkeysyms.h\n    ;; https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values\n    ((or \"ControlLeft\" \"ControlRight\"\n         \"ShiftLeft\" \"ShiftRight\"\n         \"MetaLeft\" \"MetaRight\"\n         \"AltLeft\" \"AltRight\")\n     nil)\n    (\"Minus\" \"hyphen\")\n    (\"Equal\" \"=\")\n    (\"Space\" \"space\")\n    (\"Enter\" \"return\")\n    (\"Escape\" \"escape\")\n    (\"Tab\" \"tab\")\n    (\"Comma\" \",\")\n    (\"Period\" \".\")\n    (\"Slash\" \"/\")\n    (\"Semicolon\" \";\")\n    (\"Quote\" \"'\")\n    (\"BracketLeft\" \"[\")\n    (\"BracketRight\" \"]\")\n    (\"Backslash\" \"\\\\\")\n    (\"Backquote\" \"`\")\n    (\"Backspace\" \"backspace\")\n    (\"ArrowUp\" \"up\")\n    (\"ArrowDown\" \"down\")\n    (\"ArrowRight\" \"right\")\n    (\"ArrowLeft\" \"left\")\n    (\"PageUp\" \"pageup\")\n    (\"PageDown\" \"pagedown\")\n    (\"Home\" \"home\")\n    (\"End\" \"end\")\n    (\"F1\" \"f1\")\n    (\"F2\" \"f2\")\n    (\"F3\" \"f3\")\n    (\"F4\" \"f4\")\n    (\"F5\" \"f5\")\n    (\"F6\" \"f6\")\n    (\"F7\" \"f7\")\n    (\"F8\" \"f8\")\n    (\"F9\" \"f9\")\n    (\"F10\" \"f10\")\n    (\"F11\" \"f11\")\n    (\"F12\" \"f12\")\n    ((simple-string #\\K #\\e #\\y key-value) (string-downcase (string key-value)))\n    ((simple-string #\\D #\\i #\\g #\\i #\\t digit-value) (string digit-value))\n    (_ code-string)))\n\n(defmethod on-signal-key-press-event ((sender electron-buffer) event)\n  (when (string= \"keyDown\" (assoc-value event :type))\n    (let ((modifiers (delete nil (list (when (assoc-value event :shift) :shift)\n                                       (when (assoc-value event :control) :control)\n                                       (when (assoc-value event :alt) :alt)\n                                       (when (assoc-value event :meta) :meta))))\n          (key-string (translate-code-string (assoc-value event :code))))\n      (flet ((key () (keymaps:make-key :value key-string\n                                       :modifiers (input-modifier-translator sender modifiers)\n                                       :status :pressed)))\n        (when key-string\n          (alex:appendf (key-stack sender)\n                        (list (key)))\n          (run-thread \"on-signal-key-press\" (on-signal-key-press sender (key)))\n          (dispatch-input-event event sender))))))\n\n;; TODO on-signal-* methods\n"
  },
  {
    "path": "source/renderer/gi-gtk.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n;;;; This file produces two warnings due to redefinitions of\n;;;; renderer-thread-p and ffi-initialize.\n;;;;\n;;;; This is necessary to modify the behavior of renderer-gtk to use\n;;;; gobject to launch/manage the main GTK thread. When/if this\n;;;; renderer will fully utilize gobject introspection (by taking the\n;;;; changes made on the respectively named branch, and reapplying\n;;;; them), then these errors will disappear, as there will be no\n;;;; reliance on renderer-gtk (and thus, no redefinition).\n\n(nyxt:define-package :nyxt/renderer/gi-gtk\n    (:documentation \"GTK renderer leveraging GObject Introspection.\nFor now it is also partly based on `nyxt/renderer/gtk'.\"))\n(in-package :nyxt/renderer/gi-gtk)\n\n(define-class gi-gtk-renderer (nyxt/renderer/gtk:gtk-renderer)\n  ((name \"GI-GTK\"))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:documentation \"WebKit (GI) renderer class.\"))\n\n(setf nyxt::*renderer* (make-instance 'gi-gtk-renderer))\n(pushnew :nyxt-gi-gtk *features*)\n\n(define-class gi-gtk-browser (nyxt/renderer/gtk:gtk-browser)\n  ()\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:documentation \"WebKit (GI) browser class.\"))\n\n(defvar renderer-thread-name \"Nyxt renderer thread\")\n\n(defmethod nyxt/renderer/gtk::renderer-thread-p\n    ((renderer gi-gtk-renderer)\n     &optional (thread (bt:current-thread)))\n  (string= (bt:thread-name thread)\n           renderer-thread-name))\n\n(defmethod ffi-initialize ((browser gi-gtk-browser) urls startup-timestamp)\n  (declare (ignore urls startup-timestamp))\n  (log:debug \"Initializing GI-GTK Interface\")\n  (if nyxt/renderer/gtk::gtk-running-p\n      (nyxt/renderer/gtk::within-gtk-thread (call-next-method))\n      (flet ((main-func ()\n               (with-protect (\"Error on GI-GTK thread: ~a\" :condition)\n                 (glib:g-set-prgname \"nyxt\")\n                 #+GTK-3-4\n                 (gdk:gdk-set-program-class \"Nyxt\")\n                 (gir:invoke ((gir:ffi \"Gtk\" \"3.0\") 'main)))))\n        (setf nyxt/renderer/gtk::gtk-running-p t)\n        (call-next-method)\n        (let ((main-thread (bt:make-thread\n                            #'main-func :name renderer-thread-name)))\n          (unless nyxt::*run-from-repl-p*\n            (bt:join-thread main-thread)\n            (uiop:quit (nyxt:exit-code browser) #+bsd nil))))))\n\n(nyxt/renderer/gtk:define-ffi-method ffi-kill-browser ((browser gi-gtk-browser))\n  (unless nyxt::*run-from-repl-p*\n    (gir:invoke ((gir:ffi \"Gtk\" \"3.0\") 'main-quit))))\n\n(defmethod install ((renderer gi-gtk-renderer))\n  (call-next-method)\n  (closer-mop:ensure-finalized\n   (closer-mop:ensure-class 'renderer-browser\n                            :direct-superclasses '(gi-gtk-browser)\n                            :metaclass 'interface-class)))\n"
  },
  {
    "path": "source/renderer/gtk.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/renderer/gtk\n    (:documentation \"GTK renderer using direct CFFI bindings.\"))\n(in-package :nyxt/renderer/gtk)\n\n(define-class gtk-renderer (renderer)\n  ((name \"GTK\"))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:export-predicate-name-p t)\n  (:documentation \"WebKit renderer class.\"))\n\n(setf nyxt::*renderer* (make-instance 'gtk-renderer))\n(pushnew :nyxt-gtk *features*)\n\n(defmethod renderer-thread-p ((renderer gtk-renderer) &optional (thread (bt:current-thread)))\n  (string= \"cl-cffi-gtk main thread\" (bt:thread-name thread)))\n\n(defmethod install ((renderer gtk-renderer))\n  (flet ((set-superclasses (renderer-class-sym+superclasses)\n           (closer-mop:ensure-finalized\n            (closer-mop:ensure-class (first renderer-class-sym+superclasses)\n                                     :direct-superclasses (rest renderer-class-sym+superclasses)\n                                     :metaclass 'interface-class))))\n    (mapc #'set-superclasses '((renderer-browser gtk-browser)\n                               (renderer-window gtk-window)\n                               (renderer-buffer gtk-buffer)\n                               (nyxt/mode/download:renderer-download gtk-download)\n                               (renderer-request-data gtk-request-data)\n                               (renderer-scheme gtk-scheme)\n                               (nyxt/mode/user-script:renderer-user-style gtk-user-style)\n                               (nyxt/mode/user-script:renderer-user-script gtk-user-script)))))\n\n(defmethod uninstall ((renderer gtk-renderer))\n  (flet ((remove-superclasses (renderer-class-sym)\n           (closer-mop:ensure-finalized\n            (closer-mop:ensure-class renderer-class-sym\n                                     :direct-superclasses '()\n                                     :metaclass 'interface-class))))\n    (mapc #'remove-superclasses '(renderer-browser\n                                  renderer-window\n                                  renderer-buffer\n                                  nyxt/mode/download:renderer-download\n                                  renderer-request-data\n                                  renderer-scheme\n                                  nyxt/mode/user-script:renderer-user-style\n                                  nyxt/mode/user-script:renderer-user-script))))\n\n(define-class gtk-browser ()\n  ((web-contexts\n    (make-hash-table :test 'equal)\n    :export nil\n    :documentation \"A table mapping strings to `webkit-web-context' objects.\"))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:metaclass user-class)\n  (:documentation \"WebKit browser class.\"))\n\n(defmethod get-web-context ((browser gtk-browser) name)\n  (alexandria:ensure-gethash name\n                             (web-contexts browser)\n                             (make-web-context)))\n\n(defmethod browser-schemes append ((browser gtk-browser))\n  '(\"webkit\" \"webkit-pdfjs-viewer\"))\n\n(define-class gtk-window ()\n  ((gtk-object\n    :export nil)\n   (handler-ids\n    :documentation \"Store all GObject signal handler IDs so that we can\ndisconnect the signal handler when the object is finalized.\")\n   (root-box-layout)\n   (horizontal-box-layout)\n   (main-buffer-container)\n   (prompt-buffer-container)\n   (prompt-buffer-view\n    :documentation \"A web view shared by all prompt buffers of this window.\nThis is done so that the UI is computed efficiently.\")\n   (status-container)\n   (message-container)\n   (key-string-buffer))\n  (:export-class-name-p t)\n  (:export-accessor-names-p nil)\n  (:documentation \"WebKit window class.\"))\n\n(define-class gtk-buffer ()\n  ((gtk-object)\n   (modifier-plist\n    '(:control-mask \"control\"\n      :mod1-mask \"meta\"\n      :mod5-mask nil\n      :shift-mask \"shift\"\n      :super-mask \"super\"\n      :hyper-mask \"hyper\"\n      :meta-mask nil\n      :lock-mask nil)\n    :type list\n    :documentation \"A map between GTK's and Nyxt's terminology for modifier keys.\nNote that by changing the default value, modifier keys can be remapped.\")\n   (handler-ids\n    :export nil\n    :documentation \"Store all GObject signal handler IDs so that we can\ndisconnect the signal handler when the object is finalized.\")\n   (gtk-proxy-url (quri:uri \"\"))\n   (proxy-ignored-hosts '())\n   (handle-permission-requests-p\n    nil\n    :documentation \"Whether permission requests are handled.\nWhen non-nil, they are handled by `process-permission-request'.  Otherwise, all\nrequests are denied.\"))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:metaclass user-class)\n  (:documentation \"WebKit buffer class.\"))\n\n(defmethod input-modifier-translator ((buffer gtk-buffer) input-event-modifier-state)\n  \"Return a list of modifier keys understood by `keymaps:make-key'.\"\n  (when-let ((state input-event-modifier-state))\n    (delete nil\n            (mapcar (lambda (modifier) (getf (modifier-plist buffer) modifier)) state))))\n\n(defclass webkit-website-data-manager (webkit:webkit-website-data-manager) ()\n  (:metaclass gobject:gobject-class))\n\n(defvar gtk-running-p nil\n  \"Non-nil if the GTK main loop is running.\nSee `ffi-initialize' and `ffi-kill-browser'.\n\nRestarting GTK within the same Lisp image breaks WebKitGTK.\nAs a workaround, we never leave the GTK main loop when running from a REPL.\n\nSee https://github.com/atlas-engineer/nyxt/issues/740\")\n\n(defmacro within-gtk-thread (&body body)\n  \"Protected `gtk:within-gtk-thread'.\"\n  `(gtk:within-gtk-thread\n     (with-protect (\"Error on GTK thread: ~a\" :condition)\n       ,@body)))\n\n(defmethod ffi-within-renderer-thread (thunk)\n  (within-gtk-thread (funcall thunk)))\n\n(defun %within-renderer-thread (thunk)\n  \"If the current thread is the renderer thread, execute THUNK with `funcall'.\nOtherwise run the THUNK on the renderer thread by passing it a channel and wait on the channel's result.\"\n  (if (renderer-thread-p nyxt::*renderer*)\n      (funcall thunk)\n      (let ((channel (nyxt::make-channel 1)))\n        (within-gtk-thread\n          (funcall thunk channel))\n        (calispel:? channel))))\n\n(defun %within-renderer-thread-async (thunk)\n  \"Same as `%within-renderer-thread' but THUNK is not blocking and does\nnot return.\"\n  (if (renderer-thread-p nyxt::*renderer*)\n      (funcall thunk)\n      (within-gtk-thread\n        (funcall thunk))))\n\n(export-always 'define-ffi-method)\n(defmacro define-ffi-method (name args &body body)\n  \"Define an FFI method to run in the renderer thread.\n\nReturn the value or forward the condition retrieved from the renderer thread,\nusing a channel if the current thread is not the renderer one.\n\nIt's a `defmethod' wrapper. If you don't need the body of the method to execute in\nthe renderer thread, use `defmethod' instead.\"\n  (multiple-value-bind (forms declares docstring)\n      (alex:parse-body body :documentation t)\n    `(defmethod ,name ,args\n       ,@(sera:unsplice docstring)\n       ,@declares\n       (if (renderer-thread-p nyxt::*renderer*)\n           (progn ,@forms)\n           (let ((channel (nyxt::make-channel 1))\n                 (error-channel (nyxt::make-channel 1)))\n             (within-gtk-thread\n               ;; TODO: Abstract this into `with-protect-from-thread'?\n               (if (or nyxt::*run-from-repl-p* nyxt::*restart-on-error*)\n                   (let ((current-condition nil))\n                     (restart-case\n                         (handler-bind ((condition (lambda (c) (setf current-condition c))))\n                           (calispel:! channel (progn ,@forms)))\n                       (abort-ffi-method ()\n                         :report \"Pass condition to calling thread.\"\n                         (calispel:! error-channel current-condition))))\n                   (handler-case (calispel:! channel (progn ,@forms))\n                     (condition (c)\n                       (calispel:! error-channel c)))))\n             (calispel:fair-alt\n               ((calispel:? channel result)\n                result)\n               ((calispel:? error-channel condition)\n                (with-protect (\"Error in FFI method: ~a\" :condition)\n                  (error condition)))))))))\n\n(defmethod ffi-initialize ((browser gtk-browser) urls startup-timestamp)\n  \"gtk:within-main-loop handles all the GTK initialization.\"\n  (declare (ignore urls startup-timestamp))\n  (log:debug \"Initializing GTK Interface\")\n  (if gtk-running-p\n      (within-gtk-thread (call-next-method))\n      (progn\n        (setf gtk-running-p t)\n        (glib:g-set-prgname \"nyxt\")\n        (gdk:gdk-set-program-class \"Nyxt\")\n        (gtk:within-main-loop\n          (with-protect (\"Error on GTK thread: ~a\" :condition)\n            (call-next-method)))\n        (unless nyxt::*run-from-repl-p*\n          (gtk:join-gtk-main)\n          (uiop:quit (nyxt:exit-code browser) #+bsd nil)))))\n\n(define-ffi-method ffi-kill-browser ((browser gtk-browser))\n  (gtk:leave-gtk-main))\n\n(define-class gtk-extensions-directory (nyxt-file)\n  ((files:name \"gtk-extensions\")\n   (files:base-path (uiop:merge-pathnames* \"nyxt/\" nasdf:*libdir*)))\n  (:export-class-name-p t)\n  (:documentation \"Directory to load WebKitWebExtensions from.\"))\n\n(define-class gtk-download ()\n  ((gtk-object)\n   (handler-ids\n    :export nil\n    :documentation \"See `gtk-buffer' slot of the same name.\"))\n  (:documentation \"WebKit download class.\"))\n\n(defun make-web-context ()\n  (let* ((context (make-instance 'webkit:webkit-web-context\n                                 :website-data-manager\n                                 (make-instance 'webkit-website-data-manager)))\n         (cookie-manager (webkit:webkit-web-context-get-cookie-manager context))\n         (gtk-extensions-path (files:expand (make-instance 'gtk-extensions-directory))))\n    (webkit:webkit-web-context-set-spell-checking-enabled context t)\n    ;; Need to set the initial language list.\n    (let ((pointer (cffi:foreign-alloc :string\n                                       :initial-contents (list (or (uiop:getenv \"LANG\")\n                                                                   (uiop:getenv \"LANGUAGE\")\n                                                                   (uiop:getenv \"LC_CTYPE\")\n                                                                   \"en_US\"))\n                                       :null-terminated-p t)))\n      (webkit:webkit-web-context-set-spell-checking-languages context pointer)\n      (cffi:foreign-free pointer))\n    (when (and (not (nfiles:nil-pathname-p gtk-extensions-path))\n               ;; Either the directory exists.\n               (or (uiop:directory-exists-p gtk-extensions-path)\n                   ;; Or try to create it.\n                   (handler-case\n                       (nth-value 1 (ensure-directories-exist gtk-extensions-path))\n                     (file-error ()))))\n      (log:info \"GTK extensions directory: ~s\" gtk-extensions-path)\n      (gobject:g-signal-connect\n       context \"initialize-web-extensions\"\n       (lambda (context)\n         (with-protect (\"Error in \\\"initialize-web-extensions\\\" signal thread: ~a\" :condition)\n           ;; The following calls\n           ;; `webkit:webkit-web-context-add-path-to-sandbox' for us, so no need\n           ;; to add `gtk-extensions-path' to the sandbox manually.\n           (webkit:webkit-web-context-set-web-extensions-directory\n            context\n            (uiop:native-namestring gtk-extensions-path))))))\n    (gobject:g-signal-connect\n     context \"download-started\"\n     (lambda (context download)\n       (declare (ignore context))\n       (with-protect (\"Error in \\\"download-started\\\" signal thread: ~a\" :condition)\n         (wrap-download download))))\n    (maphash (lambda (scheme-name callbacks)\n               (ffi-register-custom-scheme (make-instance 'scheme\n                                                          :name scheme-name\n                                                          :web-context context\n                                                          :callback (first callbacks)\n                                                          :error-callback (second callbacks))))\n             nyxt::*schemes*)\n    (webkit:webkit-cookie-manager-set-persistent-storage\n     cookie-manager\n     (uiop:native-namestring (files:expand (make-instance 'nyxt-data-directory\n                                                          :base-path \"cookies\")))\n     :webkit-cookie-persistent-storage-text)\n    (setf (ffi-buffer-cookie-policy cookie-manager) (default-cookie-policy *browser*))\n    context))\n\n(define-class gtk-request-data ()\n  ((gtk-request\n    :type (maybe webkit:webkit-uri-request))\n   (gtk-response\n    :type (maybe webkit:webkit-uri-response))\n   (gtk-resource\n    :type (maybe webkit:webkit-web-resource)))\n  (:export-class-name-p t)\n  ;; We export these accessors because it can be useful to inspect the guts of a\n  ;; request, plus the upstream WebKit API is stable enough.\n  (:export-accessor-names-p t)\n  (:metaclass user-class)\n  (:documentation \"Related to WebKit's request objects.\"))\n\n(defun make-decide-policy-handler (buffer)\n  (lambda (web-view response-policy-decision policy-decision-type-response)\n    (declare (ignore web-view))\n    ;; Even if errors are caught with `with-protect', we must ignore the policy\n    ;; decision on error, lest we load a web page in an internal buffer for\n    ;; instance.\n    (g:g-object-ref (g:pointer response-policy-decision))\n    (run-thread \"asynchronous decide-policy processing\"\n      (handler-bind ((error (lambda (c)\n                              (echo-warning \"decide policy error: ~a\" c)\n                              ;; TODO: Don't automatically call the restart when from the REPL?\n                              ;; (unless nyxt::*run-from-repl-p*\n                              ;;   (invoke-restart 'ignore-policy-decision))\n                              (invoke-restart 'ignore-policy-decision))))\n        (restart-case (on-signal-decide-policy buffer response-policy-decision policy-decision-type-response)\n          (ignore-policy-decision ()\n            (webkit:webkit-policy-decision-ignore response-policy-decision)))))\n    t))\n\n(defmacro connect-signal-function (object signal fn)\n  \"Connect SIGNAL to OBJECT with a function FN.\nOBJECT must have the `gtk-object' and `handler-ids' slots.\nSee also `connect-signal'.\"\n  `(let ((handler-id (gobject:g-signal-connect\n                      (gtk-object ,object) ,signal ,fn)))\n     (push handler-id (handler-ids ,object))))\n\n(defmacro connect-signal (object signal new-thread-p (&rest args) &body body)\n  \"Connect SIGNAL to OBJECT with a lambda that takes ARGS.\nOBJECT must have the `gtk-object' and `handler-ids' slots. If\n`new-thread-p' is non-nil, then a new thread will be launched for the\nresponse.  The BODY is wrapped with `with-protect'.\"\n  (multiple-value-bind (forms declares documentation)\n      (alex:parse-body body :documentation t)\n    `(let ((handler-id (gobject:g-signal-connect\n                        (gtk-object ,object) ,signal\n                        (lambda (,@args)\n                          ,@(sera:unsplice documentation)\n                          ,@declares\n                          ,(if new-thread-p\n                               `(run-thread \"renderer signal handler\"\n                                    ,@forms)\n                               `(with-protect (\"Error in signal on renderer thread: ~a\" :condition)\n                                  ,@forms))))))\n       (push handler-id (handler-ids ,object)))))\n\n(defmethod customize-instance :after ((window gtk-window) &key)\n  (%within-renderer-thread-async\n   (lambda ()\n     (with-slots (gtk-object root-box-layout horizontal-box-layout\n                  main-buffer-container\n                  prompt-buffer-container prompt-buffer-view\n                  status-buffer status-container\n                  message-buffer message-container\n                  key-string-buffer)\n         window\n       (unless gtk-object\n         (setf gtk-object (make-instance 'gtk:gtk-window\n                                         :type :toplevel\n                                         :default-width 1024\n                                         :default-height 768))\n         (setf root-box-layout (make-instance 'gtk:gtk-box\n                                              :orientation :vertical))\n         (setf horizontal-box-layout (make-instance 'gtk:gtk-box\n                                                    :orientation :horizontal))\n         (setf main-buffer-container (make-instance 'gtk:gtk-box\n                                                    :orientation :vertical))\n         (setf prompt-buffer-container (make-instance 'gtk:gtk-box\n                                                      :orientation :vertical))\n         (setf message-container (make-instance 'gtk:gtk-box\n                                                :orientation :vertical))\n         (setf status-container (make-instance 'gtk:gtk-box\n                                               :orientation :vertical))\n         (setf key-string-buffer (make-instance 'gtk:gtk-entry))\n         (gtk:gtk-box-pack-start horizontal-box-layout\n                                 main-buffer-container\n                                 :expand t :fill t)\n         (gtk:gtk-box-pack-start root-box-layout\n                                 horizontal-box-layout\n                                 :expand t :fill t)\n         (gtk:gtk-box-pack-end root-box-layout\n                               message-container\n                               :expand nil)\n         (gtk:gtk-box-pack-start root-box-layout\n                                 message-container\n                                 :expand nil)\n         (gtk:gtk-box-pack-start message-container\n                                 (gtk-object message-buffer)\n                                 :expand t)\n         (setf (gtk:gtk-widget-height-request message-container)\n               (height message-buffer))\n         (gtk:gtk-box-pack-end root-box-layout\n                               status-container\n                               :expand nil)\n         (gtk:gtk-box-pack-start status-container\n                                 (gtk-object status-buffer)\n                                 :expand t)\n         (setf (gtk:gtk-widget-height-request status-container)\n               (height status-buffer))\n         (setf prompt-buffer-view (make-instance 'webkit:webkit-web-view))\n         (gtk:gtk-box-pack-end root-box-layout\n                               prompt-buffer-container\n                               :expand nil)\n         (gtk:gtk-box-pack-start prompt-buffer-container\n                                 prompt-buffer-view\n                                 :expand t)\n         (gtk:gtk-container-add gtk-object root-box-layout)\n         (connect-signal window \"destroy\" nil (widget)\n           (declare (ignore widget))\n           (on-signal-destroy window))\n         (connect-signal window \"window-state-event\" nil (widget event)\n           (declare (ignore widget))\n           (let ((fullscreen-p)\n                 (maximized-p))\n             (dolist (state (gdk:gdk-event-window-state-new-window-state event))\n               (case state\n                 (:fullscreen\n                  (setq fullscreen-p t)\n                  (ffi-window-fullscreen window :user-event-p nil))\n                 (:maximized\n                  (setq maximized-p t)\n                  (ffi-window-maximize window :user-event-p nil))))\n             (unless fullscreen-p (ffi-window-unfullscreen window :user-event-p nil))\n             (unless maximized-p (ffi-window-unmaximize window :user-event-p nil)))\n           nil))\n       (unless *headless-p* (gtk:gtk-widget-show-all gtk-object))))))\n\n(defmethod update-instance-for-redefined-class :after ((window window) added deleted plist &key)\n  (declare (ignore added deleted plist))\n  (customize-instance window))\n\n(define-ffi-method on-signal-destroy ((window gtk-window))\n  ;; Then remove buffer from window container to avoid corruption of buffer.\n  (gtk:gtk-container-remove (main-buffer-container window)\n                            (gtk-object (active-buffer window))))\n\n(define-ffi-method ffi-window-delete ((window gtk-window))\n  (gtk:gtk-widget-destroy (gtk-object window)))\n\n(define-ffi-method ffi-window-fullscreen ((window gtk-window) &key &allow-other-keys)\n  (gtk:gtk-window-fullscreen (gtk-object window)))\n\n(define-ffi-method ffi-window-unfullscreen ((window gtk-window) &key &allow-other-keys)\n  (gtk:gtk-window-unfullscreen (gtk-object window)))\n\n(define-ffi-method ffi-window-maximize ((window gtk-window) &key &allow-other-keys)\n  (gtk:gtk-window-maximize (gtk-object window)))\n\n(define-ffi-method ffi-window-unmaximize ((window gtk-window) &key &allow-other-keys)\n  (gtk:gtk-window-unmaximize (gtk-object window)))\n\n(defun derive-key-string (keyval character)\n  \"Return string representation of a keyval.\nReturn nil when key must be discarded, e.g. for modifiers.\"\n  (let ((result\n          (match keyval\n            ((or \"Alt_L\" \"Super_L\" \"Control_L\" \"Shift_L\"\n                 \"Alt_R\" \"Super_R\" \"Control_R\" \"Shift_R\"\n                 \"ISO_Level3_Shift\" \"Arabic_switch\")\n             ;; Discard modifiers (they usually have a null character).\n             nil)\n            ((guard s (str:contains? \"KP_\" s))\n             (str:replace-all \"KP_\" \"keypad\" s))\n            ;; With a modifier, \"-\" does not print, so we me must translate it\n            ;; to \"hyphen\" just like in `printable-p'.\n            (\"minus\" \"hyphen\")\n            ;; shift-tab:\n            (\"ISO_Left_Tab\" \"tab\")\n            ;; In most cases, return character and not keyval for punctuation.\n            ;; For instance, C-[ is not printable but the keyval is \"bracketleft\".\n            ;; ASCII control characters like Escape, Delete or BackSpace have a\n            ;; non-printable character (usually beneath #\\space), so we use the\n            ;; keyval in this case.\n            ;; Even if space in printable, C-space is not so we return the\n            ;; keyval in this case.\n            (_ (if (or (char<= character #\\space)\n                       (char= character #\\Del))\n                   keyval\n                   (string character))))))\n    (if (< 1 (length result))\n        (str:replace-all \"_\" \"\" (string-downcase result))\n        result)))\n\n(defmethod printable-p ((window gtk-window) event)\n  \"Return the printable value of EVENT.\"\n  ;; Generate the result of the current keypress into the dummy\n  ;; key-string-buffer (a GtkEntry that's never shown on screen) so that we\n  ;; can collect the printed representation of composed keypress, such as dead\n  ;; keys.\n  (gtk:gtk-entry-im-context-filter-keypress (key-string-buffer window) event)\n  (when (<= 1 (gtk:gtk-entry-text-length (key-string-buffer window)))\n    (prog1\n        (match (gtk:gtk-entry-text (key-string-buffer window))\n          ;; Special cases: these characters are not supported as is for keyspecs.\n          (\" \" \"space\")\n          (\"-\" \"hyphen\")\n          (character character))\n      (setf (gtk:gtk-entry-text (key-string-buffer window)) \"\"))))\n\n(define-ffi-method on-signal-key-press-event ((sender gtk-buffer) event)\n  (let* ((keycode (gdk:gdk-event-key-hardware-keycode event))\n         (keyval (gdk:gdk-event-key-keyval event))\n         (keyval-name (gdk:gdk-keyval-name keyval))\n         (character (gdk:gdk-keyval-to-unicode keyval))\n         (printable-value (printable-p (current-window) event))\n         (key-string (or printable-value\n                         (derive-key-string keyval-name character)))\n         (modifiers (input-modifier-translator sender (gdk:gdk-event-key-state event))))\n    (log:debug sender key-string keycode character keyval-name modifiers)\n    ;; Do not forward modifier-only presses to the renderer.\n    (if key-string\n        (flet ((key ()\n                 (keymaps:make-key :code keycode\n                                   :value (or (ignore-errors (keymaps:unshift key-string))\n                                              key-string)\n                                   :modifiers modifiers\n                                   :status :pressed)))\n          (alex:appendf (key-stack sender)\n                        (list (key)))\n          (run-thread \"on-signal-key-press\"\n            (on-signal-key-press sender (key)))\n          (dispatch-input-event event sender))\n        t)))\n\n(define-ffi-method on-signal-button-press-event ((sender gtk-buffer) event)\n  (let ((key-string (format nil \"button~s\" (gdk:gdk-event-button-button event)))\n        (modifiers (input-modifier-translator sender (gdk:gdk-event-button-state event)))\n        (buffer (or (current-prompt-buffer) sender)))\n    ;; Handle mode-specific logic here (e.g. VI switch to insertion) to not\n    ;; interfere with regular keybinding logic.\n    (flet ((key ()\n             ;; It's a function so we instantiate multiple objects and avoid sharing.\n             (keymaps:make-key :value key-string\n                               :modifiers modifiers\n                               :status :pressed)))\n      (run-thread \"on-signal-button-press\"\n        (on-signal-button-press buffer (key)))\n      (when key-string\n        (alex:appendf (key-stack buffer)\n                      (list (key)))\n        (dispatch-input-event event sender)))))\n\n(define-ffi-method on-signal-scroll-event ((sender gtk-buffer) event)\n  (let* ((button (match (gdk:gdk-event-scroll-direction event)\n                   (:up 4)\n                   (:down 5)\n                   (:left 6)\n                   (:right 7)\n                   (:smooth (cond ((>= 0 (gdk:gdk-event-scroll-delta-y event)) 4)\n                                  ((< 0 (gdk:gdk-event-scroll-delta-y event)) 5)\n                                  ((>= 0 (gdk:gdk-event-scroll-delta-x event)) 6)\n                                  ((< 0 (gdk:gdk-event-scroll-delta-x event)) 7)))))\n         (key-string (format nil \"button~s\" button))\n         (modifiers (input-modifier-translator sender (gdk:gdk-event-scroll-state event))))\n    (when key-string\n      (alex:appendf (key-stack sender)\n                    (list (keymaps:make-key :value key-string\n                                            :modifiers modifiers\n                                            :status :pressed)))\n      (dispatch-input-event event sender))))\n\n(define-class gtk-scheme ()\n  ((web-context\n    nil\n    :writer nil\n    :reader t\n    :documentation \"See `webkit-web-context'.\")\n   (local-p\n    nil\n    :writer nil\n    :reader t\n    :documentation \"Whether pages of other URI schemes cannot access URIs of\nthis scheme.\")\n   (no-access-p\n    nil\n    :writer nil\n    :reader t\n    :documentation \"Whether pages of this URI scheme cannot access other URI schemes.\")\n   (secure-p\n    nil\n    :writer nil\n    :reader t\n    :documentation \"Whether mixed content warnings aren't generated for this\nscheme when included by an HTTPS page.\n\nSee https://developer.mozilla.org/en-US/docs/Web/Security/Mixed_content.\")\n   (cors-enabled-p\n    nil\n    :writer nil\n    :reader t\n    :documentation \"Whether CORS requests are allowed.\")\n   (display-isolated-p\n    nil\n    :writer nil\n    :reader t\n    :documentation \"Whether pages cannot display URIs unless they are from the\nsame scheme.\nFor example, pages in another origin cannot create iframes or hyperlinks to URIs\nwith this scheme.\")\n   (empty-document-p\n    nil\n    :writer nil\n    :reader t\n    :documentation \"Whether pages are allowed to be loaded synchronously.\"))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:documentation \"Related to WebKit's custom schemes.\"))\n\n(defmethod manager ((scheme gtk-scheme))\n  (webkit:webkit-web-context-get-security-manager (web-context scheme)))\n\n(defmethod (setf local-p) (value (scheme gtk-scheme))\n  (when value\n    (webkit:webkit-security-manager-register-uri-scheme-as-local (manager scheme)\n                                                                 (name scheme)))\n  (setf (slot-value scheme 'local-p) value))\n\n(defmethod (setf no-access-p) (value (scheme gtk-scheme))\n  (when value\n    (webkit:webkit-security-manager-register-uri-scheme-as-no-access (manager scheme)\n                                                                     (name scheme)))\n  (setf (slot-value scheme 'no-access-p) value))\n\n(defmethod (setf secure-p) (value (scheme gtk-scheme))\n  (when value\n    (webkit:webkit-security-manager-register-uri-scheme-as-secure (manager scheme)\n                                                                  (name scheme)))\n  (setf (slot-value scheme 'secure-p) value))\n\n(defmethod (setf cors-enabled-p) (value (scheme gtk-scheme))\n  (when value\n    (webkit:webkit-security-manager-register-uri-scheme-as-cors-enabled (manager scheme)\n                                                                        (name scheme)))\n  (setf (slot-value scheme 'cors-enabled-p) value))\n\n(defmethod (setf display-isolated-p) (value (scheme gtk-scheme))\n  (when value\n    (webkit:webkit-security-manager-register-uri-scheme-as-display-isolated (manager scheme)\n                                                                            (name scheme)))\n  (setf (slot-value scheme 'display-isolated-p) value))\n\n(defmethod (setf empty-document-p) (value (scheme gtk-scheme))\n  (when value\n    (webkit:webkit-security-manager-register-uri-scheme-as-empty-document (manager scheme)\n                                                                          (name scheme)))\n  (setf (slot-value scheme 'empty-document-p) value))\n\n(defmethod initialize-instance :after ((scheme gtk-scheme) &key)\n  ;; NOTE: No security settings for the nyxt scheme since:\n  ;; - :local-p makes it inaccessible from other schemes.\n  ;; - :display-isolated-p does not allow embedding a nyxt scheme page inside a\n  ;;   page of the same scheme.\n  ;; - :secure-p and :cors-enabled-p are too permissive for a scheme that allows\n  ;;   evaluating Lisp code.\n  ;; Therefore, no settings provide the best configuration so that:\n  ;; - <iframe> embedding and exploitation are impossible.\n  ;; - Redirection—both as window.location.(href|assign|replace) and\n  ;;   HTTP status code 301—works.\n  ;; - nyxt scheme pages are linkable from pages of other schemes.\n  (match (name scheme)\n    (\"nyxt-resource\" (setf (secure-p scheme) t))\n    (\"lisp\" (setf (cors-enabled-p scheme) t))\n    (\"view-source\" (setf (no-access-p scheme) t))\n    (_ t)))\n\n;; From https://github.com/umpirsky/language-list/tree/master/data directory\n;; listing $(ls /data) and processed with:\n;; (defun iso-languages (languages-file-string)\n;;   (remove-if (lambda (lang)\n;;                (or (uiop:emptyp lang)\n;;                    (/= 1 (count #\\_ lang))\n;;                    (notevery #'upper-case-p (subseq lang 3))))\n;;              (uiop:split-string languages-file-string :separator '(#\\Space #\\Tab #\\Newline))))\n(defvar *spell-check-languages*\n  (list \"be_BY\" \"en_LC\" \"en_VG\" \"ff_MR\" \"fr_SC\" \"ln_CF\" \"nl_BE\" \"rm_CH\" \"uk_UA\"\n        \"af_NA\" \"el_CY\" \"en_LR\" \"en_VI\" \"ff_SN\" \"fr_SN\" \"ii_CN\" \"ln_CG\" \"nl_BQ\"\n        \"af_ZA\" \"bg_BG\" \"el_GR\" \"en_LS\" \"en_VU\" \"fr_SY\" \"nl_CW\" \"rn_BI\" \"ur_IN\"\n        \"en_MG\" \"en_WS\" \"fi_FI\" \"fr_TD\" \"is_IS\" \"lo_LA\" \"nl_NL\" \"ur_PK\" \"ak_GH\"\n        \"en_AG\" \"en_MH\" \"en_ZA\" \"fr_TG\" \"nl_SR\" \"ro_MD\" \"en_AI\" \"en_MO\" \"en_ZM\"\n        \"fo_FO\" \"fr_TN\" \"it_CH\" \"lt_LT\" \"nl_SX\" \"ro_RO\" \"uz_AF\" \"am_ET\" \"en_AS\"\n        \"en_MP\" \"en_ZW\" \"fr_VU\" \"it_IT\" \"bn_BD\" \"en_AU\" \"en_MS\" \"fr_BE\" \"fr_WF\"\n        \"it_SM\" \"lu_CD\" \"nn_NO\" \"ru_BY\" \"ar_AE\" \"bn_IN\" \"en_BB\" \"en_MT\" \"fr_BF\"\n        \"fr_YT\" \"ru_KG\" \"sr_ME\" \"ar_BH\" \"en_BE\" \"en_MU\" \"es_AR\" \"fr_BI\" \"ja_JP\"\n        \"lv_LV\" \"no_NO\" \"ru_KZ\" \"sr_RS\" \"ar_DJ\" \"bo_CN\" \"en_BM\" \"en_MW\" \"es_BO\"\n        \"fr_BJ\" \"fy_NL\" \"ru_MD\" \"sr_XK\" \"ar_DZ\" \"bo_IN\" \"en_BS\" \"en_MY\" \"es_CL\"\n        \"fr_BL\" \"ka_GE\" \"mg_MG\" \"om_ET\" \"ru_RU\" \"ar_EG\" \"en_BW\" \"en_NA\" \"es_CO\"\n        \"fr_CA\" \"ga_IE\" \"om_KE\" \"ru_UA\" \"sv_AX\" \"uz_UZ\" \"ar_EH\" \"br_FR\" \"en_BZ\"\n        \"en_NF\" \"es_CR\" \"fr_CD\" \"ki_KE\" \"mk_MK\" \"sv_FI\" \"ar_ER\" \"en_CA\" \"en_NG\"\n        \"es_CU\" \"fr_CF\" \"gd_GB\" \"or_IN\" \"rw_RW\" \"sv_SE\" \"vi_VN\" \"ar_IL\" \"bs_BA\"\n        \"en_CC\" \"en_NR\" \"es_DO\" \"fr_CG\" \"ml_IN\" \"ar_IQ\" \"en_CK\" \"en_NU\" \"es_EA\"\n        \"fr_CH\" \"gl_ES\" \"os_GE\" \"se_FI\" \"sw_CD\" \"ar_JO\" \"en_CM\" \"en_NZ\" \"es_EC\"\n        \"fr_CI\" \"kk_KZ\" \"os_RU\" \"se_NO\" \"sw_KE\" \"yo_BJ\" \"ar_KM\" \"en_CX\" \"en_PG\"\n        \"es_ES\" \"fr_CM\" \"gu_IN\" \"se_SE\" \"sw_TZ\" \"yo_NG\" \"ar_KW\" \"en_DG\" \"en_PH\"\n        \"es_GQ\" \"fr_DJ\" \"kl_GL\" \"mn_MN\" \"sw_UG\" \"ar_LB\" \"en_DM\" \"en_PK\" \"es_GT\"\n        \"fr_DZ\" \"gv_IM\" \"sg_CF\" \"zh_CN\" \"ar_LY\" \"ca_AD\" \"en_ER\" \"en_PN\" \"es_HN\"\n        \"fr_FR\" \"km_KH\" \"mr_IN\" \"ta_IN\" \"zh_HK\" \"ar_MA\" \"ca_ES\" \"en_FJ\" \"en_PR\"\n        \"es_IC\" \"fr_GA\" \"ha_GH\" \"sh_BA\" \"ta_LK\" \"ar_MR\" \"ca_FR\" \"en_FK\" \"en_PW\"\n        \"es_MX\" \"fr_GF\" \"kn_IN\" \"ms_BN\" \"pa_IN\" \"ta_MY\" \"ar_OM\" \"ca_IT\" \"en_FM\"\n        \"en_RW\" \"es_NI\" \"fr_GN\" \"pa_PK\" \"si_LK\" \"ta_SG\" \"ar_PS\" \"en_GB\" \"en_SB\"\n        \"es_PA\" \"fr_GP\" \"ko_KP\" \"ar_QA\" \"cs_CZ\" \"en_GD\" \"en_SC\" \"es_PE\" \"fr_GQ\"\n        \"ko_KR\" \"pl_PL\" \"sk_SK\" \"te_IN\" \"ar_SA\" \"en_GG\" \"en_SD\" \"es_PH\" \"fr_HT\"\n        \"ha_NE\" \"ar_SD\" \"cy_GB\" \"en_GH\" \"en_SG\" \"es_PR\" \"fr_KM\" \"ha_NG\" \"ms_MY\"\n        \"ps_AF\" \"sl_SI\" \"th_TH\" \"ar_SO\" \"en_GI\" \"en_SH\" \"es_PY\" \"fr_LU\" \"ms_SG\"\n        \"ar_SS\" \"da_DK\" \"en_GM\" \"en_SL\" \"es_SV\" \"fr_MA\" \"he_IL\" \"ks_IN\" \"pt_AO\"\n        \"sn_ZW\" \"ti_ER\" \"ar_SY\" \"da_GL\" \"en_GU\" \"en_SS\" \"es_US\" \"fr_MC\" \"mt_MT\"\n        \"pt_BR\" \"ti_ET\" \"zh_MO\" \"ar_TD\" \"en_GY\" \"en_SX\" \"es_UY\" \"fr_MF\" \"hi_IN\"\n        \"kw_GB\" \"pt_CV\" \"so_DJ\" \"zh_SG\" \"ar_TN\" \"de_AT\" \"en_HK\" \"en_SZ\" \"es_VE\"\n        \"fr_MG\" \"my_MM\" \"pt_GW\" \"so_ET\" \"tl_PH\" \"zh_TW\" \"ar_YE\" \"de_BE\" \"en_IE\"\n        \"en_TC\" \"fr_ML\" \"hr_BA\" \"pt_MO\" \"so_KE\" \"de_CH\" \"en_IM\" \"en_TK\" \"et_EE\"\n        \"fr_MQ\" \"hr_HR\" \"nb_NO\" \"pt_MZ\" \"so_SO\" \"to_TO\" \"zu_ZA\" \"as_IN\" \"de_DE\"\n        \"en_IN\" \"en_TO\" \"fr_MR\" \"ky_KG\" \"nb_SJ\" \"pt_PT\" \"de_LI\" \"en_IO\" \"en_TT\"\n        \"eu_ES\" \"fr_MU\" \"hu_HU\" \"pt_ST\" \"sq_AL\" \"tr_CY\" \"az_AZ\" \"de_LU\" \"en_JE\"\n        \"en_TV\" \"fr_NC\" \"lb_LU\" \"nd_ZW\" \"pt_TL\" \"sq_MK\" \"tr_TR\" \"en_JM\" \"en_TZ\"\n        \"fa_AF\" \"fr_NE\" \"hy_AM\" \"sq_XK\" \"dz_BT\" \"en_KE\" \"en_UG\" \"fa_IR\" \"fr_PF\"\n        \"lg_UG\" \"ne_IN\" \"qu_BO\" \"en_KI\" \"en_UM\" \"fr_PM\" \"id_ID\" \"ne_NP\" \"qu_EC\"\n        \"sr_BA\" \"ee_GH\" \"en_KN\" \"en_US\" \"ff_CM\" \"fr_RE\" \"ln_AO\" \"qu_PE\" \"ug_CN\"\n        \"ee_TG\" \"en_KY\" \"en_VC\" \"ff_GN\" \"fr_RW\" \"ig_NG\" \"ln_CD\" \"nl_AW\")\n  \"The list of languages available for spell checking in `set-spell-check-languages'.\")\n\n(define-command-global set-spell-check-languages\n    (&key (buffer (current-buffer))\n     (languages (prompt :prompt \"Languages to spell check\"\n                        :sources (make-instance 'prompter:source\n                                                :name \"Language codes\"\n                                                :enable-marks-p t\n                                                :constructor *spell-check-languages*))))\n  (let ((pointer (cffi:foreign-alloc :string\n                                     :initial-contents languages\n                                     :null-terminated-p t)))\n    (webkit:webkit-web-context-set-spell-checking-languages\n     (webkit:webkit-web-view-web-context (gtk-object buffer))\n     pointer)\n    (cffi:foreign-free pointer)))\n\n(defmethod ffi-register-custom-scheme ((scheme gtk-scheme))\n  ;; FIXME If a define-internal-scheme is updated at runtime, it is not honored.\n  (webkit:webkit-web-context-register-uri-scheme-callback\n   (web-context scheme)\n   (name scheme)\n   (lambda (request)\n     (funcall* (callback scheme)\n               (webkit:webkit-uri-scheme-request-get-uri request)))\n   (or (error-callback scheme)\n       (lambda (c) (echo-warning \"Error while routing ~s resource: ~a\" scheme c)))))\n\n(defmethod customize-instance :after ((buffer gtk-buffer) &key &allow-other-keys)\n  (ffi-buffer-initialize-foreign-object buffer))\n\n(define-ffi-method ffi-buffer-url ((buffer gtk-buffer))\n  (quri:uri (webkit:webkit-web-view-uri (gtk-object buffer))))\n\n(define-ffi-method ffi-buffer-title ((buffer gtk-buffer))\n  (or (webkit:webkit-web-view-title (gtk-object buffer)) \"\"))\n\n(define-ffi-method on-signal-load-failed-with-tls-errors ((buffer gtk-buffer) certificate url)\n  \"Return nil to propagate further (i.e. raise load-failed signal), T otherwise.\"\n  (let ((context (webkit:webkit-web-view-web-context (gtk-object buffer)))\n        (host (quri:uri-host url)))\n    (if (and (certificate-exceptions buffer)\n             (member host (certificate-exceptions buffer) :test #'string=))\n        (progn\n          (webkit:webkit-web-context-allow-tls-certificate-for-host\n           context\n           (gobject:pointer certificate)\n           host)\n          (ffi-buffer-load buffer url)\n          t)\n        (progn\n          (nyxt::tls-help buffer url)\n          t))))\n\n(define-ffi-method on-signal-decide-policy ((buffer gtk-buffer) response-policy-decision policy-decision-type-response)\n  (let ((is-known-type t) (event-type :other) (modifiers ())\n        is-new-window navigation-action navigation-type\n        mouse-button url request mime-type method request-headers response-headers\n        file-name toplevel-p response)\n    (match policy-decision-type-response\n      (:webkit-policy-decision-type-navigation-action\n       (setf navigation-type\n             (webkit:webkit-navigation-policy-decision-navigation-type response-policy-decision)))\n      (:webkit-policy-decision-type-new-window-action\n       (setf navigation-type\n             (webkit:webkit-navigation-policy-decision-navigation-type response-policy-decision))\n       (setf is-new-window t))\n      (:webkit-policy-decision-type-response\n       (setf request\n             (webkit:webkit-response-policy-decision-request response-policy-decision))\n       (setf is-known-type\n             (webkit:webkit-response-policy-decision-is-mime-type-supported\n              response-policy-decision))\n       (setf response\n             (webkit:webkit-response-policy-decision-response response-policy-decision))\n       (setf mime-type\n             (webkit:webkit-uri-response-mime-type response))\n       (setf method\n             (webkit:webkit-uri-request-get-http-method request))\n       (setf file-name\n             (webkit:webkit-uri-response-suggested-filename response))))\n    ;; Set Event-Type\n    (setf event-type\n          (match navigation-type\n            (:webkit-navigation-type-link-clicked :link-click)\n            (:webkit-navigation-type-form-submitted :form-submission)\n            (:webkit-navigation-type-back-forward :backward-or-forward)\n            (:webkit-navigation-type-reload :reload)\n            (:webkit-navigation-type-form-resubmitted :form-resubmission)\n            (_ :other)))\n    ;; Get Navigation Parameters from WebKitNavigationAction object\n    (when navigation-type\n      (setf navigation-action\n            (webkit:webkit-navigation-policy-decision-get-navigation-action\n             response-policy-decision))\n      (setf request\n            (webkit:webkit-navigation-action-get-request navigation-action))\n      (setf mouse-button\n            (format nil \"button~d\"\n                    (webkit:webkit-navigation-action-get-mouse-button navigation-action)))\n      (setf modifiers\n            (input-modifier-translator buffer\n                                 (webkit:webkit-navigation-action-get-modifiers navigation-action))))\n    (setf url (quri:uri (webkit:webkit-uri-request-uri request)))\n    (setf request-headers\n          (let ((headers (webkit:webkit-uri-request-get-http-headers request)))\n            (unless (cffi:null-pointer-p headers)\n              (webkit:soup-message-headers-get-headers headers))))\n    (setf response-headers\n          (when response\n            (let ((headers (webkit:webkit-uri-response-get-http-headers response)))\n              (unless (cffi:null-pointer-p headers)\n                (webkit:soup-message-headers-get-headers headers)))))\n    (setf toplevel-p\n          (quri:uri= url\n                     (quri:uri (webkit:webkit-web-view-uri (gtk-object buffer)))))\n    (let* ((request-data\n            (hooks:run-hook\n             (request-resource-hook buffer)\n             (sera:lret ((data (make-instance\n                                'request-data\n                                :buffer buffer\n                                :url (quri:copy-uri url)\n                                :keys (unless (uiop:emptyp mouse-button)\n                                        (list (keymaps:make-key :value mouse-button\n                                                                :modifiers modifiers)))\n                                :event-type event-type\n                                :new-window-p is-new-window\n                                :http-method method\n                                :request-headers request-headers\n                                :response-headers response-headers\n                                :toplevel-p toplevel-p\n                                :mime-type mime-type\n                                :known-type-p is-known-type\n                                :file-name file-name)))\n                        (setf (gtk-request data) request\n                              (gtk-response data) response))))\n           (keymap (when request-data\n                     (nyxt::get-keymap (buffer request-data)\n                                       (request-resource-keyscheme-map (buffer request-data)))))\n           (bound-function (when request-data\n                             (the (or symbol keymaps:keymap null)\n                                  (keymaps:lookup-key (keys request-data) keymap)))))\n      (cond\n       ((not (typep request-data 'request-data))\n        (log:debug \"Don't forward to ~s's renderer (non request data).\"\n                   buffer)\n        (webkit:webkit-policy-decision-ignore response-policy-decision))\n       ;; FIXME: Do we ever use it? Do we actually need it?\n       (bound-function\n        (log:debug \"Resource request key sequence ~a\" (keyspecs-with-keycode (keys request-data)))\n        (funcall bound-function :url url :buffer buffer)\n        (webkit:webkit-policy-decision-ignore response-policy-decision))\n       ((new-window-p request-data)\n        (log:debug \"Load URL in new buffer: ~a\" (render-url (url request-data)))\n        (nyxt::open-urls (list (url request-data)))\n        (webkit:webkit-policy-decision-ignore response-policy-decision))\n       ((null (valid-scheme-p (quri:uri-scheme (url request-data))))\n        (log:warn \"Unsupported URI scheme: ~s.\" (quri:uri-scheme (url request-data))))\n       ((not (known-type-p request-data))\n        (log:debug \"Initiate download of ~s.\" (render-url (url request-data)))\n        (webkit:webkit-policy-decision-download response-policy-decision))\n       ((quri:uri= url (url request-data))\n        (log:debug \"Forward to ~s's renderer (unchanged URL).\"\n                   buffer)\n        (webkit:webkit-policy-decision-use response-policy-decision))\n       ((and (toplevel-p request-data)\n             (not (quri:uri= (quri:uri (webkit:webkit-uri-request-uri request))\n                             (url request-data))))\n        ;; Low-level URL string, we must not render punycode so use\n        ;; `quri:render-uri'.\n        ;; See https://datatracker.ietf.org/doc/html/rfc3492.\n        (setf (webkit:webkit-uri-request-uri request) (quri:render-uri (url request-data)))\n        (log:debug \"Don't forward to ~s's renderer (resource request replaced with ~s).\"\n                   buffer\n                   (render-url (url request-data)))\n        ;; Warning: We must ignore the policy decision _before_ we\n        ;; start the new load request, or else WebKit will be\n        ;; confused about which URL to load.\n        (webkit:webkit-policy-decision-ignore response-policy-decision)\n        (webkit:webkit-web-view-load-request (gtk-object buffer) request))\n       (t\n        (log:info \"Cannot redirect to ~a in an iframe, forwarding to the original URL (~a).\"\n                  (render-url (url request-data))\n                  (webkit:webkit-uri-request-uri request))\n        (webkit:webkit-policy-decision-use response-policy-decision))))))\n\n;; See https://webkitgtk.org/reference/webkit2gtk/stable/WebKitWebView.html#WebKitLoadEvent\n(defmethod on-signal-load-changed ((buffer gtk-buffer) load-event)\n  ;; `url' can be nil if buffer didn't have any URL associated\n  ;; to the web view, e.g. the start page, or if the load failed.\n  (when (web-buffer-p buffer)\n    (let* ((url (ignore-errors\n                 (quri:uri (webkit:webkit-web-view-uri (gtk-object buffer)))))\n           (url (if (url-empty-p url)\n                    (url buffer)\n                    url)))\n      (cond ((eq load-event :webkit-load-started)\n             (setf (nyxt::status buffer) :loading)\n             (on-signal-load-started buffer url)\n             (unless (internal-url-p url)\n               (echo \"Loading ~s.\" (render-url url))))\n            ((eq load-event :webkit-load-redirected)\n             (setf (url buffer) url)\n             (on-signal-load-redirected buffer url))\n            ((eq load-event :webkit-load-committed)\n             (on-signal-load-committed buffer url))\n            ((eq load-event :webkit-load-finished)\n             (unless (eq (slot-value buffer 'nyxt::status) :failed)\n               (setf (nyxt::status buffer) :finished))\n             (on-signal-load-finished buffer url (ffi-buffer-title buffer))\n             (unless (internal-url-p url)\n               (echo \"Finished loading ~s.\" (render-url url))))))))\n\n(define-ffi-method on-signal-mouse-target-changed ((buffer gtk-buffer)\n                                                   hit-test-result\n                                                   modifiers)\n  (declare (ignore modifiers))\n  (if-let ((url (or (webkit:webkit-hit-test-result-link-uri hit-test-result)\n                    (webkit:webkit-hit-test-result-image-uri hit-test-result)\n                    (webkit:webkit-hit-test-result-media-uri hit-test-result))))\n    (progn\n      (nyxt::print-message (str:concat \"→ \" (render-url url)))\n      (setf (url-at-point buffer) (quri:uri url)))\n    (progn\n      (nyxt::print-message \"\")\n      (setf (url-at-point buffer) (quri:uri \"\")))))\n\n(define-ffi-method ffi-window-to-foreground ((window gtk-window))\n  \"Show window in foreground.\"\n  (unless *headless-p* (gtk:gtk-window-present (gtk-object window)))\n  (call-next-method))\n\n(define-ffi-method ffi-window-title ((window gtk-window))\n  (gtk:gtk-window-title (gtk-object window)))\n(define-ffi-method (setf ffi-window-title) (title (window gtk-window))\n  (setf (gtk:gtk-window-title (gtk-object window)) title))\n\n(define-ffi-method ffi-window-active ((browser gtk-browser))\n  \"Return the focused window.\"\n  (or (find-if #'gtk:gtk-window-is-active (window-list) :key #'gtk-object)\n      (call-next-method)))\n\n(define-ffi-method ffi-window-set-buffer ((window gtk-window) (buffer gtk-buffer) &key (focus t))\n  \"Set BROWSER's WINDOW buffer to BUFFER.\"\n  (when-let ((buried-buffer (gtk-object (active-buffer window))))\n    ;; Just a precaution for the buffer to not be destroyed until we say so.\n    (g:g-object-ref (g:pointer buried-buffer))\n    (gtk:gtk-container-remove (main-buffer-container window) buried-buffer))\n  (gtk:gtk-box-pack-start (main-buffer-container window)\n                          (gtk-object buffer)\n                          :expand t :fill t)\n  (unless *headless-p* (gtk:gtk-widget-show (gtk-object buffer)))\n  (when focus (gtk:gtk-widget-grab-focus (gtk-object buffer))))\n\n(define-ffi-method ffi-height ((buffer prompt-buffer))\n  (gtk:gtk-widget-height-request (prompt-buffer-container (window buffer))))\n(define-ffi-method (setf ffi-height) ((height integer) (buffer prompt-buffer))\n  (setf (gtk:gtk-widget-height-request (prompt-buffer-container (window buffer)))\n        height))\n\n(define-ffi-method ffi-focus-buffer ((buffer gtk-buffer))\n  \"Focus PROMPT-BUFFER in WINDOW.\"\n  (gtk:gtk-widget-grab-focus (gtk-object buffer))\n  buffer)\n\n(define-ffi-method ffi-height ((buffer status-buffer))\n  (gtk:gtk-widget-height-request (status-container (window buffer))))\n(define-ffi-method (setf ffi-height) (height (buffer status-buffer))\n  (setf (gtk:gtk-widget-height-request (status-container (window buffer)))\n        height))\n\n(define-ffi-method ffi-height ((buffer message-buffer))\n  (gtk:gtk-widget-height-request (message-container (window buffer))))\n(define-ffi-method (setf ffi-height) (height (buffer message-buffer))\n  (setf (gtk:gtk-widget-height-request (message-container (window buffer)))\n        height))\n\n(defun get-bounds (object)\n  (gtk:gtk-widget-get-allocation (nyxt/renderer/gtk::gtk-object object)))\n\n(define-ffi-method ffi-height ((buffer gtk-buffer))\n  (gdk:gdk-rectangle-height (get-bounds buffer)))\n(define-ffi-method ffi-width ((buffer gtk-buffer))\n  (gdk:gdk-rectangle-width (get-bounds buffer)))\n\n(define-ffi-method ffi-height ((window gtk-window))\n  (gdk:gdk-rectangle-height (get-bounds window)))\n(define-ffi-method ffi-width ((window gtk-window))\n  (gdk:gdk-rectangle-width (get-bounds window)))\n\n(defun process-file-chooser-request (web-view file-chooser-request)\n  (declare (ignore web-view))\n  (with-protect (\"Failed to process file chooser request: ~a\" :condition)\n    (when (native-dialogs *browser*)\n      (gobject:g-object-ref (gobject:pointer file-chooser-request))\n      (run-thread \"file chooser\"\n                  (let* ((multiple (webkit:webkit-file-chooser-request-select-multiple\n                                    file-chooser-request))\n                         (files (mapcar\n                                 #'uiop:native-namestring\n                                 (handler-case\n                                     (prompt :prompt (format nil \"File~@[s~*~] to input\" multiple)\n                                             :input (or\n                                                     (and\n                                                      (webkit:webkit-file-chooser-request-selected-files\n                                                       file-chooser-request)\n                                                      (first\n                                                       (webkit:webkit-file-chooser-request-selected-files\n                                                        file-chooser-request)))\n                                                     (uiop:native-namestring (uiop:getcwd)))\n                                             :extra-modes 'nyxt/mode/file-manager:file-manager-mode\n                                             :sources (make-instance 'nyxt/mode/file-manager:file-source\n                                                                     :enable-marks-p multiple))\n                                   (prompt-buffer-canceled ()\n                                     nil)))))\n                    (if files\n                        (webkit:webkit-file-chooser-request-select-files\n                         file-chooser-request\n                         (cffi:foreign-alloc :string\n                                             :initial-contents (mapcar #'cffi:foreign-string-alloc files)\n                                             :count (if multiple\n                                                        (length files)\n                                                        1)\n                                             :null-terminated-p t))\n                        (webkit:webkit-file-chooser-request-cancel file-chooser-request))))\n      t)))\n\n(defun process-color-chooser-request (web-view color-chooser-request)\n  (declare (ignore web-view))\n  (with-protect (\"Failed to process file chooser request: ~a\" :condition)\n    (when (native-dialogs *browser*)\n      (gobject:g-object-ref (gobject:pointer color-chooser-request))\n      (run-thread\n          \"color chooser\"\n        (ps-labels\n          ((get-rgba\n            (color)\n            (let ((div (ps:chain document (create-element \"div\"))))\n              (setf (ps:chain div style color)\n                    (ps:lisp color))\n              (ps:chain document body (append-child div))\n              (ps:stringify (ps:chain window (get-computed-style div) color))))\n           (get-opacity (color)\n                        (let ((div (ps:chain document (create-element \"div\"))))\n                          (setf (ps:chain div style color)\n                                (ps:lisp color))\n                          (ps:chain document body (append-child div))\n                          (ps:stringify (ps:chain window (get-computed-style div) opacity)))))\n          (let* ((rgba (gdk:make-gdk-rgba))\n                 (rgba (progn (webkit:webkit-color-chooser-request-get-rgba\n                               color-chooser-request rgba)\n                              rgba))\n                 (color-name (prompt1 :prompt \"Color\"\n                                      :input (format nil \"rgba(~d, ~d, ~d, ~d)\"\n                                                     (round (* 255 (gdk:gdk-rgba-red rgba)))\n                                                     (round (* 255 (gdk:gdk-rgba-green rgba)))\n                                                     (round (* 255 (gdk:gdk-rgba-blue rgba)))\n                                                     (round (* 255 (gdk:gdk-rgba-alpha rgba))))\n                                      :sources 'color-source))\n                 (color (get-rgba color-name))\n                 (opacity (sera:parse-float (get-opacity color-name)))\n                 (rgba (gdk:gdk-rgba-parse color)))\n            (unless (uiop:emptyp color)\n              (webkit:webkit-color-chooser-request-set-rgba\n               color-chooser-request\n               (gdk:make-gdk-rgba :red (gdk:gdk-rgba-red rgba)\n                                  :green (gdk:gdk-rgba-green rgba)\n                                  :blue (gdk:gdk-rgba-blue rgba)\n                                  :alpha (coerce opacity 'double-float)))\n              (webkit:webkit-color-chooser-request-finish (g:pointer color-chooser-request))))))\n      t)))\n\n(defun process-script-dialog (web-view dialog)\n  (declare (ignore web-view))\n  (with-protect (\"Failed to process dialog: ~a\" :condition)\n    (when (native-dialogs *browser*)\n      (let ((dialog (gobject:pointer dialog)))\n        (webkit:webkit-script-dialog-ref dialog)\n        (run-thread \"script dialog\"\n          (case (webkit:webkit-script-dialog-get-dialog-type dialog)\n            (:webkit-script-dialog-alert (echo (webkit:webkit-script-dialog-get-message dialog)))\n            (:webkit-script-dialog-prompt\n             (let ((text (first (handler-case\n                                    (prompt\n                                     :prompt (webkit:webkit-script-dialog-get-message dialog)\n                                     :input (webkit:webkit-script-dialog-prompt-get-default-text dialog)\n                                     :sources 'prompter:raw-source)\n                                  (prompt-buffer-canceled () nil)))))\n               (if text\n                   (webkit:webkit-script-dialog-prompt-set-text dialog text)\n                   (progn\n                     (webkit:webkit-script-dialog-prompt-set-text dialog (cffi:null-pointer))\n                     (webkit:webkit-script-dialog-close dialog)))))\n            (:webkit-script-dialog-confirm\n             (webkit:webkit-script-dialog-confirm-set-confirmed\n              dialog (if-confirm ((webkit:webkit-script-dialog-get-message dialog)))))\n            (:webkit-script-dialog-before-unload-confirm\n             (webkit:webkit-script-dialog-confirm-set-confirmed\n              dialog (if-confirm ((webkit:webkit-script-dialog-get-message dialog)\n                                  :yes \"leave\" :no \"stay\")))))\n          (webkit:webkit-script-dialog-close dialog)\n          (webkit:webkit-script-dialog-unref dialog))\n        t))))\n\n(defun process-permission-request (web-view request)\n  (g:g-object-ref (g:pointer request))\n  (run-thread \"permission requester\"\n    (if-confirm ((format\n                  nil \"[~a] ~a\"\n                  (webkit:webkit-web-view-uri web-view)\n                  (etypecase request\n                    (webkit:webkit-geolocation-permission-request\n                     \"Grant this website geolocation access?\")\n                    (webkit:webkit-notification-permission-request\n                     \"Grant this website notifications access?\")\n                    (webkit:webkit-pointer-lock-permission-request\n                     \"Grant this website pointer access?\")\n                    (webkit:webkit-device-info-permission-request\n                     \"Grant this website device info access?\")\n                    (webkit:webkit-install-missing-media-plugins-permission-request\n                     (format nil \"Grant this website a media install permission for ~s?\"\n                             (webkit:webkit-install-missing-media-plugins-permission-request-get-description\n                              request)))\n                    (webkit:webkit-media-key-system-permission-request\n                     (format nil \"Grant this website an EME ~a key access?\"\n                             (webkit:webkit-media-key-system-permission-get-name request)))\n                    (webkit:webkit-user-media-permission-request\n                     (format nil \"Grant this website a~@[~*n audio~]~@[~* video~] access?\"\n                             (webkit:webkit-user-media-permission-is-for-audio-device request)\n                             (webkit:webkit-user-media-permission-is-for-video-device request)))\n                    (webkit:webkit-website-data-access-permission-request\n                     (format nil \"Grant ~a an access to ~a data?\"\n                             (webkit:webkit-website-data-access-permission-request-get-requesting-domain\n                              request)\n                             (webkit:webkit-website-data-access-permission-request-get-current-domain\n                              request)))))\n                 :yes \"grant\" :no \"deny\")\n        (webkit:webkit-permission-request-allow request)\n        (webkit:webkit-permission-request-deny request))))\n\n(defun process-notification (web-view notification)\n  (when (native-dialogs *browser*)\n    (let* ((title (webkit:webkit-notification-get-title notification))\n           (body (webkit:webkit-notification-get-body notification)))\n      (echo \"[~a] ~a: ~a\" (webkit:webkit-web-view-uri web-view) title body)\n      t)))\n\n(define-ffi-method ffi-buffer-initialize-foreign-object ((buffer gtk-buffer))\n  \"Initialize BUFFER's GTK web view.\"\n  (setf (gtk-object buffer)\n        (if (prompt-buffer-p buffer)\n            ;; A single web view is shared by all prompt buffers of a window.\n            (prompt-buffer-view (window buffer))\n            (make-instance 'webkit:webkit-web-view\n                           :web-context (get-web-context *browser* \"default\"))))\n  (when (document-buffer-p buffer)\n    (setf (ffi-buffer-smooth-scrolling-enabled-p buffer) (smooth-scrolling buffer)))\n  ;; TODO: Maybe define an FFI method?\n  (let ((settings (webkit:webkit-web-view-get-settings (gtk-object buffer))))\n    (when (getf *options* :verbose)\n      (setf (webkit:webkit-settings-enable-write-console-messages-to-stdout settings)\n            t))\n    (setf (webkit:webkit-settings-enable-resizable-text-areas settings) t\n          (webkit:webkit-settings-enable-developer-extras settings) t\n          (webkit:webkit-settings-enable-page-cache settings) t\n          (webkit:webkit-settings-enable-encrypted-media settings) t))\n  (connect-signal-function buffer \"decide-policy\" (make-decide-policy-handler buffer))\n  (connect-signal buffer \"resource-load-started\" nil (web-view resource request)\n    (declare (ignore web-view))\n    (let* ((response (webkit:webkit-web-resource-response resource))\n           (request-data (make-instance\n                          'request-data\n                          :buffer buffer\n                          :url (quri:uri (webkit:webkit-uri-request-get-uri request))\n                          :event-type :other\n                          :new-window-p nil\n                          :resource-p t\n                          :http-method (webkit:webkit-uri-request-get-http-method request)\n                          :response-headers (when response\n                                              (let ((headers (webkit:webkit-uri-response-get-http-headers request)))\n                                                (unless (cffi:null-pointer-p headers)\n                                                  (webkit:soup-message-headers-get-headers headers))))\n                          :request-headers (let ((headers (webkit:webkit-uri-request-get-http-headers request)))\n                                             (unless (cffi:null-pointer-p headers)\n                                               (webkit:soup-message-headers-get-headers headers)))\n                          :toplevel-p nil\n                          :mime-type (when response\n                                       (webkit:webkit-uri-response-mime-type response))\n                          :known-type-p t)))\n      (setf (gtk-response request-data) response\n            (gtk-request request-data) request\n            (gtk-resource request-data) resource)\n      (when (request-resource-hook buffer)\n        (hooks:run-hook (request-resource-hook buffer) request-data))))\n  (connect-signal buffer \"load-changed\" t (web-view load-event)\n    (declare (ignore web-view))\n    (on-signal-load-changed buffer load-event))\n  (connect-signal buffer \"mouse-target-changed\" nil (web-view hit-test-result modifiers)\n    (declare (ignore web-view))\n    (on-signal-mouse-target-changed buffer hit-test-result modifiers))\n  ;; Mouse events are captured by the web view first, so we must intercept them here.\n  (connect-signal buffer \"button-press-event\" nil (web-view event)\n    (declare (ignore web-view))\n    (on-signal-button-press-event buffer event))\n  (connect-signal buffer \"key_press_event\" nil (widget event)\n    (declare (ignore widget))\n    (on-signal-key-press-event buffer event))\n  (connect-signal buffer \"scroll-event\" nil (web-view event)\n    (declare (ignore web-view))\n    (on-signal-scroll-event buffer event))\n  (connect-signal-function buffer \"script-dialog\" #'process-script-dialog)\n  (connect-signal-function buffer \"run-file-chooser\" #'process-file-chooser-request)\n  (connect-signal-function buffer \"run-color-chooser\" #'process-color-chooser-request)\n  (when (handle-permission-requests-p buffer)\n    (connect-signal-function buffer \"permission-request\" #'process-permission-request))\n  (connect-signal-function buffer \"show-notification\" #'process-notification)\n  ;; TLS certificate handling\n  (connect-signal buffer \"load-failed-with-tls-errors\" nil (web-view failing-url certificate errors)\n    (declare (ignore web-view errors))\n    (on-signal-load-failed buffer (quri:uri failing-url))\n    (on-signal-load-failed-with-tls-errors buffer certificate (quri:uri failing-url)))\n  (connect-signal buffer \"notify::uri\" nil (web-view param-spec)\n    (declare (ignore web-view param-spec))\n    (on-signal-notify-uri buffer nil))\n  (connect-signal buffer \"notify::title\" nil (web-view param-spec)\n    (declare (ignore web-view param-spec))\n    (on-signal-notify-title buffer nil))\n  (connect-signal buffer \"web-process-terminated\" nil (web-view reason)\n    ;; TODO: Bind WebKitWebProcessTerminationReason in cl-webkit.\n    (echo-warning\n     \"Web process terminated for buffer ~a (opening ~a) because ~[it crashed~;of memory exhaustion~;we had to close it~]\"\n     (id buffer)\n     (url buffer)\n     (cffi:foreign-enum-value 'webkit:webkit-web-process-termination-reason reason))\n    (log:debug\n     \"Web process terminated for web view ~a because of ~[WEBKIT_WEB_PROCESS_CRASHED~;WEBKIT_WEB_PROCESS_EXCEEDED_MEMORY_LIMIT~;WEBKIT_WEB_PROCESS_TERMINATED_BY_API~]\"\n     web-view\n     (cffi:foreign-enum-value 'webkit:webkit-web-process-termination-reason reason))\n    (ffi-buffer-delete buffer))\n  (connect-signal buffer \"close\" nil (web-view)\n    (declare (ignore web-view))\n    (log:debug \"Closed ~a\" buffer))\n  (connect-signal buffer \"load-failed\" nil (web-view load-event failing-url error)\n    (declare (ignore load-event web-view))\n    (on-signal-load-failed buffer (quri:uri failing-url))\n    (cond ((= 302 (webkit::g-error-code error))\n           (on-signal-load-canceled buffer (quri:uri failing-url)))\n          ((or (member (slot-value buffer 'nyxt::status) '(:finished :failed))\n               ;; WebKitGTK emits the WEBKIT_PLUGIN_ERROR_WILL_HANDLE_LOAD\n               ;; (204) if the plugin will handle loading content of the\n               ;; URL. This often happens with videos. The only thing we\n               ;; can do is ignore it.\n               ;;\n               ;; TODO: Use cl-webkit provided error types. How\n               ;; do we use it, actually?\n               (= 204 (webkit::g-error-code error)))\n           nil)\n          (t\n           (echo \"Failed to load URL ~a in buffer ~a.\" failing-url (id buffer))\n           (setf (nyxt::status buffer) :failed)\n           (ffi-buffer-load-alternate-html\n            buffer\n            (spinneret:with-html-string\n              (:head\n               (:nstyle (style buffer)))\n              (:h1 \"Page could not be loaded.\")\n              (:h2 \"URL: \" failing-url)\n              (:ul\n               (:li \"Try again in a moment, maybe the site will be available again.\")\n               (:li \"If the problem persists for every site, check your Internet connection.\")\n               (:li \"Make sure the URL is valid.\"\n                    (when (quri:uri-https-p (quri:uri failing-url))\n                      \"If this site does not support HTTPS, try with HTTP (insecure).\"))))\n            failing-url\n            failing-url)))\n    t)\n  (connect-signal buffer \"create\" nil (web-view navigation-action)\n    (declare (ignore web-view))\n    (let ((url (webkit:webkit-uri-request-uri\n                (webkit:webkit-navigation-action-get-request\n                 (gobject:pointer navigation-action)))))\n      (gtk-object (make-buffer-focus :url (quri:uri url)))))\n  (connect-signal buffer \"context-menu\" nil (web-view context-menu event hit-test-result)\n    (declare (ignore web-view event hit-test-result))\n    (loop with length = (webkit:webkit-context-menu-get-n-items context-menu)\n          for i below length\n          for item = (webkit:webkit-context-menu-get-item-at-position context-menu i)\n          when (and (or (status-buffer-p buffer) (message-buffer-p buffer))\n                    (not (eq (webkit:webkit-context-menu-item-get-stock-action item)\n                             :webkit-context-menu-action-inspect-element)))\n            do (webkit:webkit-context-menu-remove context-menu item)\n          else\n            do (match (webkit:webkit-context-menu-item-get-stock-action item)\n                 (:webkit-context-menu-action-open-link-in-new-window\n                  (webkit:webkit-context-menu-remove context-menu item)\n                  (webkit:webkit-context-menu-insert\n                   context-menu\n                   (webkit:webkit-context-menu-item-new-from-stock-action-with-label\n                    :webkit-context-menu-action-open-link-in-new-window\n                    \"Open Link in New Buffer\")\n                   i))))\n    (webkit:webkit-context-menu-append\n     context-menu (webkit:webkit-context-menu-item-new-separator))\n    (let* ((accessible-commands\n             (mapcar #'name\n                     (nyxt::list-commands\n                      :global-p t\n                      :mode-symbols (mapcar #'sera:class-name-of\n                                            (sera:filter #'enabled-p (modes buffer)))))))\n      (maphash (lambda (label function)\n                 (flet ((make-item (label function)\n                          ;; Using stock actions here, because cl-cffi-gtk has a\n                          ;; terrible API for GActions, requiring an exact type\n                          ;; to be passed and disallowing NULL as a type.\n                          (sera:lret ((item (webkit:webkit-context-menu-item-new-from-stock-action-with-label\n                                             :webkit-context-menu-action-action-custom label)))\n                            (gobject:g-signal-connect\n                             (webkit:webkit-context-menu-item-get-g-action item) \"activate\"\n                             (lambda (action parameter)\n                               (declare (ignore action parameter))\n                               (nyxt::run-async function))))))\n                   (cond\n                     ((or (and (command-p function)\n                               (member function accessible-commands))\n                          (functionp function))\n                      (webkit:webkit-context-menu-append context-menu (make-item label function)))\n                     ((listp function)\n                      (let ((submenu (webkit:webkit-context-menu-new)))\n                        (loop for (command command-label) in function\n                              do (webkit:webkit-context-menu-append\n                                  submenu (make-item command-label command)))\n                        (webkit:webkit-context-menu-append\n                         context-menu\n                         (webkit:webkit-context-menu-item-new-with-submenu label submenu)))))))\n               nyxt::*context-menu-commands*))\n    nil)\n  (connect-signal buffer \"enter-fullscreen\" nil (web-view)\n    (declare (ignore web-view))\n    (ffi-window-fullscreen (current-window) :user-event-p nil)\n    ;; As to account for JS's Fullscreen API.\n    (disable-message-buffer (current-window))\n    (disable-status-buffer (current-window))\n    nil)\n  (connect-signal buffer \"leave-fullscreen\" nil (web-view)\n    (declare (ignore web-view))\n    (ffi-window-unfullscreen (current-window) :user-event-p nil)\n    ;; Ideally, the UI state prior to fullscreen must be recovered.\n    (enable-message-buffer (current-window))\n    (enable-status-buffer (current-window))\n    nil)\n  buffer)\n\n(define-ffi-method ffi-buffer-delete ((buffer gtk-buffer))\n  (with-slots (gtk-object handler-ids) buffer\n    (webkit:webkit-web-view-try-close gtk-object)\n    (mapc (lambda (id) (gobject:g-signal-handler-disconnect gtk-object id))\n          handler-ids)\n    (unless (prompt-buffer-p buffer) (gtk:gtk-widget-destroy gtk-object))\n    (setf gtk-object nil)\n    (when (prompt-buffer-p buffer) (setf (ffi-height buffer) 0))))\n\n(define-ffi-method ffi-buffer-load ((buffer gtk-buffer) url)\n  \"Load URL in BUFFER.\"\n  (declare (type quri:uri url))\n  ;; Mark buffer as :loading right away so functions like\n  ;; `ffi-window-set-buffer' don't try to reload if they are called before the\n  ;; \"load-changed\" signal is emitted.\n  (when (web-buffer-p buffer) (setf (nyxt::status buffer) :loading))\n  (webkit:webkit-web-view-load-uri (gtk-object buffer) (quri:render-uri url)))\n\n(define-ffi-method ffi-buffer-reload ((buffer gtk-buffer))\n  (webkit:webkit-web-view-reload (gtk-object buffer))\n  buffer)\n\n(define-ffi-method ffi-buffer-load-alternate-html ((buffer gtk-buffer)\n                                                   html-content\n                                                   content-url\n                                                   url)\n  (webkit:webkit-web-view-load-alternate-html (gtk-object buffer)\n                                              html-content\n                                              (quri:render-uri (url content-url))\n                                              (if (uiop:emptyp url) \"about:blank\" url)))\n\n(defmethod ffi-buffer-evaluate-javascript ((buffer gtk-buffer) javascript &optional world-name)\n  (%within-renderer-thread\n   (lambda (&optional channel)\n     (when (gtk-object buffer)\n       (webkit2:webkit-web-view-evaluate-javascript\n        (gtk-object buffer)\n        javascript\n        (if channel\n            (lambda (result jsc-result)\n              (declare (ignore jsc-result))\n              (calispel:! channel result))\n            (lambda (result jsc-result)\n              (declare (ignore jsc-result))\n              result))\n        (lambda (condition)\n          (nyxt::javascript-error-handler condition)\n          ;; Notify the listener that we are done.\n          (when channel\n            (calispel:! channel nil)))\n        world-name)))))\n\n(defmethod ffi-buffer-evaluate-javascript-async ((buffer gtk-buffer) javascript &optional world-name)\n  (%within-renderer-thread-async\n   (lambda ()\n     (when (gtk-object buffer)\n       (webkit2:webkit-web-view-evaluate-javascript\n        (gtk-object buffer)\n        javascript\n        nil\n        #'nyxt::javascript-error-handler\n        world-name)))))\n\n(defun list-of-string-to-foreign (list)\n  (if list\n      (cffi:foreign-alloc :string\n                          :count (length list)\n                          :initial-contents list\n                          :null-terminated-p t)\n      (cffi:null-pointer)))\n\n(define-class gtk-user-style ()\n  ((gtk-object))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:documentation \"Related to WebKit's user style sheets.\"))\n\n(define-ffi-method ffi-buffer-add-user-style ((buffer gtk-buffer) (style gtk-user-style))\n  (let* ((content-manager\n           (webkit:webkit-web-view-get-user-content-manager\n            (gtk-object buffer)))\n         (frames (if (nyxt/mode/user-script:all-frames-p style)\n                     :webkit-user-content-inject-all-frames\n                     :webkit-user-content-inject-top-frame))\n         (style-level (if (eq (nyxt/mode/user-script:level style) :author)\n                          :webkit-user-style-level-author\n                          :webkit-user-style-level-user))\n         (style-sheet\n           (if (nyxt/mode/user-script:world-name style)\n               (webkit:webkit-user-style-sheet-new-for-world\n                (nyxt/mode/user-script:code style)\n                frames style-level\n                (nyxt/mode/user-script:world-name style)\n                (list-of-string-to-foreign (nyxt/mode/user-script:include style))\n                (list-of-string-to-foreign (nyxt/mode/user-script:exclude style)))\n               (webkit:webkit-user-style-sheet-new\n                (nyxt/mode/user-script:code style)\n                frames style-level\n                (list-of-string-to-foreign (nyxt/mode/user-script:include style))\n                (list-of-string-to-foreign (nyxt/mode/user-script:exclude style))))))\n    (setf (gtk-object style) style-sheet)\n    (webkit:webkit-user-content-manager-add-style-sheet\n     content-manager style-sheet)\n    style))\n\n(define-ffi-method ffi-buffer-remove-user-style ((buffer gtk-buffer) (style gtk-user-style))\n  (webkit:webkit-user-content-manager-remove-style-sheet\n   (webkit:webkit-web-view-get-user-content-manager (gtk-object buffer))\n   (gtk-object style)))\n\n(define-class gtk-user-script ()\n  ((gtk-object))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:documentation \"Related to WebKitUserScript.\"))\n\n(define-ffi-method ffi-buffer-add-user-script ((buffer gtk-buffer) (script gtk-user-script))\n  (if-let ((code (nyxt/mode/user-script:code script)))\n    (let* ((content-manager\n             (webkit:webkit-web-view-get-user-content-manager\n              (gtk-object buffer)))\n           (frames (if (nyxt/mode/user-script:all-frames-p script)\n                       :webkit-user-content-inject-all-frames\n                       :webkit-user-content-inject-top-frame))\n           (inject-time (if (eq :document-start (nyxt/mode/user-script:run-at script))\n                            :webkit-user-script-inject-at-document-start\n                            :webkit-user-script-inject-at-document-end))\n           (allow-list (list-of-string-to-foreign\n                        (or (nyxt/mode/user-script:include script)\n                            '(\"http://*/*\" \"https://*/*\"))))\n           (block-list (list-of-string-to-foreign\n                        (nyxt/mode/user-script:exclude script)))\n           (user-script (if (nyxt/mode/user-script:world-name script)\n                            (webkit:webkit-user-script-new-for-world\n                             code frames inject-time\n                             (nyxt/mode/user-script:world-name script) allow-list block-list)\n                            (webkit:webkit-user-script-new\n                             code frames inject-time allow-list block-list))))\n      (setf (gtk-object script) user-script)\n      (webkit:webkit-user-content-manager-add-script\n       content-manager user-script)\n      script)\n    (echo-warning \"User script ~a is empty.\" script)))\n\n(define-ffi-method ffi-buffer-remove-user-script ((buffer gtk-buffer) (script gtk-user-script))\n  (let ((content-manager\n          (webkit:webkit-web-view-get-user-content-manager\n           (gtk-object buffer))))\n    (when (and script (gtk-object script))\n      (webkit:webkit-user-content-manager-remove-script\n       content-manager (gtk-object script)))))\n\n(defmacro define-ffi-settings-accessor (setting-name webkit-setting)\n  (let ((full-name (intern (format nil \"FFI-BUFFER-~a\" setting-name))))\n    (symbol-function full-name)\n    `(progn\n       (define-ffi-method ,full-name ((buffer gtk-buffer))\n         (,webkit-setting\n          (webkit:webkit-web-view-get-settings (gtk-object buffer))))\n       (define-ffi-method (setf ,full-name) (value (buffer gtk-buffer))\n         (setf (,webkit-setting\n                (webkit:webkit-web-view-get-settings (gtk-object buffer)))\n               value)))))\n\n(define-ffi-settings-accessor javascript-enabled-p webkit:webkit-settings-enable-javascript)\n(define-ffi-settings-accessor javascript-markup-enabled-p webkit:webkit-settings-enable-javascript-markup)\n(define-ffi-settings-accessor smooth-scrolling-enabled-p webkit:webkit-settings-enable-smooth-scrolling)\n(define-ffi-settings-accessor media-enabled-p webkit:webkit-settings-enable-media)\n(define-ffi-settings-accessor webgl-enabled-p webkit:webkit-settings-enable-webgl)\n(define-ffi-settings-accessor auto-load-image-enabled-p webkit:webkit-settings-auto-load-images)\n\n(defmethod ffi-buffer-sound-enabled-p ((buffer gtk-buffer))\n  (not (webkit:webkit-web-view-get-is-muted (gtk-object buffer))))\n(defmethod (setf ffi-buffer-sound-enabled-p) (value (buffer gtk-buffer))\n  (webkit:webkit-web-view-set-is-muted (gtk-object buffer) (not value)))\n\n;; KLUDGE: PDF.js in WebKit (actual for version 2.41.4) always saves\n;; PDFs as \"document.pdf\". This is because WebKit does not pass \"file\"\n;; parameter to the viewer. See\n;; https://stackoverflow.com/questions/47098206/pdf-js-downloading-as-document-pdf-instead-of-filename .\n;; Here we restore the original file name from an URL if a suggested\n;; file name looks suspicious.\n(sera:-> maybe-fix-pdfjs-filename (string quri:uri)\n         (values string &optional))\n(defun maybe-fix-pdfjs-filename (suggested-file-name uri)\n  (let ((pathname (pathname (quri:uri-path uri))))\n    (if (and (string= suggested-file-name \"document.pdf\")\n             (string= (pathname-type pathname) \"pdf\"))\n        (uiop:native-namestring\n         (make-pathname :name (pathname-name pathname)\n                        :type \"pdf\"))\n        suggested-file-name)))\n\n(defun wrap-download (webkit-download)\n  (sera:lret ((original-url (url (current-buffer)))\n              (download (make-instance 'nyxt/mode/download:download\n                                       :url (webkit:webkit-uri-request-uri\n                                             (webkit:webkit-download-get-request webkit-download))\n                                       :gtk-object webkit-download)))\n    (setf (nyxt/mode/download::cancel-function download)\n          (lambda ()\n            (setf (nyxt/mode/download:status download) :canceled)\n            (webkit:webkit-download-cancel webkit-download)))\n    (push download (downloads *browser*))\n    (connect-signal download \"received-data\" nil (webkit-download data-length)\n      (declare (ignore data-length))\n      (setf (nyxt/mode/download:bytes-downloaded download)\n            (webkit:webkit-download-get-received-data-length webkit-download))\n      (setf (nyxt/mode/download:completion-percentage download)\n            (* 100 (webkit:webkit-download-estimated-progress webkit-download))))\n    (connect-signal download \"decide-destination\" nil (webkit-download suggested-file-name)\n      (when-let* ((suggested-file-name (maybe-fix-pdfjs-filename suggested-file-name original-url))\n                  (download-dir (or (ignore-errors\n                                     (download-directory\n                                      (find (webkit:webkit-download-get-web-view webkit-download)\n                                            (buffer-list) :key #'gtk-object)))\n                                    (make-instance 'download-directory)))\n                  (download-directory (files:expand download-dir))\n                  (native-download-directory (unless (files:nil-pathname-p download-directory)\n                                               (uiop:native-namestring download-directory)))\n                  (path (str:concat native-download-directory suggested-file-name))\n                  (unique-path (download-manager::ensure-unique-file path))\n                  (file-path (format nil \"file://~a\" unique-path)))\n        (if (string= path unique-path)\n            (log:debug \"Downloading file to ~s.\" unique-path)\n            (echo \"Destination ~s exists, saving as ~s.\" path unique-path))\n        (webkit:webkit-download-set-destination webkit-download file-path)))\n    (connect-signal download \"created-destination\" nil (webkit-download destination)\n      (declare (ignore destination))\n      (setf (nyxt/mode/download:destination-path download)\n            (uiop:ensure-pathname\n             (quri:uri-path (quri:uri\n                             (webkit:webkit-download-destination webkit-download))))))\n    (connect-signal download \"failed\" nil (webkit-download error)\n      (declare (ignore error))\n      (unless (eq (nyxt/mode/download:status download) :canceled)\n        (setf (nyxt/mode/download:status download) :failed))\n      (echo \"Download failed for ~s.\"\n            (webkit:webkit-uri-request-uri\n             (webkit:webkit-download-get-request webkit-download))))\n    (connect-signal download \"finished\" nil (webkit-download)\n      (declare (ignore webkit-download))\n      (unless (member (nyxt/mode/download:status download) '(:canceled :failed))\n        (setf (nyxt/mode/download:status download) :finished)\n        ;; If download was too small, it may not have been updated.\n        (setf (nyxt/mode/download:completion-percentage download) 100)))))\n\n(defmethod ffi-buffer-download ((buffer gtk-buffer) url)\n  (webkit:webkit-web-view-download-uri (gtk-object buffer) url))\n\n(define-ffi-method ffi-buffer-user-agent ((buffer gtk-buffer))\n  (when-let ((settings (webkit:webkit-web-view-get-settings (gtk-object buffer))))\n    (webkit:webkit-settings-user-agent settings)))\n\n(define-ffi-method (setf ffi-buffer-user-agent) (value (buffer gtk-buffer))\n  (when-let ((settings (webkit:webkit-web-view-get-settings (gtk-object buffer))))\n    (setf (webkit:webkit-settings-user-agent settings) value)))\n\n(define-ffi-method ffi-buffer-proxy ((buffer gtk-buffer))\n  \"Return the proxy URL and list of ignored hosts (a list of strings) as second value.\"\n  (the (values (or quri:uri null) (list-of string))\n       (values (gtk-proxy-url buffer)\n               (proxy-ignored-hosts buffer))))\n(define-ffi-method (setf ffi-buffer-proxy) (proxy-specifier (buffer gtk-buffer))\n  \"Redirect network connections of BUFFER to proxy server PROXY-URL.\nHosts in IGNORE-HOSTS (a list of strings) ignore the proxy.\nFor the user-level interface, see `proxy-mode'.\n\nPROXY-SPECIFIER is either a PROXY-URL or a pair of (PROXY-URL IGNORE-HOSTS).\n\nNote: WebKit supports three proxy 'modes': default (the system proxy),\ncustom (the specified proxy) and none.\"\n  (let ((proxy-url (first (alex:ensure-list proxy-specifier)))\n        (ignore-hosts (or (second (alex:ensure-list proxy-specifier))\n                          nil)))\n    (declare (type quri:uri proxy-url))\n    (setf (gtk-proxy-url buffer) proxy-url)\n    (setf (proxy-ignored-hosts buffer) ignore-hosts)\n    (let* ((context (webkit:webkit-web-view-web-context (gtk-object buffer)))\n           (settings (cffi:null-pointer))\n           (mode :webkit-network-proxy-mode-no-proxy)\n           (ignore-hosts (cffi:foreign-alloc :string\n                                             :initial-contents ignore-hosts\n                                             :null-terminated-p t)))\n      (unless (url-empty-p proxy-url)\n        (setf mode :webkit-network-proxy-mode-custom)\n        (setf settings\n              (webkit:webkit-network-proxy-settings-new (render-url proxy-url)\n                                                        ignore-hosts)))\n      (cffi:foreign-free ignore-hosts)\n      (webkit:webkit-web-context-set-network-proxy-settings context\n                                                            mode\n                                                            settings))))\n\n(define-ffi-method ffi-buffer-zoom-ratio ((buffer gtk-buffer))\n  (webkit:webkit-web-view-zoom-level (gtk-object buffer)))\n(define-ffi-method (setf ffi-buffer-zoom-ratio) (value (buffer gtk-buffer))\n  (if (and (floatp value) (plusp value))\n      (setf (webkit:webkit-web-view-zoom-level (gtk-object buffer)) value)\n      (echo-warning \"Zoom ratio must be a positive floating point number.\")))\n\n(define-ffi-method ffi-inspector-show ((buffer gtk-buffer))\n  (webkit:webkit-web-inspector-show\n   (webkit:webkit-web-view-get-inspector (gtk-object buffer))))\n\n(defmethod ffi-buffer-cookie-policy ((buffer gtk-buffer))\n  (if (renderer-thread-p nyxt::*renderer*)\n      (progn\n        (log:warn \"Querying cookie policy in WebKitGTK is only supported from a non-renderer thread.\")\n        nil)\n      (let ((result-channel (nyxt::make-channel 1)))\n        (run-thread \"WebKitGTK cookie-policy\"\n          (within-gtk-thread\n            (let* ((context (webkit:webkit-web-view-web-context (gtk-object buffer)))\n                   (cookie-manager (webkit:webkit-web-context-get-cookie-manager context)))\n              ;; TODO: Update upstream to export and fix `with-g-async-ready-callback'.\n              (webkit::with-g-async-ready-callback (callback\n                                                     (declare (ignorable webkit::user-data webkit::source-object))\n                                                     (calispel:! result-channel\n                                                                 (webkit:webkit-cookie-manager-get-accept-policy-finish\n                                                                  cookie-manager\n                                                                  webkit::result)))\n                (webkit:webkit-cookie-manager-get-accept-policy\n                 cookie-manager\n                 (cffi:null-pointer)\n                 callback\n                 (cffi:null-pointer))))))\n        (calispel:? result-channel))))\n(defmethod (setf ffi-buffer-cookie-policy) (value (buffer gtk-buffer))\n  \"Set the cookie policy to VALUE.\nValid values are determined by the `cookie-policy' type.\"\n  (let* ((context (webkit:webkit-web-view-web-context (gtk-object buffer)))\n         (cookie-manager (webkit:webkit-web-context-get-cookie-manager context)))\n    (setf (ffi-buffer-cookie-policy cookie-manager) value)\n    buffer))\n(defmethod (setf ffi-buffer-cookie-policy) (value (cookie-manager webkit:webkit-cookie-manager))\n  \"Set the cookie policy to VALUE.\nValid values are determined by the `cookie-policy' type.\"\n  (webkit:webkit-cookie-manager-set-accept-policy\n   cookie-manager\n   (match value\n     (:accept :webkit-cookie-policy-accept-always)\n     (:never :webkit-cookie-policy-accept-never)\n     (:no-third-party :webkit-cookie-policy-accept-no-third-party))))\n\n(defmethod ffi-preferred-languages ((buffer gtk-buffer))\n  \"Not supported by WebKitGTK.\nOnly the setf method is.\"\n  nil)\n(defmethod (setf ffi-preferred-languages) (language-list (buffer gtk-buffer))\n  \"LANGUAGE-LIST is a list of strings like '(\\\"en_US\\\" \\\"fr_FR\\\").\"\n  (let ((langs (cffi:foreign-alloc :string\n                                   :initial-contents language-list\n                                   :null-terminated-p t)))\n    (webkit:webkit-web-context-set-preferred-languages\n     (webkit:webkit-web-view-web-context (gtk-object buffer))\n     langs)))\n\n(define-ffi-method ffi-focused-p ((buffer gtk-buffer))\n  (gtk:gtk-widget-is-focus (gtk-object buffer)))\n\n(defmethod itp-enabled-p ((buffer gtk-buffer))\n  \"Return non-nil when Intelligent Tracking Prevention is enabled.\"\n  (webkit:webkit-website-data-manager-get-itp-enabled\n   (webkit:webkit-web-context-website-data-manager\n    (webkit:webkit-web-view-web-context (gtk-object buffer)))))\n(defmethod (setf itp-enabled-p) (value (buffer gtk-buffer))\n  (webkit:webkit-website-data-manager-set-itp-enabled\n   (webkit:webkit-web-context-website-data-manager\n    (webkit:webkit-web-view-web-context (gtk-object buffer)))\n   value))\n\n(defmethod enable :after ((mode nyxt/mode/reduce-tracking:reduce-tracking-mode) &key)\n  (setf (itp-enabled-p (buffer mode)) t))\n\n(defmethod disable :after ((mode nyxt/mode/reduce-tracking:reduce-tracking-mode) &key)\n  (setf (itp-enabled-p (buffer mode)) nil))\n\n(defmethod ffi-buffer-copy ((gtk-buffer gtk-buffer) &optional (text nil text-provided-p))\n  (if text-provided-p\n      (trivial-clipboard:text text)\n      (let ((channel (nyxt::make-channel 1)))\n        (webkit:webkit-web-view-can-execute-editing-command\n         (gtk-object gtk-buffer) webkit2:+webkit-editing-command-copy+\n         (lambda (can-execute?)\n           (if can-execute?\n               (progn\n                 (webkit:webkit-web-view-execute-editing-command\n                  (gtk-object gtk-buffer) webkit2:+webkit-editing-command-copy+)\n                 (calispel:! channel t)\n                 (echo \"~s copied to clipboard.\" text))\n               (calispel:! channel nil)))\n         (lambda (e) (echo-warning \"~s failed to copy to clipboard.\" e)))\n        (if (calispel:? channel)\n            (trivial-clipboard:text)\n            nil))))\n\n(defmethod ffi-buffer-paste ((gtk-buffer gtk-buffer) &optional (text nil text-provided-p))\n  (webkit:webkit-web-view-can-execute-editing-command\n   (gtk-object gtk-buffer) webkit2:+webkit-editing-command-paste+\n   (lambda (can-execute?)\n     (when can-execute?\n       (when text-provided-p\n         (trivial-clipboard:text text))\n       (webkit:webkit-web-view-execute-editing-command\n        (gtk-object gtk-buffer) webkit2:+webkit-editing-command-paste+)))\n   (lambda (e) (echo-warning \"~s failed to paste.\" e))))\n\n(defmethod ffi-buffer-cut ((gtk-buffer gtk-buffer))\n  (let ((channel (nyxt::make-channel 1)))\n    (webkit:webkit-web-view-can-execute-editing-command\n     (gtk-object gtk-buffer) webkit2:+webkit-editing-command-cut+\n     (lambda (can-execute?)\n       (if can-execute?\n           (progn\n             (webkit:webkit-web-view-execute-editing-command\n              (gtk-object gtk-buffer) webkit2:+webkit-editing-command-cut+)\n             (calispel:! channel t))\n           (calispel:! channel nil)))\n     (lambda (e) (echo-warning \"Cannot cut: ~a\" e)))\n    (if (calispel:? channel)\n        (trivial-clipboard:text)\n        nil)))\n\n(defmethod ffi-buffer-select-all ((gtk-buffer gtk-buffer))\n  (webkit:webkit-web-view-can-execute-editing-command\n   (gtk-object gtk-buffer) webkit2:+webkit-editing-command-select-all+\n   (lambda (can-execute?)\n     (when can-execute?\n       (webkit:webkit-web-view-execute-editing-command\n        (gtk-object gtk-buffer) webkit2:+webkit-editing-command-select-all+)))\n   (lambda (e) (echo-warning \"Cannot select all: ~a\" e))))\n\n(defmethod ffi-buffer-undo ((gtk-buffer gtk-buffer))\n  (webkit:webkit-web-view-can-execute-editing-command\n   (gtk-object gtk-buffer) webkit2:+webkit-editing-command-undo+\n   (lambda (can-execute?)\n     (when can-execute?\n       (webkit:webkit-web-view-execute-editing-command\n        (gtk-object gtk-buffer) webkit2:+webkit-editing-command-undo+)))\n   (lambda (e) (echo-warning \"Cannot undo: ~a\" e))))\n\n(defmethod ffi-buffer-redo ((gtk-buffer gtk-buffer))\n  (webkit:webkit-web-view-can-execute-editing-command\n   (gtk-object gtk-buffer) webkit2:+webkit-editing-command-redo+\n   (lambda (can-execute?)\n     (when can-execute?\n       (webkit:webkit-web-view-execute-editing-command\n        (gtk-object gtk-buffer) webkit2:+webkit-editing-command-redo+)))\n   (lambda (e) (echo-warning \"Cannot redo: ~a\" e))))\n"
  },
  {
    "path": "source/renderer-script.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt)\n\n(export-always 'ps-eval)\n(defmacro ps-eval (&body args)\n  \"Generate the JavaScript code and run it right away.\n\nIf :ASYNC is provided as T before the body, then the code is run asynchronously.\nIf :BUFFER is provided before the body, the code is evaluated in the provided\nbuffer, instead of the default `current-buffer'.\n\nThe body of the code is expanded in the implicit `ps:ps'.\n\nReturns the transformed result of evaluating JavaScript code or NIL if :ASYNC.\n\nExamples:\n;; Set input in the `current-prompt-buffer' asynchronously.\n\\(ps-eval :buffer (current-prompt-buffer) :async t\n  (setf (ps:@ (nyxt/ps:qs document \\\"#input\\\") value) \\\"foo\\\"))\n\n;; Get the class of the active element in the `current-buffer'\n\\(ps-eval (ps:@ document active-element class-name))\"\n  (let ((async-p (second (member :async args)))\n        (buffer (second (member :buffer args))))\n    `(progn\n       (,(if async-p\n             'ffi-buffer-evaluate-javascript-async\n             'ffi-buffer-evaluate-javascript)\n        ,(or buffer '(current-buffer))\n        (ps:ps ,@(loop for index below (length args)\n                       for arg = (nth index args)\n                       when (member arg '(:buffer :async))\n                         do (incf index 1)\n                       else collect arg)))\n       ;; Return nil on async invocations.\n       ,@(when async-p '(nil)))))\n\n(export-always 'define-parenscript)\n(defmacro define-parenscript (script-name args &body script-body)\n  \"Define parenscript method SCRIPT-NAME with arguments ARGS.\nSCRIPT-BODY must be a valid parenscript and will be wrapped in `ps:ps'.\nAny Lisp expression must be wrapped in `ps:lisp'.\n\nThe compiled Javascript runs in the current buffer.\n\nSince it is defined via `defmethod', it is extensible via method\nqualifiers (`:before', `:after', `:around').\"\n  (multiple-value-bind (body declarations documentation)\n      (alex:parse-body script-body :documentation t)\n    (declare (ignore declarations))\n    `(defmethod ,script-name ,args\n       ,documentation\n       (ps-eval :buffer (current-buffer) ,@body))))\n\n(export-always 'define-parenscript-async)\n(defmacro define-parenscript-async (script-name args &body script-body)\n  \"Like `define-parenscript', but Javascript runs asynchronously.\"\n  (multiple-value-bind (body declarations documentation)\n      (alex:parse-body script-body :documentation t)\n    (declare (ignore declarations))\n    `(defmethod ,script-name ,args\n       ,documentation\n       (ps-eval :async t :buffer (current-buffer) ,@body))))\n\n(export-always 'ps-labels)\n(defmacro ps-labels (&body args)\n  \"Create `labels'-like Parenscript functions callable from Lisp.\nARGS can start with :ASYNC and :BUFFER keyword args.\n- :BUFFER is the buffer to run the created functions in. Defaults to\n  `current-buffer'.\n- :ASYNC is whether the function runs asynchronously. Defaults to NIL, so the\n  bound functions return the result of JS evaluation synchronously.\n\nBindings are similar to the `labels'/`flet' bindings. They have a structure of:\n\\(NAME [:BUFFER BUFFER] [:ASYNC BOOLEAN] ARGS\n   &BODY BODY)\n\nBinding-specific :BUFFER and :ASYNC can override the `pl-labels'-global :BUFFER\nand :ASYNC.\n\nExample:\n\\(ps-labels\n  :buffer some-buffer\n  :async t ;; Run functions asynchronously by default.\n  ((print-to-console\n    ;; Override the buffer to current one.\n    :buffer (current-buffer)\n    (something)\n    ;; Notice the `ps:lisp': args are Lisp values.\n    (ps:chain console (log (ps:stringify (ps:lisp something)))))\n   (add\n    ;; Override the :ASYNC for the function to be synchronous.\n    :async nil\n    (n1 n2)\n    (+ (ps:lisp n1) (ps:lisp n2))))\n  (print-to-console (add 5 200.8)))\"\n  (let* ((global-buffer (second (member :buffer args)))\n         (global-async (second (member :async args)))\n         (functions (find-if (lambda (e) (and (listp e) (every #'listp e)))\n                             args))\n         (body (rest (member functions args))))\n    (flet ((transform-definition (name args)\n             (let ((buffer (if (member :buffer args)\n                               (second (member :buffer args))\n                               global-buffer))\n                   (async-p (if (member :async args)\n                                (second (member :async args))\n                                global-async))\n                   (args (loop for index below (length args)\n                               for arg = (nth index args)\n                               when (member arg '(:buffer :async))\n                                 do (incf index 1)\n                               else collect arg)))\n               `(,name ,(first args)\n                       (,(if async-p\n                             'ffi-buffer-evaluate-javascript-async\n                             'ffi-buffer-evaluate-javascript)\n                        ,(or buffer '(current-buffer))\n                        (ps:ps ,@(rest args)))\n                       ;; Return nil on async invocations.\n                       ,@(when async-p '(nil))))))\n      `(labels ,(loop for (name . args) in functions\n                      collect (transform-definition name args))\n         ,@body))))\n\n(define-parenscript %document-scroll-position (&optional (y 0 y-provided-p) (x 0 x-provided-p))\n  (let ((x (ps:lisp x))\n        (y (ps:lisp y)))\n    (if (or (ps:lisp x-provided-p) (ps:lisp y-provided-p))\n        (ps:chain window (scroll-to x y))\n        (list (ps:chain window page-y-offset)\n              (ps:chain window page-x-offset)))))\n\n(export-always 'document-scroll-position)\n(defmethod document-scroll-position (&optional (buffer (current-buffer)))\n  \"Get current scroll position or set it.\nIf passed no arguments, return a list of two elements: vertical (Y) and\nhorizontal (X) offset.\nIf `setf'-d to a single value (or a single list) -- set Y to it.\nIf `setf'-d to a list of two values -- set Y to `first' and X to `second' element.\"\n  (with-current-buffer buffer\n    (let ((position (%document-scroll-position)))\n      (when (listp position)\n        position))))\n\n(defmethod (setf document-scroll-position) (value &optional (buffer (current-buffer)))\n  (when value\n    (with-current-buffer buffer\n      (destructuring-bind (y &optional x)\n          (uiop:ensure-list value)\n        (%document-scroll-position y x)))))\n\n(export-always 'document-get-paragraph-contents)\n(define-parenscript document-get-paragraph-contents (&key (limit 100000))\n  \"Get all the <p> elements text.\"\n  (let ((result \"\"))\n    (loop for element in (nyxt/ps:qsa document (list \"p\"))\n          do (setf result (+ result\n                             (ps:chain element text-content))))\n    (ps:chain result (slice 0 (ps:lisp limit)))))\n\n(export-always 'add-stylesheet)\n(defun add-stylesheet (id style &optional (buffer (current-buffer)))\n  \"Set STYLE of element featuring ID.\"\n  (ps-eval :async t :buffer buffer\n    (unless (nyxt/ps:qs document (ps:lisp (str:concat \"#\" id)))\n      (ps:try\n       (ps:let ((style-element (ps:chain document (create-element \"style\"))))\n         (setf (ps:@ style-element id) (ps:lisp id))\n         (ps:chain document head (append-child style-element))\n         (setf (ps:chain style-element inner-text) (ps:lisp style)))\n       (:catch (error))))))\n\n(defun html-write (html-document &optional (buffer (current-buffer)))\n  \"Set BUFFER's document to HTML-DOCUMENT.\nOverwrites the whole HTML document (head and body elements included).\"\n  ;; Don't use document.write().\n  ;; See https://developer.mozilla.org/en-US/docs/Web/API/Document/write.\n  (ps-eval :async t :buffer buffer\n    (setf (ps:chain document (get-elements-by-tag-name \"html\") 0 |innerHTML|)\n          (ps:lisp html-document))))\n\n(defun html-set (content &optional (buffer (current-buffer)))\n  \"Set BUFFER contents to CONTENT.\"\n  (ps-eval :async t :buffer buffer\n    (setf (ps:@ document body |innerHTML|) (ps:lisp content))))\n\n(defun html-set-style (style-string &optional (buffer (current-buffer)))\n  (let ((style (spinneret:with-html-string (:style (:raw style-string)))))\n    (ps-eval :async t :buffer buffer\n      (ps:chain document body (|insertAdjacentHTML| \"afterbegin\" (ps:lisp style))))))\n\n(eval-always\n  (defvar *nyxt-url-commands* (make-hash-table) ; TODO: Rename to `*internal-pages-command-list*'.\n    \"A map from allowed nyxt: URLs symbols to the functions that generate code of\n  the pages related to these commands.\"))\n\n(defun internal-page-symbol-p (sym)\n  (gethash sym *nyxt-url-commands*))\n\n(deftype internal-page-symbol ()\n  \"Whether the value is a symbol having an `internal-page' associated to it.\"\n  `(and symbol (satisfies internal-page-symbol-p)))\n\n(export-always 'match-internal-page)\n(defun match-internal-page (symbol)\n  \"Return a predicate for URL designators matching the page of SYMBOL name.\"\n  (lambda (url) (eq (internal-page-name url) symbol)))\n\n(define-class internal-page (command)\n  ((dynamic-title ; Not `title' so that it does not clash with other `title' methods.\n    \"\"\n    :initarg :title\n    :accessor nil\n    :type (or string function)\n    :documentation \"If a function, it is called with the internal page arguments\nand must return a string.\")\n   (page-mode\n    nil\n    :export t\n    :type symbol\n    :documentation \"The mode that's specific to a nyxt:// page.\nIt's automatically enabled when the page is loaded and disabled when another URL\nis loaded.\")\n   (form\n    nil\n    :initarg nil\n    :writer nil\n    :reader t\n    :type (maybe function)\n    :documentation \"Function that returns HTML content when a nyxt:// URL is\ninvoked.\nThe nyxt:// URL query arguments are passed to this function as keyword arguments.\"))\n  (:metaclass closer-mop:funcallable-standard-class)\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:documentation \"Each instance is a unique internal page generator for the\nnyxt:// URL scheme.\n\nRegister a new nyxt:// URL under NAME.\nWhen loaded, BODY is run to populate the page content.\n\nWARNING: Don't run anything sensitive in the BODY as any third-party page can\nload nyxt:// URLs.\n\nBODY should end with a form returning the HTML body as a string.\n\nARGLIST is arguments for the underlying page-generating\nfunction. Any argument from it is safe to use in the body of this macro.\nBeware: the ARGLIST should have nothing but keyword arguments because it's\nmapped to the URL query parameters.\nOnly Lisp values that can be converted to JavaScript with\n`webkit:lisp-to-jsc-value' are accepted.\n\nSee `find-internal-page-buffer'.\"))\n\n(defmethod (setf form) (lambda-expression (page internal-page))\n  (let ((arglist (second lambda-expression)))\n    (multiple-value-bind (required optional rest keyword allow-other-keys-p aux key-p)\n        (alex:parse-ordinary-lambda-list arglist)\n      (declare (ignore rest keyword allow-other-keys-p key-p))\n      (when (or required optional aux)\n        (error \"Only rest and keyword parameters are allowed in an internal-page definition.\"))\n      (setf (slot-value page 'form)\n            (lambda (&rest args)\n              (let ((*print-pretty* nil)\n                    (%buffer% (getf args :%buffer%))\n                    (contents (apply (compile nil lambda-expression) args)))\n                (spinneret:with-html-string\n                  (:doctype)\n                  (:html\n                   (:head\n                    (:title (apply #'dynamic-title\n                                   (gethash (name page) *nyxt-url-commands*)\n                                   args))\n                    (:style (:raw (style %buffer%))))\n                   (:body (:raw contents))))))))))\n\n(defmethod set-internal-page-method ((page internal-page) form)\n  (when form\n    (let* ((arglist (second form))\n           (arglist-filtered (remove '%buffer% arglist))\n           (body (cddr form))\n           (documentation (nth-value 2 (alex:parse-body body :documentation t))))\n      (multiple-value-bind (required optional rest keywords allow-other-keys-p aux key-p)\n          (alex:parse-ordinary-lambda-list arglist-filtered)\n        (declare (ignore required optional allow-other-keys-p aux key-p))\n        (closer-mop:ensure-method\n         page\n         `(lambda (,@(unless rest '(&rest args)) ,@arglist)\n            ,@(when documentation (list documentation))\n            (declare (ignorable %buffer% ,@(mappend #'cdar keywords) ,(or rest 'args)))\n            (funcall #'buffer-load-internal-page-focus\n                     (name ,page)\n                     ,@(mappend #'first keywords))))))))\n\n(defmethod initialize-instance :after\n    ((page internal-page) &key form &allow-other-keys)\n  \"Register PAGE into the globally known nyxt:// URLs.\"\n  (when form\n    (set-internal-page-method page form)\n    (setf (form page) form))\n  (setf (gethash (name page) *nyxt-url-commands*) page))\n\n(defmethod reinitialize-instance :after\n    ((page internal-page) &key form &allow-other-keys)\n  \"Register PAGE into the globally known nyxt:// URLs.\"\n  (when form\n    (set-internal-page-method page form)\n    (setf (form page) form))\n  (setf (gethash (name page) *nyxt-url-commands*) page))\n\n(defmethod dynamic-title ((page internal-page) &rest args)\n  (with-slots ((title dynamic-title)) page\n    (cond ((stringp title) title)\n          ((functionp title) (apply title args))\n          (t (format nil \"*~a*\" (string-downcase (name page)))))))\n\n(defun find-internal-page-buffer (name)\n  \"Return first buffer which URL is a NAME `internal-page'.\"\n  (find name (buffer-list) :key (compose #'internal-page-name #'url)))\n\n(-> find-url-internal-page ((or quri:uri string null)) (or internal-page null))\n(defun find-url-internal-page (url)\n  \"Return the `internal-page' corresponding to URL.\"\n  (gethash (internal-page-name url) *nyxt-url-commands*))\n\n(export-always 'buffer-load-internal-page-focus)\n(defun buffer-load-internal-page-focus (name &rest args)\n  \"Return internal page with name NAME and focus it.\nARGS are passed as internal page parameters.\"\n  (set-current-buffer (ffi-buffer-load\n                       (or (find-internal-page-buffer name)\n                           (make-instance 'web-buffer))\n                       (quri:uri (apply #'nyxt-url name args)))))\n\n(defun ensure-keyword-argument (lambda-list keyword)\n  (unless (member keyword lambda-list)\n    (if (member '&key lambda-list)\n        ;; If &key exists, add the keyword argument at the end\n        (append lambda-list (list keyword))\n        ;; If &key doesn't exist, add it with the keyword\n        (append lambda-list (list '&key keyword)))))\n\n(export-always 'define-internal-page)\n(defmacro define-internal-page (name (&rest form-args) (&rest initargs) &body body)\n  \"Define an `internal-page'.\nFORM-ARGS are the `internal-page' `form' keyword arguments.\nINITARGS are passed to the `internal-page' initialization arguments.\n\nExample:\n\n\\(define-internal-page my-page (&key arg1 arg2)\n  (:title \\\"My beautiful page\\\")\n  ...)\"\n  (let ((arglist-with-buffer (ensure-keyword-argument form-args '%buffer%)))\n    `(apply #'make-instance 'internal-page\n            :name ',name\n            :visibility :anonymous\n            :lambda-list ',arglist-with-buffer\n            :form (quote (lambda (,@arglist-with-buffer)\n                           (declare (ignorable %buffer%))\n                           ,@body))\n            (list ,@initargs))))\n\n(export-always 'define-internal-page-command)\n(defmacro define-internal-page-command\n    (name (&rest arglist) (buffer-var title &optional mode) &body body)\n  \"Define a command called NAME creating an `internal-page'.\"\n  (multiple-value-bind (stripped-body declarations documentation)\n      (alex:parse-body body :documentation t)\n    (let ((arglist-with-buffer (ensure-keyword-argument arglist '%buffer%)))\n      `(progn\n         (export-always ',name (symbol-package ',name))\n         (sera:lret ((gf (defgeneric ,name (,@(generalize-lambda-list arglist-with-buffer))\n                           (:documentation ,documentation)\n                           (:generic-function-class internal-page))))\n           (let ((wrapped-body '(lambda (,@arglist-with-buffer)\n                                 ,@(when documentation (list documentation))\n                                 ,@declarations\n                                 (let ((,buffer-var %buffer%))\n                                   (declare (ignorable ,buffer-var))\n                                   ,@stripped-body))))\n             (set-internal-page-method gf wrapped-body)\n             (setf (slot-value #',name 'visibility) :mode)\n             (setf (page-mode #',name) ,mode)\n             (setf (slot-value #',name 'dynamic-title)\n                 ,(if (stringp title)\n                      title\n                      (let ((keywords (nth-value 3 (alex:parse-ordinary-lambda-list arglist-with-buffer)))\n                            (rest (nth-value 2 (alex:parse-ordinary-lambda-list arglist-with-buffer))))\n                        `(lambda (,@(unless (member '&rest arglist-with-buffer)\n                                      '(&rest args))\n                                  ,@arglist-with-buffer)\n                           (declare (ignorable ,@(mappend #'cdar keywords) ,(or rest 'args)))\n                           ,title))))\n             (setf (form gf) wrapped-body)))))))\n\n\n(export-always 'define-internal-page-command-global)\n(defmacro define-internal-page-command-global\n    (name (&rest arglist) (buffer-var title &optional mode) &body body)\n  \"Define a global command called NAME creating an `internal-page'.\"\n  `(prog1 (define-internal-page-command ,name (,@arglist)\n              (,buffer-var ,title ,mode) ,@body)\n     (setf (slot-value #',name 'visibility) :global)))\n"
  },
  {
    "path": "source/renderer.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt)\n\n(define-class renderer ()\n  ((name \"Default\"))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:documentation \"Specialize this class and bind an instance to `*renderer*' to set the default renderer.\"))\n\n(export-always 'install)\n(defgeneric install (renderer)\n  (:documentation \"Setup for renderer.  This may have side effects.\nSee also `uninstall'.\"))\n\n(export-always 'uninstall)\n(defgeneric uninstall (renderer)\n  (:documentation \"Revert the side effects induced by `install'.\"))\n"
  },
  {
    "path": "source/search-engine.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt)\n\n(define-class search-engine ()\n  ((name\n    (alex:required-argument 'name)\n    :type string\n    :documentation \"Name of the search engine.\")\n   (shortcut\n    (alex:required-argument 'shortcut)\n    :type string\n    :documentation \"Alternative shorter name of the search engine.\nUseful for commands such as `set-url', whose prompt buffer sources include\n`url-or-query-source'.\")\n   ;; An alternative to control strings is to leverage `quri:uri-query-params'.\n   (control-url\n    (alex:required-argument 'control-url)\n    :type string\n    :documentation \"Format string to request search queries.\")\n   (control-completion-url\n    nil\n    :type (maybe string)\n    :documentation \"Format string to request search query suggestions.\nWhen nil, search suggestions aren't computed.\"))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:documentation \"A representation of search engines.\n\nSee configuration slots `search-engines' and `search-engine-suggestions-p'.\"))\n\n(defmethod print-object ((search-engine search-engine) stream)\n  (print-unreadable-object (search-engine stream :type t)\n    (format stream \"~a\" (name search-engine))))\n\n(defmethod format-url (query (search-engine search-engine))\n  (format nil (control-url search-engine) (quri:url-encode query)))\n\n(defmethod format-completion-url (query (search-engine search-engine))\n  (format nil (control-completion-url search-engine) (quri:url-encode query)))\n\n(defmethod format-query (query (search-engine search-engine))\n  (format nil \"~a ~a\" (shortcut search-engine) query))\n\n(defgeneric suggestions (query search-engine)\n  (:method (query (search-engine search-engine))\n    (log:debug \"Search suggestions aren't supported for ~a.\" (name search-engine)))\n  (:documentation \"Return a list of search suggestions based on QUERY.\"))\n\n(defmethod prompter:object-attributes ((engine search-engine) (source prompter:source))\n  (declare (ignore source))\n  `((\"Name\" ,(name engine) (:width 3))\n    (\"Shortcut\" ,(shortcut engine) (:width 1))))\n\n(define-class search-engine-source (prompter:source)\n  ((prompter:name \"Search engines\")\n   (prompter:constructor (search-engines *browser*))\n   (prompter:filter-preprocessor #'prompter:filter-exact-matches))\n  (:documentation \"Source listing all the search engines.\"))\n\n(define-command query-selection-in-search-engine (&key (query-in-new-buffer-p t))\n  \"Search selected text using the queried search engine.\nWhen QUERY-IN-NEW-BUFFER-P is non-nil, open the results in a new buffer.\"\n  (ffi-buffer-load\n   (if query-in-new-buffer-p (make-buffer-focus) (current-buffer))\n   (format-url (ffi-buffer-copy (current-buffer))\n               (prompt1 :prompt \"Search engine\"\n                        :sources 'search-engine-source))))\n\n(define-class ddg-search-engine (search-engine)\n  ((name \"DuckDuckGo\")\n   (shortcut \"ddg\")\n   (control-url \"https://duckduckgo.com/?q=~a\")\n   (control-completion-url \"https://duckduckgo.com/ac/?q=~a\"))\n  (:export-class-name-p t)\n  (:documentation \"A representation of the DuckDuckGo search engine.\"))\n\n(defun request (url) (j:decode (dex:get url)))\n\n(defmethod suggestions (query (ddg ddg-search-engine))\n  \"Return a list of search suggestions based on QUERY.\"\n  (unless (str:blankp query)\n    (map 'list\n         (lambda (hash-table) (first (alex:hash-table-values hash-table)))\n         (request (format-completion-url query ddg)))))\n\n(define-class wikipedia-search-engine (search-engine)\n  ((name \"Wikipedia\")\n   (shortcut \"wiki\")\n   (control-url \"https://en.wikipedia.org/w/index.php?search=~a\")\n   (control-completion-url \"https://en.wikipedia.org/w/api.php?action=opensearch&format=json&search=~a\"))\n  (:export-class-name-p t)\n  (:documentation \"A representation of the Wikipedia search engine.\"))\n\n(defmethod suggestions (query (wikipedia wikipedia-search-engine))\n  \"Return a list of search suggestions based on QUERY.\"\n  (unless (str:blankp query)\n    (coerce (j:get 1 (request (format-completion-url query wikipedia)))\n            'list)))\n"
  },
  {
    "path": "source/spinneret-tags.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :spinneret)\n\n(defun %nstyle-body (forms)\n  (reduce #'uiop:strcat\n          (mapcar (lambda (f) (typecase f\n                                (list (lass:compile-and-write f))\n                                (string f)))\n                  forms)))\n\n(deftag :nstyle (body attrs &key &allow-other-keys)\n  \"Regular <style>, but with contents staying unescaped.\nForms in BODY can be:\n- Literal strings --- get concatenated into a stylesheet as is.\n- Lists --- get processed by LASS' `lass:compile-and-write' and then\n  concatenated.\"\n  `(:style ,@attrs (:raw (theme:with-theme (nyxt::theme nyxt:*browser*)\n                           (%nstyle-body (list ,@body))))))\n\n(defun %nscript-body (forms)\n  (reduce #'uiop:strcat\n          (mapcar (lambda (f) (typecase f\n                                (list (ps:ps* f))\n                                (string f)))\n                  forms)))\n\n(deftag :nscript (body attrs &key &allow-other-keys)\n  \"Regular <script>, but with contents staying unescaped.\nForms in BODY can be:\n- Literal strings --- get concatenated into a stylesheet as is.\n- Lists --- get processed by Parenscript's `ps:ps*' and then concatenated.\"\n  `(:script ,@attrs (:raw (%nscript-body (list ,@body)))))\n\n(serapeum:-> %nselect-onchange (string (nyxt:maybe nyxt:buffer) (nyxt:list-of list)) t)\n(defun %nselect-onchange (id buffer clauses)\n  \"Compiles the CLAUSES body into Parenscript code.\nParenscript fetches values from <select> with ID and evaluates the respective\nforms in BUFFER.\"\n  (alexandria:when-let ((buffer (or buffer (nyxt:current-buffer))))\n    (ps:ps*\n     (with-ps-gensyms (var inner-var)\n       `(nyxt/ps:lisp-eval\n         (:title ,(format nil \"nselect ~a choice\" id)\n                 ,@(when buffer\n                     `(:buffer ,buffer)))\n         (let ((,var (alexandria:ensure-list\n                      (nyxt:ps-eval\n                        (ps:chain *Array (from (ps:chain (nyxt/ps:qs document (+ \"#\" (ps:lisp ,id)))\n                                                         selected-options))\n                                  (map (lambda (e) (ps:@ e value))))))))\n           (dolist (,inner-var ,var)\n             (str:string-case ,inner-var\n               ,@(loop for (clause . forms) in clauses\n                       for value = (first (uiop:ensure-list clause))\n                       collect (cons (nyxt:prini-to-string value)\n                                     forms))))))))))\n\n(serapeum:-> %nselect-options ((nyxt:list-of list)) t)\n(defun %nselect-options (clauses)\n  \"Produces a set of options for :nselect based on CLAUSES list.\"\n  (spinneret:with-html-string\n    (loop for (value display title) in (mapcar (alexandria:compose #'uiop:ensure-list\n                                                                   #'first)\n                                               clauses)\n          collect (:option\n                   :value (nyxt:prini-to-string value)\n                   (when title (list :title title))\n                   (string-capitalize (or display (nyxt:prini-to-string value)))))))\n\n(deftag :nselect (body attrs &key default (id (alexandria:required-argument 'id))\n                       buffer &allow-other-keys)\n  \"Generate <select> tag from the BODY resembling cond clauses.\n\nBODY is a list of forms, each given by ((VALUE DISPLAY TITLE) . FORMS).\nIt expands to <option value=\\\"value\\\" title=\\\"title\\\">display</option> and runs\nFORMS when selected. DISPLAY and TITLE are optional literal strings.\n\nIn both cases, VALUEs should be literal (and printable) atoms. For instance,\nsymbol, number, string, or keyword.\n\nBUFFER is a buffer to bind the actions of this tag to.\n\nDEFAULT is a form evaluating to a string that is used as the default option.\n\nIn case some variable from the outer scope should be captured, injecting a\nclosure into the clause would work best.\n\nExample:\n\\(:nselect :id \\\"number-guessing\\\"\n  :default \\\"Guess the number\\\"\n  '(1 (nyxt:echo \\\"Too low!\\\"))\n  (list 2 (nyxt:echo \\\"Correct!\\\"))\n  `(3 (funcall ,(lambda () (nyxt:echo \\\"Too high!\\\")))))\"\n  (once-only (id)\n    (with-gensyms (body-var)\n      `(let ((,body-var ,@body))\n         (:select.button\n          ,@attrs\n          :id ,id\n          :onchange (%nselect-onchange ,id ,buffer ,body-var)\n          ,@(when default\n              `((:option :selected t :disabled t ,default)))\n          (:raw (%nselect-options ,body-var)))))))\n\n(serapeum:-> %nradio-onchange (list (nyxt:maybe nyxt:buffer)) t)\n(defun %nradio-onchange (onchange buffer)\n  \"Produce Parenscript to run ONCHANGE expression in BUFFER when :nradio is modified.\"\n  (when (or buffer (nyxt:current-buffer))\n    (ps:ps*\n     `(nyxt/ps:lisp-eval\n       (:title \"nradio onchange\"\n               ,@(when buffer\n                   (list :buffer buffer)))\n       ,onchange))))\n\n(serapeum:-> %nradio-inputs (string (nyxt:maybe nyxt:buffer) symbol boolean (nyxt:list-of list)) t)\n(defun %nradio-inputs (name buffer checked vertical clauses)\n  (spinneret:with-html-string\n          (loop for (id label body) in (mapcar #'uiop:ensure-list clauses)\n                collect (:label\n                         :class \"radio-label\"\n                         (:input\n                          :class \"radio-input\"\n                          :type \"radio\"\n                          :id (nyxt:prini-to-string id)\n                          :onchange (%nradio-onchange body buffer)\n                          :name name\n                          :checked (equal id checked))\n                         label (when vertical (:br))))))\n\n(deftag :nradio (body attrs &key (name (alexandria:required-argument 'name)) checked vertical buffer &allow-other-keys)\n  \"Generate radio buttons corresponding to clauses in BODY.\nClauses should be of the form (ID LABEL . FORM), where FORM is evaluated\nwhen a radio button is selected.\"\n  (let ((attrs attrs))\n    ;; Hack to silence the fact that attrs is not used.\n    (declare (ignore attrs))\n    (with-gensyms (body-var)\n      `(let ((,body-var ,(if (serapeum:single body)\n                             (first body)\n                             `(list ,@body))))\n         (:div\n          :class \"radio-div\"\n          (:raw (%nradio-inputs ,name ,buffer ,checked ,vertical ,body-var)))))))\n\n(serapeum:-> %ncheckbox-onchange (list list (nyxt:maybe nyxt:buffer)) t)\n(defun %ncheckbox-onchange (checked-body unchecked-body buffer)\n  \"Produce Parenscript to run ONCHANGE expression in BUFFER when :ncheckbox is modified.\"\n  (when (or buffer (nyxt:current-buffer))\n    (ps:ps*\n     `(if (ps:chain window event target checked)\n         (nyxt/ps:lisp-eval\n           (:title \"ncheckbox checked\"\n                   ,@(when buffer\n                       (list :buffer buffer)))\n           ,checked-body)\n         (nyxt/ps:lisp-eval\n           (:title \"ncheckbox unchecked\"\n                   ,@(when buffer\n                       (list :buffer buffer)))\n           ,unchecked-body)))))\n\n(serapeum:-> %ncheckbox-inputs (string (nyxt:maybe nyxt:buffer) boolean list) t)\n(defun %ncheckbox-inputs (name buffer checked body)\n  (destructuring-bind ((id label) checked-body unchecked-body)\n      (mapcar #'uiop:ensure-list body)\n    (spinneret:with-html-string\n      (:input\n       :class \"checkbox-input\"\n       :type \"checkbox\"\n       :id (nyxt:prini-to-string id)\n       :onchange (%ncheckbox-onchange checked-body unchecked-body buffer)\n       :name name\n       :checked checked)\n      (:label\n       :class \"checkbox-label\"\n       :for name\n       label))))\n\n(deftag :ncheckbox (body attrs &key (name (alexandria:required-argument 'name)) checked buffer &allow-other-keys)\n  \"Generate a checkbox corresponding to BODY.\nBODY should be of the form (ID LABEL FORM-CHECKED . FORM-UNCHECKED), where FORM-CHECKED is evaluated\nwhen the checkbox is checked and FORM-UNCHECKED when it is unchecked.\"\n  (let ((attrs attrs))\n    ;; Hack to silence the fact that attrs is not used.\n    (declare (ignore attrs))\n    (with-gensyms (body-var)\n      `(let* ((,body-var ,(if (serapeum:single body)\n                              (first body)\n                              `(list ,@body))))\n         (:div\n          :class \"checkbox-div\"\n          (:raw (%ncheckbox-inputs ,name ,buffer ,checked ,body-var)))))))\n\n(defun %nxref-doc (type symbol &optional (class-name (when (eq type :slot)\n                                                       (alexandria:required-argument 'class-name))))\n  \"NOTE: TYPE for classes is :CLASS, not :CLASS-NAME (as in `:nxref').\"\n  (format nil \"[~a]~@[ ~a~]\"\n          (if class-name\n              (format nil \"SLOT of ~a\" class-name)\n              type)\n          (when-let ((doc (case type\n                            (:package (documentation (find-package symbol) t))\n                            (:variable (documentation symbol 'variable))\n                            ((:slot   ; KLUDGE: Any simple way to get slot docs?\n                              :macro :function :command)\n                             (documentation symbol 'function))\n                            ((:mode :class)\n                             (documentation symbol 'type)))))\n            ;; Copied from describe.lisp to avoid `nyxt::first-line' use.\n            (find-if (complement #'uiop:emptyp) (serapeum:lines doc)))))\n\n(defun %nxref-link (type symbol &optional (class-name (when (eq type :slot)\n                                                        (alexandria:required-argument 'class-name))))\n  \"Generate a nyxt: link to the describe-* page based on SYMBOL's TYPE.\nCLASS-NAME is specific to :slot type.\"\n  (case type\n    (:package (nyxt:nyxt-url (read-from-string \"nyxt:describe-package\") :package symbol))\n    (:variable (nyxt:nyxt-url (read-from-string \"nyxt:describe-variable\")\n                              :variable symbol))\n    ((:command :function :macro)\n     (nyxt:nyxt-url (read-from-string \"nyxt:describe-function\")\n                    :fn symbol))\n    (:slot (nyxt:nyxt-url (read-from-string \"nyxt:describe-slot\")\n                          :name symbol :class class-name))\n    ((:mode :class)\n     (nyxt:nyxt-url (read-from-string \"nyxt:describe-class\")\n                    :class symbol))\n    (t (nyxt:nyxt-url (read-from-string \"nyxt:describe-any\")\n                      :input symbol))))\n\n(deftag :nxref (body attrs &key slot mode class-name function macro command (command-key-p t) variable package (target \"_self\") &allow-other-keys)\n  \"Create a link to a respective describe-* page for BODY symbol.\n\nRelies on the type keywords (SLOT, MODE, CLASS-NAME, FUNCTION, MACRO, COMMAND,\nVARIABLE, PACKAGE, TARGET) to guess the right page, always provide those.\n\nCLASS-NAME, if present, should be the symbol designating a class. It's not\ncalled CLASS because Spinneret has special behavior for CLASS pre-defined and\nnon-overridable.\"\n  (let* ((first (first body))\n         (symbol (or package variable function macro command slot class-name mode\n                     (when (symbolp first) first)))\n         (printable (or (when (and (symbolp first) (eq first symbol))\n                          (second body))\n                        first package variable function macro command slot class-name mode))\n         (type (cond\n                 (package :package)\n                 (variable :variable)\n                 (macro :macro)\n                 (command :command)\n                 (function :function)\n                 ((and slot class-name) :slot)\n                 (mode :mode)\n                 (class-name :class))))\n    (when (and (getf attrs :class)\n               (or (getf attrs :slot)\n                   (every #'null (list slot class-name mode function macro command variable package))))\n      (error \":class attribute used ambiguously in :nxref tag. Use :class-name instead.\"))\n    `(:a.link\n      :target ,target\n      ,@attrs\n      :href (%nxref-link ,type ,symbol\n                         ,@(when (and slot class-name)\n                             (list class-name)))\n      :title (%nxref-doc ,type ,symbol\n                         ,@(when (and slot class-name)\n                             (list class-name)))\n      (:code\n       (let ((*print-escape* nil))\n         (nyxt:prini-to-string ,printable))\n       ,@(when (and command command-key-p)\n           `(\" (\"\n             (funcall (read-from-string \"nyxt::binding-keys\")\n                      ,command ,@(when mode\n                                   `(:modes (cl:list (make-instance ,mode)))))\n             \")\"))))))\n\n(defun %ncode-resolve-linkable-symbols (form)\n  \"Helper function for :NCODE tag.\nReturns all the linkable symbols from FORM as multiple values:\n- Function symbols.\n- Variable symbols.\n- Macro symbols.\n- All the special forms (including some macros and functions needing extra care).\n- All the strings that may potentially be resolvable with\n  `nyxt:resolve-backtick-quote-links'.\"\n  (let ((functions (list))\n        (classes (list))\n        (variables (list))\n        (macros (list))\n        (specials (list))\n        (all-specials '(quote\n                        flet labels symbol-macrolet macrolet\n                        block catch eval-when progv lambda\n                        progn prog1 unwind-protect tagbody setf setq multiple-value-prog1\n                        let let* prog prog*\n                        return-from throw the\n                        multiple-value-call funcall apply\n                        function\n                        go locally))\n        (linkable-strings (list)))\n    (labels ((resolve-symbols-internal (form)\n               (typecase form\n                 (boolean nil)\n                 (keyword nil)\n                 (cons\n                  (let ((first (first form)))\n                    (alexandria:destructuring-case form\n                      ;; More forms: def*, make-instance, slots, special forms?\n                      ((make-instance class &rest args)\n                       (push first functions)\n                       (if (and (listp class)\n                                (eq 'quote (first class)))\n                           (push (second class) classes)\n                           (resolve-symbols-internal class))\n                       (resolve-symbols-internal args))\n                      (((flet labels symbol-macrolet macrolet)\n                        (&rest bindings) &body body)\n                       (push first specials)\n                       (mapcar (lambda (b)\n                                 (resolve-symbols-internal (cddr b)))\n                               bindings)\n                       (mapc #'resolve-symbols-internal body))\n                      (((block catch eval-when progv lambda) arg &body body)\n                       (declare (ignore arg))\n                       (push first specials)\n                       (mapc #'resolve-symbols-internal body))\n                      (((progn prog1 unwind-protect tagbody setf setq multiple-value-prog1)\n                        &body body)\n                       (push first specials)\n                       (mapc #'resolve-symbols-internal body))\n                      (((let let* prog prog*) (&rest bindings) &body body)\n                       (push first specials)\n                       (mapcar (alexandria:compose\n                                #'resolve-symbols-internal #'second #'uiop:ensure-list)\n                               bindings)\n                       (mapc #'resolve-symbols-internal body))\n                      (((return-from throw the) arg &optional value)\n                       (declare (ignore arg))\n                       (push first specials)\n                       (resolve-symbols-internal value))\n                      (((multiple-value-call funcall apply) function &rest args)\n                       (push first specials)\n                       (match function\n                         ((list 'quote name)\n                          (pushnew name functions))\n                         ((list 'function name)\n                          (when (symbolp name)\n                            (pushnew name functions))))\n                       (mapc #'resolve-symbols-internal args))\n                      ((function value)\n                       (push first specials)\n                       (when (symbolp value)\n                         (pushnew value functions)))\n                      (((go locally) &rest values)\n                       (declare (ignore values))\n                       (push first specials))\n                      ((t &rest rest)\n                       (cond\n                         ((listp first)\n                          (resolve-symbols-internal first)\n                          (mapc #'resolve-symbols-internal rest))\n                         ((member first all-specials)\n                          (pushnew first specials))\n                         ((and (symbolp first)\n                               (nsymbols:macro-symbol-p first))\n                          (pushnew first macros)\n                          (let* ((arglist (trivial-arguments:arglist first))\n                                 (rest-position (or (position '&rest arglist)\n                                                    (position '&body arglist))))\n                            (if rest-position\n                                (mapc #'resolve-symbols-internal (nthcdr rest-position rest))\n                                (mapc #'resolve-symbols-internal rest))))\n                         ((and (symbolp first)\n                               (nsymbols:function-symbol-p first))\n                          (pushnew first functions)\n                          (mapc #'resolve-symbols-internal rest)))))))\n                 (symbol\n                  (when (nsymbols:variable-symbol-p form)\n                    (pushnew form variables)))\n                 (string\n                  (pushnew form linkable-strings)))))\n      (resolve-symbols-internal form)\n      (values (set-difference functions all-specials) classes variables macros specials linkable-strings))))\n\n(defun %ncode-prini (object package)\n  \"Custom `:ncode'-specific `nyxt:prini-to-string' with narrower margins.\"\n  (nyxt:prini-to-string object :readably t :right-margin 70 :package package))\n\n(defun %ncode-htmlize-body (form package &optional (listing (%ncode-prini form package)))\n  \"Turn the FORM into an HTMLized rich text, augmented with `:nxref's to the used entities.\nLISTING is the string to enrich, autogenerated from FORM on demand.\"\n  (let ((*suppress-inserted-spaces* t)\n        (*html-style* :tree)\n        (*print-pretty* nil))\n    (when (listp form)\n      (multiple-value-bind (functions classes variables macros specials linkable-strings)\n          (%ncode-resolve-linkable-symbols form)\n        ;; We use \\\\s, because lots of Lisp symbols include non-word\n        ;; symbols and would break if \\\\b was used.\n        (macrolet ((replace-symbol-occurences (symbols type &key (prefix \"(\\\\()\") (suffix \"(\\\\)|\\\\s)\") (style :plain))\n                     (alexandria:with-gensyms (sym sym-listing)\n                       `(dolist (,sym ,symbols)\n                          (when (search (%ncode-prini ,sym package) listing)\n                            (let ((,sym-listing (%ncode-prini ,sym package)))\n                              (setf listing\n                                    (ppcre:regex-replace-all\n                                     (uiop:strcat\n                                      ,prefix (ppcre:quote-meta-chars ,sym-listing) ,suffix)\n                                     listing\n                                     (list\n                                      0 ,(case style\n                                           (:link `(with-html-string\n                                                     (:nxref ,type ,sym ,sym-listing)))\n                                           (:plain `(with-html-string\n                                                      (:nxref :style \"color: inherit; background-color: inherit;\" ,type ,sym ,sym-listing)))\n                                           (:span `(with-html-string\n                                                     (:span.action ,sym-listing))))\n                                      1)))))))))\n          (replace-symbol-occurences macros :macro :style :link)\n          (replace-symbol-occurences functions :function :prefix \"(\\\\(|#'|')\")\n          (replace-symbol-occurences classes :class-name :prefix \"(')\")\n          (replace-symbol-occurences\n           variables :variable :prefix \"(\\\\s)\" :suffix \"(\\\\)|\\\\s)\")\n          (replace-symbol-occurences specials nil :style :span))\n        (dolist (string linkable-strings)\n          (setf listing (str:replace-all (%ncode-prini string package)\n                                         (nyxt:prini-to-string\n                                          (nyxt:resolve-backtick-quote-links string package)\n                                          :escape t :readably t :package package)\n                                         listing)))))\n    listing))\n\n(defun %ncode-htmlize-unless-string (form package)\n  (typecase form\n    (string form)\n    (list (%ncode-htmlize-body form package))))\n\n(defun %ncode-inline-p (body package)\n  \"BODY is only inline if it actually is a one-liner, literal or printed out.\"\n  (and (serapeum:single body)\n       (zerop (count #\\newline\n                     (if (stringp (first body))\n                         (first body)\n                         (%ncode-prini (first body) package))))))\n\n(deftag :ncode (body attrs &key\n                     (package :nyxt)\n                     (inline-p nil inline-provided-p)\n                     (copy-p t) file (external-editor-p file)\n                     &allow-other-keys)\n  \"Generate the <pre>/<code> listing from the provided Lisp BODY.\n\nForms in BODY should be quoted.\n\nINLINE-P is about omitting newlines and <pre> tags---basically a <code> tag with\nsyntax highlighting and actions. If not provided, is determined automatically\nbased on BODY length.\n\nMost *-P arguments mandate whether to add the buttons for:\n- Copying the source to clipboard (COPY-P).\n- Editing the FILE it comes from (if present), in `nyxt:external-editor-program'\n  (EXTERNAL-EDITOR-P).\"\n  (once-only (package)\n    (with-gensyms (body-var inline-var file-var first plaintext htmlized)\n      (let* ((*print-escape* nil)\n             (id (nyxt:prini-to-string (gensym)))\n             (select-code\n               `(:nselect\n                 :id ,id\n                 :class \"code-select\"\n                 :default \"☰\"\n                 :style (if ,inline-var \"font-weight: bold\" \"font-weight: normal\")\n                 (list\n                  ,@(when copy-p\n                      `(`((copy \"Copy\" \"Copy the code to clipboard.\")\n                          (funcall (read-from-string \"nyxt:ffi-buffer-copy\")\n                                   (nyxt:current-buffer) ,,plaintext))))\n                  ,@(when (and file external-editor-p)\n                      `(`((external-editor\n                           \"Open in external editor\"\n                           \"Open the file this code comes from in external editor.\")\n                          (funcall (read-from-string \"nyxt/mode/file-manager:edit-file-with-external-editor\")\n                                   (uiop:ensure-list ,,file-var)))))))))\n        `(let* ((,body-var (list ,@body))\n                (,first (first ,body-var))\n                (,inline-var ,(if inline-provided-p\n                                  inline-p\n                                  `(%ncode-inline-p ,body-var ,package)))\n                (,file-var ,file)\n                (,plaintext (cond\n                              ((and (serapeum:single ,body-var)\n                                    (stringp ,first))\n                               ,first)\n                              ((serapeum:single ,body-var)\n                               (%ncode-prini ,first ,package))\n                              (t (str:join\n                                  (make-string 2 :initial-element #\\newline)\n                                  (mapcar (lambda (f) (if (stringp f)\n                                                          f\n                                                          (%ncode-prini f ,package)))\n                                          ,body-var)))))\n                (,htmlized (if (serapeum:single ,body-var)\n                               (%ncode-htmlize-unless-string ,first ,package)\n                               (str:join\n                                (make-string 2 :initial-element #\\newline)\n                                (mapcar (lambda (f) (%ncode-htmlize-unless-string f ,package)) ,body-var)))))\n           (declare (ignorable ,plaintext ,file-var))\n           ,(if inline-p\n                `(:span (:code ,@attrs (:raw ,htmlized)) ,select-code)\n                `(:div :style \"position: relative\"\n                       (:pre ,@attrs ,select-code\n                             (:code (:raw ,htmlized))))))))))\n\n(defun %nsection-id (title)\n  (if (stringp title)\n      (reduce (lambda (string char)\n                (if (and (plusp (length string))\n                         (eql (elt string (1- (length string))) #\\-)\n                         (eql char #\\-))\n                    string\n                    (uiop:strcat string char)))\n              (substitute-if-not\n               #\\- #'alphanumericp\n               (string-trim serapeum:whitespace (string-downcase title)))\n              :initial-value \"\")\n      (alexandria:required-argument 'id)))\n\n(deftag :nsection (body attrs &key (title (alexandria:required-argument 'title))\n                        level\n                        (anchor-p t)\n                        (open-p t)\n                        (id (if (stringp title)\n                                (str:remove-punctuation (str:downcase title) :replacement \"-\")\n                                (alexandria:required-argument 'id)))\n                        &allow-other-keys)\n  \"Collapsible and reference-able <section> with a neader.\nTITLE should be a human-readable title for a section, or the form producing one.\nLEVEL (if provided), is the level of heading for the section. If it's 2, the\nheading is <h2>, if it's 3, then <h3> etc. If not provided, uses <h*> Spinneret\ntag to intelligently guess the current heading level.\nID is the string identifier with which to reference the section elsewhere. Is\nauto-generated from title by replacing all the punctuation and spaces with\nhyphens, if not provided AND if the TITLE is a string.\nOPEN-P mandates whether the section is collapsed or not. True (= not collapsed)\nby default.\"\n  (check-type level (or null (integer 2 6)))\n  (with-gensyms (id-var)\n    `(let ((spinneret::*html-path*\n             ;; Push as many :section tags into the path, as necessary to imply\n             ;; LEVEL for the sections inside this one. A trick on Spinneret to\n             ;; make it think it's deeply nested already.\n             (append\n              spinneret::*html-path*\n              (make-list ,(if level\n                              `(1- (- ,level (spinneret::heading-depth)))\n                              0)\n                         :initial-element :section)))\n           (,id-var ,id))\n       (:section.section\n        :id ,id-var\n        (:details\n         :open ,open-p\n         (:summary\n          :class \"nsection-summary\"\n          (:header\n           :style \"display: inline\"\n           (:h* :style \"display: inline\"\n             ,@attrs ,title)\n           \" \" (when ,anchor-p\n                 (:a :class \"link nsection-anchor\" :href (uiop:strcat \"#\" ,id-var) \"#\"))))\n         ,@body)))))\n\n(serapeum:-> %nbutton-onclick (string (nyxt:maybe nyxt:buffer) (nyxt:list-of list)) t)\n(defun %nbutton-onclick (title buffer clauses)\n  \"Produce Parenscript to run Lisp CLAUSES in BUFFER.\nTITLE is the debuggable name for the callback.\"\n  (when (or buffer (nyxt:current-buffer))\n    (ps:ps*\n     `(nyxt/ps:lisp-eval\n       (:title ,(format nil \"nbutton ~a\" title)\n               ,@(when buffer\n                   (list :buffer buffer)))\n       ,@clauses))))\n\n(deftag :nbutton (body attrs &key (text (alexandria:required-argument 'text)) title buffer\n                       &allow-other-keys)\n  \"A Lisp-invoking button with TEXT text and BODY action.\nEvaluates (via `nyxt/ps:lisp-eval') the BODY in BUFFER when clicked.\n\nBODY can consist of quoted lists or forms producing those. These will be\ncompiled, so, if you want to close over some value, inject a closure right\ninside the forms.\n\nExample:\n\\(:nbutton\n  :buffer buffer\n  :text \\\"Do something\\\"\n  '(nyxt:echo \\\"Hello!\\\")\n  (list 'foo)\n  `(funcall ,(lambda () (do-something-with closed-over-value))))\"\n  `(:button.button\n    :onclick (%nbutton-onclick ,(or title text) ,buffer (list ,@body))\n    ,@(when title\n        (list :title title))\n    ,@attrs\n    ,text))\n\n(serapeum:-> %ninput-onfocus (list (nyxt:maybe nyxt:buffer)) t)\n(defun %ninput-onfocus (onfocus buffer)\n  \"Produce Parenscript to run ONFOCUS expression in BUFFER when :ninput is focused.\"\n  (when (or buffer (nyxt:current-buffer))\n    (ps:ps*\n     `(nyxt/ps:lisp-eval\n       (:title \"ninput onfocus\"\n               ,@(when buffer\n                   (list :buffer buffer)))\n       ,onfocus))))\n\n(serapeum:-> %ninput-onchange (list (nyxt:maybe nyxt:buffer)) t)\n(defun %ninput-onchange (onchange buffer)\n  \"Produce Parenscript to run ONCHANGE expression in BUFFER when :ninput is modified.\"\n  (when (or buffer (nyxt:current-buffer))\n    (ps:ps*\n     `(nyxt/ps:lisp-eval\n       (:title \"ninput onchange\"\n               ,@(when buffer\n                   (list :buffer buffer)))\n       ,onchange))))\n\n(deftag :ninput (body attrs &key rows cols onfocus onchange buffer &allow-other-keys)\n  \"Nicely styled <textarea> with a reasonable number of ROWS/COLS to accommodate the BODY.\nCalls Lisp forms in ONFOCUS and ONCHANGE when one focuses and edits the input (respectively).\n\nBODY should be a string or an implicit progn producing a string.\n\nONFOCUS, and ONCHANGE can consist of quoted lists or forms producing\nthose. These lists will be compiled, so, if you want to close over some value,\ninject a closure right inside the forms.\"\n  (once-only (buffer)\n    (with-gensyms (input)\n      ;; NOTE: It's unlikely that BODY will have multiple forms, but better\n      ;; prepare for it, just in case someone goes stateful.\n      `(let ((,input (progn ,@body)))\n         (:textarea.input\n          :rows (or ,rows (1+ (count #\\Newline ,input)) 1)\n          :cols (or ,cols (ignore-errors (reduce #'max (mapcar #'length (str:lines ,input)))) 80)\n          ,@(when onfocus\n              `(:onfocus (%ninput-onfocus ,onfocus ,buffer)))\n          ,@(when onchange\n              ;; More events here.\n              `(:onkeydown (%ninput-onchange ,onchange ,buffer)))\n          ,@attrs\n          (:raw (the string ,input)))))))\n\n(serapeum:-> %ntoc-create-toc ((integer 2 6) string) *)\n(defun %ntoc-create-toc (depth body)\n  \"Generate the code for the table of contents based on string BODY.\"\n  (labels ((parent-section (elem)\n             (find-if #'nyxt/dom:section-element-p (nyxt/dom:parents elem)))\n           (format-section (heading level)\n             (with-html-string\n               (let ((parent-section (parent-section heading)))\n                 (:li (:a :href (format nil \"#~a\" (plump:attribute parent-section \"id\"))\n                          (plump:text heading)))\n                 (serapeum:and-let* (((< level depth))\n                                     (inner-level (1+ level))\n                                     (inner-headers\n                                      (clss:ordered-select (format nil \"h~a\" inner-level) parent-section)))\n                   (:ul (loop for inner-header across inner-headers\n                              collect (:raw (format-section inner-header inner-level)))))))))\n    (let* ((dom (nyxt/dom:named-html-parse body))\n           (h2s (clss:ordered-select \"h2\" dom)))\n      (with-html-string\n        (loop for h2 across h2s\n              collect (:ul (:raw (format-section h2 2))))))))\n\n(deftag :ntoc (body attrs &key (title \"Table of contents\") (depth 3) &allow-other-keys)\n  \"Generate table of contents for BODY up to DEPTH.\nLooks for section tags with ID-s to link to.\n:nsection sections are perfectly suitable for that.\"\n  (with-gensyms (body-var)\n    `(let* ((*html-style* :tree)\n            (,body-var (with-html-string ,@body)))\n       (:nav#toc\n        ,@attrs\n        (:nsection\n         :title ,title\n         (:raw (%ntoc-create-toc ,depth ,body-var))))\n       (:raw ,body-var))))\n"
  },
  {
    "path": "source/start.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt)\n\n(define-class socket-file (files:runtime-file nyxt-file)\n  ((files:base-path #p\"nyxt.socket\")\n   (editable-p nil))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:documentation \"Socket files are typically stored in a dedicated directory.\"))\n\n(defmethod files:resolve ((profile nyxt-profile) (socket socket-file))\n  \"Return finalized path for socket files.\"\n  (uiop:ensure-pathname (or (getf *options* :socket) (call-next-method))\n                        :truenamize t))\n\n(export-always '*socket-file*)\n(defvar *socket-file* (make-instance 'socket-file)\n  \"Path of the Unix socket used to communicate between different instances of\nNyxt.\n\nThis path cannot be set from the configuration file because we want to be able\nto set and use the socket without parsing any file.  Instead, the socket can be\nset from the corresponding command line option.\")\n\n(defun handle-malformed-cli-arg (condition)\n  (format t \"Error parsing argument ~a: ~a.~&\" (opts:option condition) condition)\n  (opts:describe)\n  (uiop:quit 0 #+bsd nil))\n\n(eval-always\n  (defun define-opts ()\n    \"Define command line options.\nThis must be called on startup so that code is executed in the user environment\nand not the build environment.\"\n    (opts:define-opts\n      (:name :help\n       :description \"Print this help and exit.\"\n       :short #\\h\n       :long \"help\")\n      (:name :verbose\n       :short #\\v\n       :long \"verbose\"\n       :description \"Print debugging information to stdout.\")\n      (:name :version\n       :long \"version\"\n       :description \"Print version and exit.\")\n      (:name :system-information\n       :long \"system-information\"\n       :description \"Print system information and exit.\")\n      (:name :config\n       :short #\\i\n       :long \"config\"\n       :arg-parser #'identity\n       :description (format nil \"Set path to configuration file.\nDefault: ~s\" (files:expand *config-file*)))\n      (:name :no-config\n       :short #\\I\n       :long \"no-config\"\n       :description \"Do not load the configuration file.\")\n      (:name :auto-config\n       :short #\\c\n       :long \"auto-config\"\n       :arg-parser #'identity\n       :description (format nil \"Set path to auto-configuration file.\nDefault: ~s\" (files:expand *auto-config-file*)))\n      (:name :no-auto-config\n       :short #\\C\n       :long \"no-auto-config\"\n       :description \"Do not load the user auto-configuration file.\")\n      (:name :socket\n       :short #\\s\n       :long \"socket\"\n       :arg-parser #'identity\n       :description \"Set path to socket.\nUnless evaluating remotely (see --remote).\")\n      (:name :eval\n       :short #\\e\n       :long \"eval\"\n       :arg-parser #'identity\n       :description \"Eval the Lisp expressions.  Can be specified multiple times.\nWithout --quit or --remote, the evaluation is done after parsing the config file\n(if any) and before initializing the browser.\")\n      (:name :load\n       :short #\\l\n       :long \"load\"\n       :arg-parser #'identity\n       :description \"Load the Lisp file.  Can be specified multiple times.\nWithout --quit or --remote, the loading is done after parsing the config file\n(if any) and before initializing the browser.\")\n      (:name :quit\n       :short #\\q\n       :long \"quit\"\n       :description \"Quit after --load or --eval.\")\n      (:name :remote\n       :short #\\r\n       :long \"remote\"\n       :description\n       \"Send the --eval and --load arguments to the running instance of Nyxt.\nUnless --quit is specified, also send s-expressions from the standard input.\nThe remote instance must be listening on a socket which you can specify with\n--socket and have the `remote-execution-p' browser slot to non-nil.\")\n      (:name :headless\n       :long \"headless\"\n       :description \"Start Nyxt without showing any graphical element.\nThis is useful to run scripts for instance.\")\n      (:name :electron-opts\n       :long \"electron-opts\"\n       :arg-parser #'identity\n       :description \"Command-line options to pass to Electron\"))))\n;; Also define command line options at read-time because we parse\n;; `opts::*options*' in `start'.\n(eval-always (define-opts))\n\n(define-command quit (&optional (code 0))\n  \"Quit Nyxt.\"\n  (let ((*quitting-nyxt-p* t))\n    (when (slot-value *browser* 'ready-p)\n      (hooks:run-hook (before-exit-hook *browser*))\n      ;; Unready browser:\n      ;; - after the hook, so that on hook error the browser is still ready;\n      ;; - before the rest, so to avoid nested `quit' calls.\n      (setf (slot-value *browser* 'ready-p) nil)\n      (setf (slot-value *browser* 'exit-code) code)\n      (mapcar #'ffi-window-delete (window-list))\n      (when (socket-thread *browser*)\n        (destroy-thread* (socket-thread *browser*))\n        ;; Warning: Don't attempt to remove socket-path if socket-thread was not\n        ;; running or we risk removing an unrelated file.\n        (let ((socket (files:expand *socket-file*)))\n          (when (uiop:file-exists-p socket)\n            (log:info \"Deleting socket ~s.\" socket)\n            (uiop:delete-file-if-exists socket))))\n      (ffi-kill-browser *browser*)\n      ;; Reset global state.\n      (setf *browser* nil\n            *options* nil)\n      (uninstall *renderer*)\n      ;; Destroy all kernel threads.\n      (lparallel.kernel:end-kernel))))\n\n(cffi:defcallback handle-interrupt\n    :void ((signum :int) (siginfo :pointer) (ptr :pointer))\n  (declare (ignore signum siginfo ptr))\n  (quit))\n\n(export-always 'entry-point)\n(defun entry-point ()\n  \"Read the CLI arguments and start the browser.\nThis is the entry point of the binary program.\nDon't run this from a REPL, prefer `start' instead.\"\n  (define-opts)\n  (multiple-value-bind (options free-args)\n      (handler-bind ((opts:unknown-option #'handle-malformed-cli-arg)\n                     (opts:missing-arg #'handle-malformed-cli-arg)\n                     (opts:arg-parser-failed #'handle-malformed-cli-arg))\n        (opts:get-opts))\n    (setf *run-from-repl-p* nil)\n    (apply #'start (append options (list :urls free-args)))))\n\n(defun eval-expr (expr)\n  \"Evaluate the form EXPR (string) and print the result of the last expression.\"\n  (with-input-from-string (input expr)\n    (let ((*package* (find-package :nyxt-user)))\n      (flet ((eval-protect (s-exp)\n               (with-protect (\"Error in s-exp evaluation: ~a\" :condition)\n                 (eval s-exp))))\n        (let* ((sexps (safe-slurp-stream-forms input))\n               (but-last (butlast sexps))\n               (last (alex:last-elt sexps)))\n          (mapc #'eval-protect but-last)\n          (format t \"~&~a~&\" (eval-protect last)))))))\n\n(defun parse-urls (expr)\n  \"Do _not_ evaluate EXPR and try to parse URLs that were sent to it.\nEXPR is expected to be as per the expression sent in `listen-or-query-socket'.\"\n  (let* ((urls (ignore-errors (rest (read-from-string expr nil))))\n         (urls (ignore-errors (remove-if #'url-empty-p (mapcar #'url urls)))))\n    (unless urls\n      (log:warn \"Could not extract URLs from ~s.\" expr))\n    urls))\n\n(defun listen-socket ()\n  \"Listen to to see if requests arise to open URLs or evaluate s-expressions.\"\n  (files:with-paths ((socket-path *socket-file*))\n    (let ((native-socket-path (uiop:native-namestring socket-path)))\n      (ensure-directories-exist socket-path :mode #o700)\n      (iolib:with-open-socket (s :address-family :local\n                                 :connect :passive\n                                 :local-filename native-socket-path)\n        (isys:chmod native-socket-path #o600)\n        (log:info \"Listening to socket: ~s\" socket-path)\n        (loop as connection = (iolib:accept-connection s)\n              while connection\n              do (when-let\n                     ((expr (alex:read-stream-content-into-string connection)))\n                   (unless (uiop:emptyp expr)\n                     (cond ((remote-execution-p *browser*)\n                            (log:info \"External evaluation request: ~s\" expr)\n                            (eval-expr expr))\n                           ((parse-urls expr)\n                            (ffi-within-renderer-thread\n                             (lambda () (open-urls (parse-urls expr))))\n                            (when (current-window)\n                                (ffi-window-to-foreground\n                                 (current-window))))\n                           (t (make-window))))))))))\n\n(defun listening-socket-p ()\n  (ignore-errors\n   (iolib:with-open-socket (s :address-family :local\n                              :remote-filename (uiop:native-namestring\n                                                (files:expand *socket-file*)))\n     (iolib:socket-connected-p s))))\n\n(-> listen-or-query-socket ((or null (cons quri:uri *))) *)\n(defun listen-or-query-socket (urls)\n  \"If another Nyxt is listening on the socket, tell it to open URLS.\nOtherwise bind socket and return the listening thread.\"\n  (let ((socket-path (files:expand *socket-file*)))\n    (if (listening-socket-p) ;; Check if Nyxt is already running.\n        (iolib:with-open-socket\n            (s :address-family :local\n               :remote-filename (uiop:native-namestring socket-path))\n          (if urls\n            (progn\n              (log:info \"Nyxt started, trying to open URL(s): ~{~a~^, ~}\" urls)\n              (format s \"~s\" `(open-urls ,@(mapcar #'quri:render-uri urls))))\n            (progn\n              (log:info \"Nyxt started, opening new window.\")\n              (format s \"~s\" `(make-window)))))\n        (progn\n          (uiop:delete-file-if-exists socket-path)\n          (run-thread \"socket listener\"\n            (listen-socket))))))\n\n(defun remote-eval (expr)\n  \"If another Nyxt is listening on the socket, tell it to evaluate EXPR.\"\n  (if (listening-socket-p)\n      (iolib:with-open-socket (s :address-family :local\n                                 :remote-filename (uiop:native-namestring\n                                                   (files:expand *socket-file*)))\n        (write-string expr s))\n      (progn\n        (log:info \"No instance running.\")\n        (uiop:quit 0 #+bsd nil))))\n\n(eval-always\n  (defvar %start-args\n    (mapcar (compose #'intern #'symbol-name #'opts::name) opts::*options*)))\n\n(export-always 'start)\n(defun start #.(append '(&rest options &key urls) %start-args)\n  #.(format nil \"Parse command line or REPL options then start the browser.\nLoad URLS if any (a list of strings).\n\nThis function focuses on OPTIONS parsing.  For the actual startup procedure, see\n`start-browser'.\n\nThe OPTIONS are the same as the command line options.\n\n~a\" (with-output-to-string (s) (opts:describe :stream s)))\n  (declare #.(cons 'ignorable %start-args))\n  ;; Nyxt extensions should be made accessible straight from the beginning,\n  ;; e.g. before a script is run.\n  (pushnew 'nyxt-source-registry asdf:*default-source-registries*)\n  (asdf:clear-configuration)\n  (let ((source-directory (files:expand *source-directory*)))\n    (if (uiop:directory-exists-p source-directory)\n        (set-nyxt-source-location source-directory)\n        (log:debug \"Nyxt source directory not found.\")))\n  ;; Initialize the lparallel kernel.\n  (initialize-lparallel-kernel)\n  ;; Options should be accessible anytime, even when run from the REPL.\n  (setf *options* options)\n  (destructuring-bind (&key (headless *headless-p*) verbose help version\n                         system-information load eval quit remote\n                       &allow-other-keys)\n      options\n    (setf *headless-p* headless)\n    (if verbose\n        (progn\n          (log:config :debug)\n          (format t \"Arguments parsed: ~a and ~a~&\" options urls))\n        (log:config :pattern *log-pattern*))\n    (cond\n      (help\n       (opts:describe :prefix \"nyxt [options] [URLs]\"))\n      (version\n       (format t \"Nyxt version ~a~&\" +version+))\n      (system-information\n       (princ (system-information)))\n      ((or remote (and (or load eval) quit))\n       (start-load-or-eval))\n      (t\n       (with-protect (\"Error: ~a\" :condition)\n         (start-browser urls))))\n    (unless *run-from-repl-p* (uiop:quit 0 #+bsd nil))))\n\n(defun load-or-eval (&key remote)\n  (when remote\n    (log:info \"Probing remote instance listening to ~a.\"\n              (files:expand *socket-file*)))\n  (loop for (opt value . nil) on *options*\n        do (match opt\n             (:load (let ((value (uiop:truename* value)))\n                      (if remote\n                          (remote-eval (format nil \"~s\" `(load-lisp ,value)))\n                          (load-lisp value))))\n             (:eval (if remote\n                        (remote-eval value)\n                        (eval-expr value)))))\n  (when (and remote (not (getf *options* :quit)))\n    (log:info \"Reading s-expressions from standard input (end with Ctrl+d).\")\n    (handler-case (loop for sexp = (read)\n                        do (remote-eval (write-to-string sexp)))\n      (end-of-file ()\n        (log:info \"Quitting interpreter.\"))))\n  (when remote\n    (uiop:quit 0 #+bsd nil)))\n\n(defun start-load-or-eval ()\n  \"Evaluate Lisp.\nThe evaluation may happen on its own instance or on an already running instance.\"\n  (let ((remote (getf *options* :remote)))\n    (unless remote\n      (let ((user-package (find-package :nyxt-user)))\n        (load-lisp (files:expand *auto-config-file*) :package user-package)\n        (load-lisp (files:expand *config-file*) :package user-package)))\n    (load-or-eval :remote remote)))\n\n(defun start-browser (url-strings)\n  \"Start Nyxt.\nFirst load `*auto-config-file*' if any.\nThen load `*config-file*' if any.\nInstantiate `*browser*'.\nFinally, run the browser, load URL-STRINGS if any, then run\n`after-init-hook'.\"\n  (restart-case\n      (progn\n        (when *browser*\n          (error 'browser-already-started\n                 :message \"Another global browser instance is already running.\"))\n        (let ((log-path (files:expand *log-file*)))\n          (unless (files:nil-pathname-p log-path)\n            (uiop:delete-file-if-exists log-path) ; Otherwise `log4cl' appends.\n            (log:config :backup nil :pattern *log-pattern* :daily log-path)))\n        (format t \"Nyxt version ~a~&\" +version+)\n        (log:info \"Source location: ~s\" (files:expand *source-directory*))\n        (install *renderer*)\n        (let* ((urls (remove-if #'url-empty-p (mapcar #'url url-strings)))\n               (startup-timestamp (time:now))\n               (startup-error-reporter nil))\n          (if (or (null (files:expand *socket-file*))\n                  (not (listening-socket-p)))\n              (progn\n                (load-lisp (files:expand *auto-config-file*)\n                           :package (find-package :nyxt-user))\n                (multiple-value-bind (condition backtrace)\n                    (load-lisp (files:expand *config-file*)\n                               :package (find-package :nyxt-user))\n                  (when backtrace\n                    (setf startup-error-reporter\n                          (lambda ()\n                            (echo-warning \"~a\" condition)\n                            (error-in-new-window \"Configuration file errors\"\n                                                 (princ-to-string condition)\n                                                 backtrace)))))\n                (load-or-eval :remote nil)\n                (setf *browser*\n                      (make-instance\n                       'browser\n                       :startup-error-reporter-function startup-error-reporter\n                       :startup-timestamp startup-timestamp\n                       :socket-thread\n                       (unless\n                           (nfiles:nil-pathname-p (files:expand *socket-file*))\n                         (listen-or-query-socket urls))))\n                ;; This must be done in a separate thread because the calling\n                ;; thread may have set `*package*' as an initial-binding (see\n                ;; `bt:make-thread'), as is the case with the SLY mrepl thread.\n                (bt:make-thread (lambda () (in-package :nyxt-user)))\n                (ffi-initialize *browser* urls startup-timestamp)\n                (lpara:force (slot-value *browser* 'startup-promise)))\n              (listen-or-query-socket urls))))\n    (quit ()\n      :report \"Run `nyxt:quit' and try again.\"\n      (quit)\n      (start-browser url-strings))\n    (force-quit ()\n      :report \"Run `nyxt:quit' and set `*browser*' to NIL in any case.\"\n      (ignore-errors (quit))\n      (setf *browser* nil)\n      (start-browser url-strings))))\n\n(defun restart-with-message (&key condition backtrace)\n  (flet ((set-error-message (condition backtrace)\n           (let ((*package* (find-package :cl)))\n             (write-to-string\n              `(hooks:add-hook\n                (nyxt:after-init-hook nyxt:*browser*)\n                (make-instance\n                 'hooks:handler\n                 :fn (lambda ()\n                       (setf (nyxt::startup-error-reporter-function *browser*)\n                             (lambda ()\n                               (nyxt:echo-warning\n                                \"Restarted due to configuration error: ~a\"\n                                ,(princ-to-string condition))\n                               (nyxt::error-in-new-window\n                                \"Initialization error\"\n                                ,(princ-to-string condition)\n                                ,backtrace))))\n                 :name 'error-reporter))))))\n    (log:warn \"Restarting with ~s.\"\n              (append (uiop:raw-command-line-arguments) '(\"--no-config\"\n                                                          \"--no-auto-config\")))\n    (uiop:launch-program (append (uiop:raw-command-line-arguments)\n                                 `(\"--no-config\"\n                                   \"--no-auto-config\"\n                                   \"--eval\"\n                                   ,(set-error-message condition backtrace))))\n    (quit 1)))\n\n(define-command nyxt-init-time ()\n  \"Return the duration of Nyxt initialization.\"\n  (echo \"~,2f seconds\" (slot-value *browser* 'init-time)))\n"
  },
  {
    "path": "source/status.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt)\n\n(define-class status-buffer (input-buffer)\n  ((window\n    nil\n    :type (maybe window)\n    :documentation \"The `window' to which the status buffer is attached.\")\n   (height\n    36\n    :type integer\n    :writer nil\n    :reader height\n    :export t\n    :documentation \"The height of the status buffer in pixels.\")\n   (glyph-mode-presentation-p\n    nil\n    :documentation \"Display the modes as a list of glyphs.\")\n   (glyph-left (gethash \"left.svg\" *static-data*))\n   (glyph-right (gethash \"right.svg\" *static-data*))\n   (glyph-reload (gethash \"reload.svg\" *static-data*))\n   (glyph-lambda (gethash \"lambda.svg\" *static-data*))\n   (style\n    (theme:themed-css (theme *browser*)\n      '(:font-face :font-family \"public sans\" :font-style \"normal\" :font-weight\n        \"400\" :src \"url('nyxt-resource:PublicSans-Regular.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"italic\" :font-weight\n        \"400\" :src \"url('nyxt-resource:PublicSans-Italic.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"normal\" :font-weight\n        \"100\" :src \"url('nyxt-resource:PublicSans-Thin.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"italic\" :font-weight\n        \"100\" :src \"url('nyxt-resource:PublicSans-ThinItalic.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"normal\" :font-weight\n        \"200\" :src \"url('nyxt-resource:PublicSans-ExtraLight.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"italic\" :font-weight\n        \"200\" :src \"url('nyxt-resource:PublicSans-ExtraLightItalic.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"normal\" :font-weight\n        \"300\" :src \"url('nyxt-resource:PublicSans-Light.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"italic\" :font-weight\n        \"300\" :src \"url('nyxt-resource:PublicSans-LightItalic.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"normal\" :font-weight\n        \"500\" :src \"url('nyxt-resource:PublicSans-Medium.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"italic\" :font-weight\n        \"500\" :src \"url('nyxt-resource:PublicSans-MediumItalic.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"normal\" :font-weight\n        \"600\" :src \"url('nyxt-resource:PublicSans-SemiBold.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"italic\" :font-weight\n        \"600\" :src \"url('nyxt-resource:PublicSans-SemiBoldItalic.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"normal\" :font-weight\n        \"700\" :src \"url('nyxt-resource:PublicSans-Bold.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"italic\" :font-weight\n        \"700\" :src \"url('nyxt-resource:PublicSans-BoldItalic.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"normal\" :font-weight\n        \"800\" :src \"url('nyxt-resource:PublicSans-ExtraBold.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"italic\" :font-weight\n        \"800\" :src \"url('nyxt-resource:PublicSans-ExtraBoldItalic.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"normal\" :font-weight\n        \"900\" :src \"url('nyxt-resource:PublicSans-Black.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"public sans\" :font-style \"italic\" :font-weight\n        \"900\" :src \"url('nyxt-resource:PublicSans-BlackItalic.woff')\"\n        \"format('woff')\")\n      '(:font-face :font-family \"dejavu sans mono\" :src\n        \"url('nyxt-resource:DejaVuSansMono.ttf')\" \"format('ttf')\")\n      `(body\n        :background-color ,theme:secondary-color+\n        :color ,theme:on-secondary-color\n        :font-family ,theme:font-family\n        :line-height \"100vh\"\n        :font-size \"40vh\"\n        :white-space \"nowrap\"\n        :padding 0\n        :margin 0)\n      '(\"#container\"\n        :display \"flex\"\n        :justify-content \"space-between\"\n        :overflow-y \"hidden\")\n      `(\"#controls\"\n        :z-index \"3\"\n        :flex-basis \"90px\"\n        :display \"flex\"\n        :margin-top \"1px\")\n      '(\"#controls > button\"\n        :margin-right \"-3px\"\n        :max-width \"24px\"\n        :height \"35px\"\n        :aspect-ratio \"1/1\")\n      `(\"#url\"\n        :background-color ,theme:background-color-\n        :color ,theme:on-background-color\n        :min-width \"100px\"\n        :line-height \"75vh\"\n        :margin \"4px\"\n        :padding-left \"8px\"\n        :border-radius \"4px\"\n        :z-index \"2\"\n        :flex-grow \"3\"\n        :flex-shrink \"2\"\n        :overflow \"hidden\"\n        :flex-basis \"144px\")\n      '(\"#url button\"\n        :text-align \"left\"\n        :width \"100%\")\n      `(\"#tabs\"\n        :overflow-x \"scroll\"\n        :line-height \"75vh\"\n        :min-width \"100px\"\n        :text-align \"left\"\n        :padding-left \"3px\"\n        :z-index \"1\"\n        :flex-grow \"10\"\n        :flex-shrink \"4\"\n        :flex-basis \"144px\")\n      '(\"#tabs::-webkit-scrollbar\"\n        :display \"none\")\n      `(\".tab\"\n        :border-radius \"4px\"\n        :color ,theme:on-secondary-color\n        :display \"inline-block\"\n        :padding-left \"18px\"\n        :padding-right \"18px\"\n        :margin \"4px\"\n        :text-decoration \"transparent\"\n        :font \"inherit\"\n        :outline \"inherit\")\n      `(\"#modes\"\n        :border-radius \"4px\"\n        :background-color ,theme:secondary-color\n        :color ,theme:on-background-color\n        :padding-left \"4px\"\n        :text-align \"right\"\n        :z-index \"2\"\n        :padding-right \"3px\"\n        :line-height \"75vh\"\n        :margin \"4px\")\n      '(\"#modes > button\"\n        :border-radius \"0\"\n        :padding-left \"3px\"\n        :padding-right \"3px\")\n      '(\"#modes::-webkit-scrollbar\"\n        :display \"none\")\n      '(button\n        :background \"transparent\"\n        :color \"inherit\"\n        :text-decoration \"transparent\"\n        :border \"transparent\"\n        :border-radius \"2px\"\n        :padding 0\n        :font \"inherit\"\n        :outline \"inherit\")\n      `((:and (:or .button .tab \"#url\") :hover)\n        :cursor \"pointer\"\n        :background-color ,theme:action-color\n        :color ,theme:on-action-color)\n      `((:and (:or .button .tab) :active)\n        :background-color ,theme:action-color-\n        :color ,theme:on-action-color)\n      `(.selected-tab\n        :background-color ,theme:background-color+\n        :color ,theme:on-background-color))))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:export-predicate-name-p t)\n  (:metaclass user-class))\n\n(defmethod (setf height) (value (status-buffer status-buffer))\n  (setf (ffi-height status-buffer) value)\n  (setf (slot-value status-buffer 'height) value))\n\n(export-always 'mode-status)\n(defgeneric mode-status (status mode)\n  (:method ((status status-buffer) (mode mode))\n    (if (glyph-mode-presentation-p status)\n        (glyph mode)\n        (str:downcase (name mode))))\n  (:documentation \"Return a MODE `mode' string description for the STATUS\n `status-buffer'. Upon returning NIL, the mode is not displayed.\"))\n\n(defun sort-modes-for-status (modes)\n  \"Return visible modes in MODES, with `nyxt/mode/keyscheme:keyscheme-mode'\n placed first.\"\n  (multiple-value-bind (keyscheme-mode other-modes)\n      (sera:partition #'nyxt/mode/keyscheme::keyscheme-mode-p\n                      (sera:filter #'visible-in-status-p modes))\n    (append keyscheme-mode other-modes)))\n\n(export-always 'format-status-modes)\n(defmethod format-status-modes ((status status-buffer))\n  \"Render the enabled modes to HTML string.\nAny `nyxt/mode/keyscheme:keyscheme-mode' is placed first.\n\nThis leverages `mode-status' which can be specialized for individual modes.\"\n  (let ((buffer (active-buffer (window status))))\n    (spinneret:with-html\n      (:nbutton\n        :buffer status\n        :text \"±\"\n        :title (modes-string buffer)\n        '(nyxt:toggle-modes))\n      (loop for mode in (sort-modes-for-status (enabled-modes buffer))\n            collect\n            (let ((mode mode))\n              (when-let ((formatted-mode (mode-status status mode)))\n                (:nbutton\n                  :buffer status\n                  :text formatted-mode\n                  :title (format nil \"Describe ~a\" mode)\n                  `(describe-class :class (quote ,(name mode))))))))))\n\n(defmethod modes-string ((buffer buffer))\n  (format nil \"~{~a~^~%~}\" (append '(\"Enabled modes:\")\n                                   (mapcar #'princ-to-string\n                                           (enabled-modes buffer)))))\n\n(export-always 'format-status-buttons)\n(defmethod format-status-buttons ((status status-buffer))\n  \"Render interactive buttons to HTML string.\"\n  (spinneret:with-html\n    (:nbutton\n      :buffer status\n      :text (:raw (glyph-left status))\n      :title \"History-Backwards\"\n      '(nyxt/mode/history:history-backwards))\n    (:nbutton\n      :buffer status\n      :text (:raw (glyph-right status))\n      :title \"History-Forwards\"\n      '(nyxt/mode/history:history-forwards))\n    (:nbutton\n      :buffer status\n      :id \"reload\"\n      :text (:raw (glyph-reload status))\n      :title \"Reload\"\n      '(nyxt:reload-current-buffer))\n    (:nbutton\n      :buffer status\n      :text (:raw (glyph-lambda status))\n      :title \"Execute-Command Menu\"\n      '(nyxt:execute-command))))\n\n(export-always 'format-status-load-status)\n(defmethod format-status-load-status ((status status-buffer))\n  \"Render the load status to HTML string.\nBy default, renders a hourglass when loading a URL.\"\n  (let ((buffer (active-buffer (window status))))\n    (spinneret:with-html\n      (:span\n       (if (and (web-buffer-p buffer)\n                (eq (slot-value buffer 'status) :loading))\n           \"⧖ \"\n           \"\")))))\n\n(export-always 'format-status-url)\n(defmethod format-status-url ((status status-buffer))\n  \"Format the current URL for the STATUS buffer.\"\n  (let* ((buffer (active-buffer (window status)))\n         (url (url buffer))\n         (url-display (cond ((quri:uri-https-p url)\n                             (quri:uri-host url))\n                            ((quri:uri-http-p url)\n                             (format nil \"! ~a\" (render-url url)))\n                            (t (render-url url)))))\n    (spinneret:with-html\n      (:nbutton :buffer status :text url-display :title (title buffer)\n        '(nyxt:set-url)))))\n\n(export-always 'format-status-tabs)\n(defmethod format-status-tabs ((status status-buffer))\n  \"Render the open buffers to HTML string suitable for STATUS.\"\n  (let* ((buffers (reverse (buffer-list)))\n         (current-buffer (active-buffer (window status))))\n    (spinneret:with-html\n      (loop for buffer in buffers\n            collect\n            (let* ((buffer buffer)\n                   (url (url buffer))\n                   (domain (quri:uri-domain url))\n                   (tab-display-text (if (internal-url-p url)\n                                         (format nil \"~a:~a\"\n                                                 (quri:uri-scheme url)\n                                                 (quri:uri-path url))\n                                         domain)))\n              (:span\n               :class (if (eq current-buffer buffer)\n                          \"selected-tab tab\"\n                          \"tab\")\n               :onclick (ps:ps\n                          (nyxt/ps:lisp-eval\n                           (:title \"select-tab\" :buffer status)\n                           (set-current-buffer buffer)))\n               tab-display-text))))))\n\n(defmethod update-status-modes ((status status-buffer))\n  \"Update ONLY the modes to avoid redrawing the whole status buffer.\"\n  (ps-eval :async t :buffer status\n    (setf (ps:@ (nyxt/ps:qs document \"#modes\") |innerHTML|)\n          (ps:lisp\n           (spinneret:with-html-string (format-status-modes status))))))\n\n(defmethod update-status-tabs ((status status-buffer))\n  \"Update ONLY the status tabs to avoid redrawing the whole status buffer.\"\n  (ps-eval :async t :buffer status\n    (setf (ps:@ (nyxt/ps:qs document \"#tabs\") |innerHTML|)\n          (ps:lisp\n           (spinneret:with-html-string (format-status-tabs status))))))\n\n(defmethod update-status-url ((status status-buffer))\n  \"Update ONLY the URL to avoid redrawing the whole status buffer.\"\n  (ps-eval :async t :buffer status\n    (setf (ps:@ (nyxt/ps:qs document \"#url\") |innerHTML|)\n          (ps:lisp\n           (spinneret:with-html-string (format-status-url status))))))\n\n(export-always 'format-status)\n(defmethod format-status ((status status-buffer))\n  \"Return a string corresponding to the body of the HTML document of STATUS.\n\nTo override all that is displayed on STATUS, redefine this method. To partially\noverride it, redefine methods such as `format-status-url' or\n`format-status-modes'.\"\n  (let* ((buffer (active-buffer (window status))))\n    (spinneret:with-html-string\n      (:div :id \"container\"\n            (:div :id \"controls\"\n                  (format-status-buttons status))\n            (:div :id \"url\"\n                  (format-status-load-status status)\n                  (format-status-url status))\n            (:div :id \"tabs\"\n                  (format-status-tabs status))\n            (:div :id \"modes\"\n                  :title (modes-string buffer)\n                  (format-status-modes status))))))\n\n(defmethod show-selected-tab ((status status-buffer))\n  \"Scroll the selected tab into view.\"\n  (ps-eval :async t :buffer status\n    (let ((selected-tab\n            (ps:aref\n             (ps:chain document\n                       (get-elements-by-class-name \"selected-tab\")) 0)))\n      (if selected-tab\n          (ps:chain selected-tab\n                    (scroll-into-view (ps:create inline \"center\"\n                                                 behavior \"smooth\")))))))\n\n(defvar *setf-handlers* (sera:dict)\n  \"A hash-table mapping (CLASS SLOT) pairs to (OBJECT HANDLER) pairs.\nOBJECT is an instance of CLASS.\nHANDLER is a function that takes a CLASS instance as argument.\n\nSee `define-setf-handler'.\")\n\n(export-always 'define-setf-handler)\n(defmacro define-setf-handler (class-name slot bound-object handler)\n  \"When (setf (SLOT-WRITER CLASS-INSTANCE) VALUE) is called,\nall handlers corresponding to (CLASS SLOT) are evaluated with CLASS-INSTANCE as\nargument.\n\nThere is a unique HANDLER per BOUND-OBJECT.\n\nWhen BOUND-OBJECT is garbage-collected, the corresponding handler is\nautomatically removed.\"\n  (alex:with-gensyms (key)\n    `(let ((,key (list (find-class ',class-name) ',slot)))\n       (handler-bind ((warning (if (gethash ,key *setf-handlers*)\n                                   #'muffle-warning\n                                   #'identity)))\n         (setf (gethash ,bound-object\n                        (alex:ensure-gethash\n                         ,key\n                         *setf-handlers*\n                         (tg:make-weak-hash-table :test 'equal\n                                                  :weakness :key)))\n               ,handler)\n         (defmethod (setf ,slot) :after (value (,class-name ,class-name))\n           (declare (ignorable value))\n           (dolist (handler (alex:hash-table-values\n                             (gethash ,key *setf-handlers*)))\n             (funcall* handler ,class-name)))))))\n\n(defmethod print-status ((status-buffer status-buffer))\n  (ffi-print-status status-buffer (format-status status-buffer))\n  (show-selected-tab status-buffer))\n\n(defmethod customize-instance :after ((status-buffer status-buffer) &key)\n  \"Add handlers to redraw STATUS-BUFFER.\nSee `define-setf-handler'.\"\n  (with-slots (window) status-buffer\n    (define-setf-handler modable-buffer modes status-buffer\n      (lambda (buffer) (when (eq buffer (active-buffer window))\n                         (print-status status-buffer))))\n    (define-setf-handler mode enabled-p status-buffer\n      (lambda (mode) (when (eq (buffer mode) (active-buffer window))\n                       (print-status status-buffer))))\n    (define-setf-handler document-buffer url status-buffer\n      (lambda (_) (declare (ignore _))\n        (print-status status-buffer)))\n    (define-setf-handler window active-buffer status-buffer\n      (lambda (_) (declare (ignore _))\n        (update-status-url status-buffer)\n        (update-status-modes status-buffer)\n        (update-status-tabs status-buffer)\n        (show-selected-tab status-buffer)))\n    (define-setf-handler network-buffer status status-buffer\n      (lambda (buffer) (when (eq buffer (active-buffer window))\n                         (print-status status-buffer))))\n    (define-setf-handler browser buffers status-buffer\n      (lambda (_) (declare (ignore _))\n        (print-status status-buffer)))))\n"
  },
  {
    "path": "source/time.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt)\n\n(defun sort-by-time (sequence &key (key #'last-access))\n  \"Return a timely ordered SEQUENCE by KEY.  More recent elements come first.\"\n  (sort sequence #'time:timestamp> :key key))\n"
  },
  {
    "path": "source/tutorial.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt)\n\n(defun tutorial-content ()\n  (spinneret:with-html\n    (:nsection :title \"Core concepts\"\n\n      (:nsection :title \"Keybindings and commands\"\n        (:p \"Commands are invoked by pressing specific keys or from\nthe \" (:nxref :command 'execute-command)\n\".\")\n        (:p \"Keybindings are represented like this: 'C-x'. In this example, 'C' is a\nshortcut for the modifier 'control', and 'x' represents the character 'x'. To\ninput the 'C-x' keybinding you would keep 'control' pressed and then hit 'x'.\nMultiple key presses can be chained: in 'C-x C-s', you would have to press\n'C-x', and then press 'C-s'.\")\n        (:p \"Modifier keys legend:\")\n        (:ul\n         (:li (:code \"control\") \" (\" (:code \"C\") \"): Control key\")\n         (:li (:code \"super\") \" (\" (:code \"S\") \"): Windows key, Command key\")\n         (:li (:code \"meta\") \" (\" (:code \"M\") \"): Alt key, Option key\")\n         (:li (:code \"shift\") \" (\" (:code \"s\") \"): Shift key\"))\n        (:p \"Modifiers can be remapped, see slot \" (:code \"modifier-plist\") \".\"))\n\n      (:nsection :title \"Quickstart keys\"\n        (:ul\n         (list-command-information '(set-url reload-current-buffer\n                                     set-url-new-buffer\n                                     switch-buffer-previous\n                                     nyxt/mode/history:history-backwards\n                                     nyxt/mode/history:history-forwards\n                                     nyxt/mode/hint:follow-hint\n                                     nyxt/mode/hint:follow-hint-new-buffer\n                                     quit execute-command describe-bindings))))\n\n      (:nsection :title \"Buffers\"\n        (:p \"Nyxt uses the concept of buffers instead of tabs. Unlike tabs, buffers\nare fully separated, each buffer having its own behavior and settings.\"))\n\n      (:nsection :title \"Modes\"\n        (:p \"Each buffer has its own list of modes, ordered by priority.  A mode is a\nset of functions, hooks, keybindings and other facilities that may modify the\nbehavior of a buffer.  For example, 'blocker-mode' can be used for domain-based\nad-blocking while 'no-script-mode' disables JavaScript.\")\n        (:p \"Each buffer has separate instances of modes, which means that altering\nthe settings of a mode in a buffer does not impact other buffers.  Mode specific\nfunctions/commands are only available when a mode is enabled for the current\nbuffer.\")\n        (:p \"Each mode has an associated \" (:i \"mode toggler\") \" which is a command\nof the same name that toggles the mode for the current buffer.\"))\n\n      (:nsection :title \"Prompt buffer\"\n        (:p \"The prompt buffer is a menu that will appear when a command requests user\ninput. For example, when invoking the \" (:code \"set-url\") \" command, you must\nsupply the URL you would like to navigate to.  The prompt buffer can provide\nsuggestions.  The list of suggestions will automatically narrow down to those\nmatching your input as you type.\")\n        (:ul\n         (:li  (:nxref :command 'nyxt/mode/prompt-buffer:run-action-on-return\n                 :mode 'nyxt/mode/prompt-buffer:prompt-buffer-mode)\n               \": Validate the selected suggestion(s) or the current input if there is\nno suggestion.\")\n         (:li  (:nxref :command 'nyxt/mode/prompt-buffer:set-action-on-return\n                 :mode 'nyxt/mode/prompt-buffer:prompt-buffer-mode)\n               \": Query the user for an action to run over the marked suggestion(s).\"))\n        (:p \" Some commands support marks, for\ninstance \" (:code \"delete-buffer\") \" can delete all selected buffers at once.\nWhen the input is changed and the suggestions are re-filtered, the marks are\nnot altered even if the marked suggestions aren't visible.\")\n        (:p \"When at least one suggestion is marked, only the marked suggestions are processed\nupon return.  The suggestion under the cursor is not processed if not marked.\")\n        (:ul\n         (:li  (:nxref :command 'nyxt/mode/prompt-buffer:toggle-mark-forwards\n                 :mode 'nyxt/mode/prompt-buffer:prompt-buffer-mode)\n               \": Select or deselect the current suggestion.\")\n         (:li  (:nxref :command 'nyxt/mode/prompt-buffer:mark-all\n                 :mode 'nyxt/mode/prompt-buffer:prompt-buffer-mode)\n               \": Select all currently-displayed suggestions.\")\n         (:li  (:nxref :command 'nyxt/mode/prompt-buffer:unmark-all\n                 :mode 'nyxt/mode/prompt-buffer:prompt-buffer-mode)\n               \": Deselect all currently-displayed suggestions.\")\n         (:li  (:nxref :command 'nyxt/mode/prompt-buffer:toggle-mark-all\n                 :mode 'nyxt/mode/prompt-buffer:prompt-buffer-mode)\n               \": Toggle the mark of all currently-displayed suggestions.\")\n         (:li  (:nxref :command 'nyxt/mode/prompt-buffer:toggle-attributes-display\n                 :mode 'nyxt/mode/prompt-buffer:prompt-buffer-mode)\n               \": Change which attributes are displayed in the suggestions list.\")))\n\n      (:nsection :title \"Message buffer\"\n        (:p \"The message buffer represents a space (typically at the bottom of a\nwindow) where Nyxt outputs messages back to you. To view the history of all\nmessages, invoke the command \" (:nxref :command 'nyxt/mode/message:list-messages) \".\"))\n\n      (:nsection :title \"Status buffer\"\n        (:p \"The status buffer is where information about the state of that buffer is\nprinted (typically at the bottom of a window). By default, this includes the\nactive modes, the URL, and the title of the current buffer.\")))\n\n    (:nsection :title \"Basic controls\"\n\n      (:nsection :title \"Moving within a buffer\"\n        (:p \"To move within a buffer, several commands are provided:\")\n        (:ul\n         (list-command-information '(nyxt/mode/document:scroll-down\n                                     nyxt/mode/document:scroll-up\n                                     nyxt/mode/document:scroll-page-down\n                                     nyxt/mode/document:scroll-page-up\n                                     nyxt/mode/document:scroll-to-bottom\n                                     nyxt/mode/document:scroll-to-top))))\n\n      (:nsection :title \"Setting the URL\"\n        (:p \"When ambiguous URLs are inputted, Nyxt will attempt the best guess it\ncan. If you do not supply a protocol in a URL, HTTPS will be assumed. To\nvisit a site supporting only the less secure HTTP, you must explicitly type the\nfull URL including the 'http://' prefix.\")\n        (:ul\n         (list-command-information '(set-url set-url-new-buffer make-buffer-focus))))\n\n      (:nsection :title \"Switching buffers\"\n        (:ul\n         (list-command-information '(switch-buffer switch-buffer-next switch-buffer-previous))))\n\n      (:nsection :title \"Copy and paste\"\n        (:p \"Unlike other web browsers, Nyxt provides powerful ways of copying\n   and pasting content via different commands. Starting from:\")\n        (:ul\n         (list-command-information '(nyxt/mode/document:copy nyxt/mode/document:paste)))\n        (:p \"Passing through webpage's data:\")\n        (:ul\n         (list-command-information '(copy-url copy-title nyxt/mode/hint:copy-hint-url)))\n        (:p \"Leveraging password managers: \")\n        (:ul\n         (list-command-information '(nyxt/mode/password:copy-username nyxt/mode/password:copy-password)))\n        (:p \"And more: \")\n        (:ul\n         (list-command-information '(nyxt/mode/document:paste-from-clipboard-ring show-system-information))))\n\n      (:nsection :title \"Link navigation\"\n        (:p \"Link-hinting allows you to visit URLs on a page without using the mouse.\nInvoke one of the commands below: several hints will appear on screen and all\nlinks on the page will be listed in the prompt buffer.  You can select the hints\nby matching against the hint, the URL or the title.\")\n        (:ul\n         (list-command-information '(nyxt/mode/hint:follow-hint\n                                     nyxt/mode/hint:follow-hint-new-buffer-focus\n                                     nyxt/mode/hint:follow-hint-new-buffer))))\n\n      (:nsection :title \"Incremental Search\"\n        (:p \"Nyxt's search is incremental, i.e. it begins as soon as you type\nthe first character of the search string.  A single or multiple buffers can be\nqueried, and all results are displayed in the prompt buffer.\")\n        (:p \"This makes it easy to interact with results found in different URLs\nfrom a unified interface.\")\n        (:ul\n         (list-command-information '(nyxt/mode/search-buffer:search-buffer\n                                     nyxt/mode/search-buffer:search-buffers\n                                     nyxt/mode/search-buffer:remove-search-marks))))\n\n      (:nsection :title \"Bookmarks\"\n        (:p \"The bookmark file \"\n            (:code (let ((mode (make-instance 'nyxt/mode/bookmark:bookmark-mode)))\n                     (files:expand (nyxt/mode/bookmark:bookmarks-file mode))))\n            \" is made to be human readable and editable.\nBookmarks can have the following settings:\")\n        (:ul\n         (:li (:code \":url\") \": The URL of the bookmark.\")\n         (:li (:code \":title\") \": The title of the bookmark.\")\n         (:li (:code \":tags\") \": A list of strings.  Useful to categorize and filter bookmarks.\"))\n        (:p \"Bookmark-related commands\")\n        (:ul\n         (list-command-information '(nyxt/mode/bookmark:add-bookmark\n                                     nyxt/mode/bookmark:bookmark-hint\n                                     nyxt/mode/bookmark:set-url-from-bookmark\n                                     nyxt/mode/bookmark:edit-bookmark\n                                     nyxt/mode/bookmark:delete-bookmark\n                                     nyxt/mode/bookmark:list-bookmarks\n                                     nyxt/mode/bookmark:import-bookmarks-from-html))))\n\n      (:nsection :title \"Annotations\"\n        (:p \"Annotations can have the following settings:\")\n        (:ul\n         (:li (:nxref :slot 'nyxt/mode/annotate:snippet\n                :class-name 'nyxt/mode/annotate:snippet-annotation)\n              \": The snippet which was highlighted by the user.\")\n         (:li (:nxref :slot 'nyxt/mode/annotate::url\n                :class-name 'nyxt/mode/annotate:url-annotation)\n              \": The URL of the annotation.\")\n         (:li (:nxref :slot 'nyxt/mode/annotate:page-title\n                :class-name 'nyxt/mode/annotate:url-annotation)\n              \": The title of the annotation.\")\n         (:li (:nxref :slot 'nyxt/mode/annotate::data\n                :class-name 'nyxt/mode/annotate:annotation)\n              \": The comment about the highlighted snippet or\nthe URL.\")\n         (:li (:nxref :slot 'nyxt/mode/annotate:tags\n                :class-name 'nyxt/mode/annotate:annotation)\n              \": A list of strings.  Useful to categorize and filter annotations.\"))\n        (:p \"Annotate-related commands\")\n        (:ul\n         (list-command-information '(nyxt/mode/annotate:annotate-current-url nyxt/mode/annotate:annotate-highlighted-text\n                                     nyxt/mode/annotate:show-annotation nyxt/mode/annotate:show-annotations nyxt/mode/annotate:show-annotations-for-current-url))))\n\n      (:nsection :title \"Passthrough mode\"\n        (:p \"The command \" (:code \"passthrough-mode\") \" forwards all keys to the\nrenderer. For instance, using the default binding of Nyxt (\" (:code \"web-cua-map\") \") the\nkeybinding \" (:code \"C-i\") \" executes \" (:code \"autofill\") \". Suppose\na user is using their email client which also uses \" (:code \"C-i\") \" for the italic command. Thus, after\nexecuting \" (:code \"passthrough-mode\") \" the \" (:code \"C-i\") \" binding is associated\nwith the webpage's italic command instead of \" (:code \"autofill\") \". Finally, the\nuser can return to their configuration just by executing \" (:code \"passthrough-mode\") \" again.\"))\n\n      (:nsection :title \"Enable, disable, and toggle multiple modes\"\n        (:p \"The command \" (:nxref :command 'enable-modes) \" allows the user to apply multiple\nmodes (such as \" (:code \"nosound-mode\") \" and \" (:code \"dark-mode\") \") to\nmultiple buffers at once. Conversely, it is possible to revert this action by\nexecuting \" (:nxref :command 'disable-modes) \" while choosing exactly the same buffers and\nmodes previously selected. Finally, \" (:code \"toggle-mode\") \" also allows\nactivation and deactivation of multiple modes, but only for the current\nbuffer.\"))\n\n      (:nsection :title \"Structural navigation\"\n        (:p \"It is possible to navigate through headings: \")\n        (:ul\n         (list-command-information '(nyxt/mode/document:jump-to-heading\n                                     nyxt/mode/document:jump-to-heading-buffers))))\n\n      (:nsection :title \"Spelling check\"\n        (:p \"Several commands are provided to spell check words. The default is\nEnglish but it is possible to change the slot \"\n            (:nxref :slot 'nyxt/mode/spell-check:spell-check-language :class-name 'nyxt/mode/spell-check:spell-check-mode)\n            \" for other languages:\")\n        (:ul\n         (list-command-information '(nyxt/mode/spell-check:spell-check-word\n                                     nyxt/mode/spell-check:spell-check-word-at-cursor\n                                     nyxt/mode/spell-check:spell-check-suggest-word\n                                     nyxt/mode/spell-check:spell-check-highlighted-word\n                                     nyxt/mode/spell-check:spell-check-list-languages))))\n\n      (:nsection :title \"Visual mode\"\n        (:p \"Select text without a mouse. Nyxt's \"\n            (:code \"visual-mode\") \" imitates Vim's visual mode (and comes with the\nCUA and Emacs-like keybindings out of the box, too). Activate it with the \"\n            (:nxref :command 'nyxt/mode/visual:visual-mode) \" command.\")\n        (:p \"Visual mode provides the following commands: \")\n        (:ul\n         (:li (:nxref :command 'nyxt/mode/visual:visual-mode\n                :mode 'nyxt/mode/visual:visual-mode)\n              \": Quit visual mode.\")\n         (:li (:nxref :command 'nyxt/mode/visual:select-paragraph\n                :mode 'nyxt/mode/visual:visual-mode)\n              \": \" (command-docstring-first-sentence 'nyxt/mode/visual:select-paragraph))\n         (:li (:nxref :command 'nyxt/mode/visual:toggle-mark\n                :mode 'nyxt/mode/visual:visual-mode)\n              \": \" (command-docstring-first-sentence 'nyxt/mode/visual:toggle-mark))\n         (:li (:nxref :command 'nyxt/mode/visual:forward-char\n                :mode 'nyxt/mode/visual:visual-mode)\n              \": \" (command-docstring-first-sentence 'nyxt/mode/visual:forward-char))\n         (:li (:nxref :command 'nyxt/mode/visual:backward-char\n                :mode 'nyxt/mode/visual:visual-mode)\n              \": \" (command-docstring-first-sentence 'nyxt/mode/visual:backward-char))\n         (:li (:nxref :command 'nyxt/mode/visual:forward-word\n                :mode 'nyxt/mode/visual:visual-mode)\n              \": \" (command-docstring-first-sentence 'nyxt/mode/visual:forward-word))\n         (:li (:nxref :command 'nyxt/mode/visual:backward-word\n                :mode 'nyxt/mode/visual:visual-mode)\n              \": \" (command-docstring-first-sentence 'nyxt/mode/visual:backward-word))\n         (:li (:nxref :command 'nyxt/mode/visual:forward-line\n                :mode 'nyxt/mode/visual:visual-mode)\n              \": \" (command-docstring-first-sentence 'nyxt/mode/visual:forward-line))\n         (:li (:nxref :command 'nyxt/mode/visual:backward-line\n                :mode 'nyxt/mode/visual:visual-mode)\n              \": \" (command-docstring-first-sentence 'nyxt/mode/visual:backward-line))\n         (:li (:nxref :command 'nyxt/mode/visual:beginning-line\n                :mode 'nyxt/mode/visual:visual-mode)\n              \": \" (command-docstring-first-sentence 'nyxt/mode/visual:beginning-line))\n         (:li (:nxref :command 'nyxt/mode/visual:end-line\n                :mode 'nyxt/mode/visual:visual-mode)\n              \": \" (command-docstring-first-sentence 'nyxt/mode/visual:end-line))\n         (:li (:nxref :command 'nyxt/mode/visual:forward-sentence\n                :mode 'nyxt/mode/visual:visual-mode)\n              \": \" (command-docstring-first-sentence 'nyxt/mode/visual:forward-sentence))\n         (:li (:nxref :command 'nyxt/mode/visual:backward-sentence\n                :mode 'nyxt/mode/visual:visual-mode)\n              \": \" (command-docstring-first-sentence 'nyxt/mode/visual:backward-sentence)))\n        (:p \"Commands designed to ease the use for CUA users (but available to all users): \")\n        (:ul\n         (:li (:nxref :command 'nyxt/mode/visual:forward-char-with-selection\n                :mode 'nyxt/mode/visual:visual-mode)\n              \": \" (command-docstring-first-sentence 'nyxt/mode/visual:forward-char-with-selection))\n         (:li (:nxref :command 'nyxt/mode/visual:backward-char-with-selection\n                :mode 'nyxt/mode/visual:visual-mode)\n              \": \" (command-docstring-first-sentence 'nyxt/mode/visual:backward-char-with-selection))\n         (:li (:nxref :command 'nyxt/mode/visual:forward-line-with-selection\n                :mode 'nyxt/mode/visual:visual-mode)\n              \": \" (command-docstring-first-sentence 'nyxt/mode/visual:forward-line-with-selection))\n         (:li (:nxref :command 'nyxt/mode/visual:backward-line-with-selection\n                :mode 'nyxt/mode/visual:visual-mode)\n              \": \" (command-docstring-first-sentence 'nyxt/mode/visual:backward-line-with-selection)))\n        (:p \"A note for \" (:code \"emacs-mode\") \" users: unlike in Emacs, in Nyxt the command \"\n            (:nxref :command 'nyxt/mode/visual:toggle-mark\n              :mode 'nyxt/mode/visual:visual-mode)\n            \" is bound to Shift-space, as C-space is bound to 'execute-command.\"))\n\n      (:nsection :title \"Automation\"\n        (:p \"Nyxt has many facilities for automation. For instance, it is possible to\nautomate the reading experience:\")\n        (:ul\n         (list-command-information '(nyxt/mode/cruise-control:cruise-control-mode)))\n        (:p \"Symmetrically, it is possible to automate the filling of forms: \")\n        (:ul\n         (list-command-information '(nyxt/mode/autofill:autofill\n                                     nyxt/mode/bookmarklets::toggle-checkboxes)))\n        (:p \"In addition, it is possible to automate actions over time: \"\n            (:ul\n             (list-command-information '(nyxt/mode/watch:watch-mode))\n             (:li (:nxref :command 'nyxt/mode/repeat:repeat-every) \": \"\n                  (command-docstring-first-sentence 'nyxt/mode/repeat:repeat-every\n                                                    :sentence-case-p t))))\n        (:p \"Or even automate actions based on conditions: \"\n            (:ul\n             (list-command-information '(nyxt/mode/repeat:repeat-mode))))\n        (:p \"Nyxt also offers a no-code interface to build automation via Common Lisp\nmacros: \")\n        (:ul\n         (list-command-information '(nyxt/mode/macro-edit:edit-macro)))\n        (:p \"Lastly, the  \" (:nxref :mode 'nyxt/mode/process:process-mode) \" must be highlighted: \")\n        (:p (:nxref :mode 'nyxt/mode/process:process-mode) \" is actually a building block\nfor other modes previously mentioned, such as \" (:nxref :mode 'nyxt/mode/repeat:repeat-mode) \".\nThe extension relationship goes further, since\n\" (:nxref :mode 'nyxt/mode/cruise-control:cruise-control-mode) \" is in its turn an\nextension and a composition of \" (:nxref :mode 'nyxt/mode/repeat:repeat-mode) \" and \"\n(:nxref :command 'nyxt/mode/document:scroll-down) \". Further extensions and compositions can be\ncreatively tailor-made by users to automate their own use of Nyxt.\"))\n\n      (:nsection :title \"Miscellaneous\"\n        (:ul\n         (list-command-information '(nyxt/mode/document:zoom-page\n                                     nyxt/mode/document:unzoom-page\n                                     nyxt/mode/document:reset-page-zoom\n                                     nyxt/mode/autofill::autofill\n                                     nyxt/mode/file-manager:download-open-file\n                                     edit-with-external-editor)))))\n\n    (:nsection :title \"The Nyxt help system\"\n      (:p \"Nyxt provides introspective and help capabilities.  All commands,\nclasses, slots, variables, functions and bindings can be inspected for\ndefinition and documentation.\")\n      (:ul\n       (list-command-information '(tutorial describe-key describe-bindings\n                                   describe-command describe-function\n                                   describe-variable describe-class\n                                   describe-slot describe-any)))\n      (:p \"A good starting point is to study the documentation of the classes \"\n          (:code \"browser\") \", \" (:code \"window\") \", \" (:code \"buffer\") \" and \"\n          (:code \"prompt-buffer\") \".\"))))\n\n(define-internal-page-command-global quick-start (&key (page 0))\n    (buffer \"*Quick Start*\" 'nyxt/mode/help:help-mode)\n  \"Display Nyxt quick start tutorial.\"\n  (spinneret:with-html-string\n    (let* ((titles '(\"Quick Start\" \"Buffers\" \"Commands\"\n                     \"Basic Navigation\" \"Modes\" \"Well Done\"))\n           (len-titles (length titles)))\n      (case page\n        (0\n         (:h2 (nth page titles))\n         (:p \"This \" (:b \"quick start\")\n             \" presents you the key concepts to be effective at extracting\ninformation from the Internet with Nyxt.\")\n         (:div :style \"height: 20px;\")\n         (:hr)\n         (:div :style \"height: 20px;\")\n         (:p :style \"text-align:center;\" \"Table of Contents\")\n         (:ol\n          (loop for title in (rest titles) and page from 1\n                do (:li (:a :href (nyxt-url 'quick-start :page page) title))))\n         (:div :style \"height: 20px;\"))\n        (1\n         (:h2 (nth page titles))\n         (:p \"The \" (:b \"buffer\")\n             \" is the fundamental unit of information in Nyxt.\")\n         (:p \"While \" (:b \"buffers\")\n             \" are similar to browser tabs, they can be navigated and organized\nin more complex ways such as filtering, or grouping by URLs, titles, contexts,\ntags, keywords, and bookmarks.\"))\n        (2\n         (:h2 (nth page titles))\n         (:p \"Commands are invocations that trigger certain actions.  For\nexample, mouse clicks trigger commands.\")\n         (:p \"All commands in Nyxt have a \" (:b \"name\")\n             \", and some have an associated key binding (also known as keyboard\nshortcuts).\")\n         (:p \"Commands can be called by name from the \"\n             (:b \"Execute-Command Menu\") \":\")\n         (:li (:nxref :command 'nyxt:execute-command) \" to open;\")\n         (:li (:nxref :command 'nyxt/mode/prompt-buffer:quit-prompt-buffer) \" to close.\")\n         (:p \"This menu is called the prompt buffer and most of the interaction\nbetween you and Nyxt goes through it.\")\n         (:hr)\n         (:p \"Some other commands to try:\")\n         (:li (:nxref :command 'nyxt:reload-current-buffer) \" reloads the main buffer;\")\n         (:li (:nxref :command 'nyxt:make-buffer-focus) \" opens a new one.\"))\n        (3\n         (:h2 (nth page titles))\n         (:li (:nxref :command 'nyxt:set-url) \" invokes the address bar.\")\n         (:li (:nxref :command 'nyxt:set-url-new-buffer)\n              \" as above, but creates a new buffer.\")\n         (:li (:nxref :command 'nyxt:switch-buffer) \" to show a list of buffers.\")\n         (:li (:nxref :command 'nyxt:delete-current-buffer) \" to close a buffer.\")\n         (:hr)\n         (:p \"Notice that most of these commands invoke the prompt\nbuffer. Every time that a command request input from the user, this menu will\nappear.\")\n         (:p \"Each command can either be called by name or by its corresponding keyboard\nshortcut.\"))\n        (4\n         (:h2 (nth page titles))\n         (:p \"Separate tasks are best handled with separate settings.\")\n         (:p (:b \"Modes\") \" are toggled [on/off] for different settings in each\n         buffer.\")\n         (:hr)\n         (:p \"Example:\")\n         (:p \"To enjoy the silent Web, you can enable \"\n             (:nxref :mode 'nyxt/mode/no-sound:no-sound-mode)\n             \" by invoking the command \"\n             (:nxref :command 'nyxt/mode/no-sound:no-sound-mode) \".\")\n         (:p \"Again, you can access this command, and all others via \"\n             (:nxref :command 'nyxt:execute-command) \".\")\n         (:hr)\n         (:small \"A list of all modes (including those currently enabled) is\navailable by invoking the \" (:nxref :command 'nyxt:toggle-modes) \" command.\"))\n        (5\n         (:h2 (nth page titles))\n         (:p \"You are now licensed to build, destroy, and enjoy Nyxt. Where you\ngo from here is up to you.\")\n         (:div :style \"height: 20px;\")\n         (:div :style \"text-align: center;\"\n               (:nbutton\n                 :title \"Visit default new buffer\"\n                 :text \"Start using Nyxt\"\n                 :buffer buffer\n                 '(if-let ((default-new (find (default-new-buffer-url *browser*)\n                                              (buffer-list)\n                                              :test #'quri:uri= :key #'url)))\n                   (set-current-buffer default-new)\n                   (make-buffer-focus)))\n               (:div :style \"height: 20px;\")\n               (:p (:i \"Happy Hacking!\"))\n               (:div :style \"height: 20px;\"))\n         (:hr)\n         (:small \"Find further information by issuing commands that start with\ndescribe, such as \"\n                 (:nxref :command 'describe-bindings) \" or \"\n                 (:nxref :command 'describe-any) \".\")))\n      (:hr)\n      (:div :style \"position: absolute; bottom: 10px; right: 10px; left: 10px\"\n            (:div :style \"display: grid; grid-template-columns: 20px 1fr 20px; grid-gap: 10px\"\n                  (:div\n                   (:a.button\n                    :style \"width: 100%; padding: 0\"\n                    :href (nyxt-url 'quick-start :page (max (1- page) 0))\n                    (:raw (gethash \"left.svg\" *static-data*))))\n                  (:div :class \"progress-bar-container\"\n                        :style \"margin: auto;\"\n                        (:div :class \"progress-bar-base\"\n                              (:div\n                               :class \"progress-bar-fill\"\n                               :style (format nil \"width: ~,1f%;\" (* 100 (/ (1+ page) len-titles))))))\n                  (:div\n                   (:a.button\n                    :style \"width: 100%; padding: 0;\"\n                    :href (nyxt-url 'quick-start :page (min (1+ page) (1- len-titles)))\n                    (:raw (gethash \"right.svg\" *static-data*)))))))))\n"
  },
  {
    "path": "source/types.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n;; `uiop:define-package' instead of `nyxt:define-package' since it does not\n;; depend on Nyxt.\n(uiop:define-package :nyxt/types\n  (:use :cl)\n  (:import-from :serapeum #:export-always)\n  (:documentation \"Package for types.\nIt's useful to have a separate package because some types may need to generate\nfunctions for the `satisfies' type condition.\"))\n(in-package :nyxt/types)\n\n;; types:proper-list doesn't check its element type.\n(defun list-of-p (list type)\n  \"Return non-nil if LIST contains only elements of the given TYPE.\"\n  (and (listp list)\n       (every (lambda (x) (typep x type)) list)))\n\n(export-always 'list-of)\n(deftype list-of (type)\n  \"The empty list or a proper list of TYPE elements.\nUnlike `(cons TYPE *)', it checks all the elements.\n`(cons TYPE *)' does not accept the empty list.\"\n  (unless (trivial-types:type-specifier-p type)\n    (error \"Invalid type specifier: ~a\" type))\n  (let ((predicate-name (intern\n                         (string-upcase\n                          (uiop:strcat \"LIST-OF-\"\n                                       (remove-if (complement #'alphanumericp)\n                                                  (princ-to-string type))\n                                       \"-P\"))\n                         (find-package :nyxt/types))))\n    (unless (fboundp predicate-name)\n      (setf (fdefinition predicate-name)\n            (lambda (object)\n              (list-of-p object type))))\n    `(and list (satisfies ,predicate-name))))\n\n(export-always 'alist-of)\n(deftype alist-of (key-type &optional value-type)\n  \"Alist of dotted conses.\"\n  `(list-of (cons ,key-type ,(or value-type key-type))))\n\n(export-always 'cookie-policy)\n(deftype cookie-policy ()\n  \"The type of cookie policy to apply.\"\n  `(member :accept :never :no-third-party))\n\n(export-always 'maybe)\n(deftype maybe (&rest types)\n  \"An optional/maybe type for a value that is either one of TYPES, or NIL.\"\n  `(or null ,@types))\n\n(export-always 'maybe*)\n(deftype maybe* (&rest types)\n  \"A lenient `maybe' with all the 'empty' sequences as null value.\"\n  `(or null (array * (0)) ,@types))\n"
  },
  {
    "path": "source/urls.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt)\n\n(export-always 'url)\n(defmethod url ((url quri:uri))\n  url)\n\n(defmethod url ((url-string string))\n  (quri:uri url-string))\n\n(defun has-url-method-p (object)\n  \"Return non-nil if OBJECT has `url' specialization.\"\n  (has-method-p object #'url))\n\n(deftype url-designator ()\n  \"Type for anything URL-like.\nMeans that `url' can be applied to it to get `quri:uri'.\"\n  `(satisfies has-url-method-p))\n\n(export-always 'render-url)\n(defun render-url (url)\n  \"Return URL as a human-readable string.\n\nProvide protection against IDN homograph attacks, so in some cases the host part\nmay be in Punycode.\"\n  (quri:render-uri (quri:uri url)))\n\n(export-always 'fetch-url-title)\n(defun fetch-url-title (url)\n  \"Return URL's title.\nThe URL is fetched, which explains possible bottlenecks.\"\n  (ignore-errors (plump:text\n                  (aref (clss:select \"title\" (plump:parse (dex:get url))) 0))))\n\n(export-always 'error-help)\n(defun error-help (&optional (title \"Unknown error\") (text \"\"))\n  \"A helper to print error messages as displayable HTML.\"\n  (values\n   (spinneret:with-html-string\n     (:head\n      (:title title)\n      (:style (:raw (style (current-buffer)))))\n     (:body\n      (:h1 title)\n      (:pre text)))\n   \"text/html;charset=utf8\"))\n\n(export-always 'renderer-scheme)\n(defclass renderer-scheme ()\n  ()\n  (:metaclass interface-class)\n  (:documentation \"Renderer-specific representation of the custom scheme.\nShould be redefined by the renderer.\"))\n\n(define-class scheme (renderer-scheme)\n  ((name\n    (alex:required-argument 'name)\n    :documentation \"The custom scheme name to handle.\nHTTPS or FILE are examples of schemes.\")\n   (callback\n    nil\n    :type (or null function)\n    :documentation \"A function called on URL load that returns the page contents.\n\nIt takes the URL as an argument and returns up to 5 values:\n- The data for page contents (either as string or as a unsigned byte array)\n- The MIME type for the contents\n- The status code for the request\n- An alist of headers for the request\n- A status reason phrase\")\n   (error-callback\n    nil\n    :type (or null function)\n    :documentation \"Callback to use when a condition is signaled.\n\nAccepts only one argument: the signaled condition.\"))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:documentation \"Representation of Nyxt-specific internal schemes.\nHas `name' it can be accessed with. When accessed, runs `callback' to return\ncontent. In case something goes wrong, runs `error-callback'.\")\n  (:metaclass user-class))\n\n(defmethod print-object ((scheme scheme) stream)\n  (print-unreadable-object (scheme stream :type t)\n    (format stream \"~a\" (name scheme))))\n\n(defvar *schemes* (sera:dict)\n  \"A table of internal schemes registered in Nyxt.\nIt maps scheme names as strings to `scheme' objects.\")\n\n(export-always 'define-internal-scheme)\n(defun define-internal-scheme (scheme-name callback &optional error-callback)\n  \"Define handler CALLBACK for SCHEME-NAME `scheme'.\n\nSee the `callback' and `error-callback' slot documentation for their type\nsignatures.\"\n  (setf (gethash scheme-name *schemes*)\n        (list callback error-callback)))\n\n(export-always 'valid-tld-p)\n(defun valid-tld-p (hostname)\n  \"Return NIL if HOSTNAME does not include a valid TLD as determined by the\nPublic Suffix list, T otherwise.\"\n  (ignore-errors (cl-tld:get-tld hostname)))\n\n(export-always 'browser-schemes)\n(defgeneric browser-schemes (browser)\n  (:method-combination append)\n  (:documentation \"Return a list of schemes supported by BROWSER.\"))\n\n;; Set specifier to T because *BROWSER* can be bound to NIL\n(defmethod browser-schemes append ((browser t))\n  (let ((nyxt-schemes (append '(\"blob\" \"javascript\")\n                              (alex:hash-table-keys *schemes*)))\n        ;; https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml\n        ;; Last updated 2024-08-05.\n        (iana-schemes\n          '(\"aaa\" \"aaas\" \"about\" \"acap\" \"acct\" \"cap\" \"cid\" \"coap\" \"coap+tcp\"\n            \"coap+ws\" \"coaps\" \"coaps+tcp\" \"coaps+ws\" \"crid\" \"data\" \"dav\" \"dict\"\n            \"dns\" \"dtn\" \"example\" \"file\" \"ftp\" \"geo\" \"go\" \"gopher\" \"h323\" \"http\"\n            \"https\" \"iax\" \"icap\" \"im\" \"imap\" \"info\" \"ipn\" \"ipp\" \"ipps\" \"iris\"\n            \"iris.beep\" \"iris.lwz\" \"iris.xpc\" \"iris.xpcs\" \"jabber\" \"ldap\"\n            \"leaptofrogans\" \"mailto\" \"mid\" \"msrp\" \"msrps\" \"mt\" \"mtqp\" \"mupdate\"\n            \"mvrp\" \"mvrps\" \"news\" \"nfs\" \"ni\" \"nih\" \"nntp\" \"opaquelocktoken\"\n            \"pkcs11\" \"pop\" \"pres\" \"reload\" \"rtsp\" \"rtsps\" \"rtspu\" \"service\"\n            \"session\" \"shttp\" \"sieve\" \"sip\" \"sips\" \"sms\" \"snmp\" \"soap.beep\"\n            \"soap.beeps\" \"stun\" \"stuns\" \"tag\" \"tel\" \"telnet\" \"tftp\"\n            \"thismessage\" \"tip\" \"tn3270\" \"turn\" \"turns\" \"tv\" \"urn\" \"vemmi\" \"vnc\"\n            \"ws\" \"wss\" \"xcon\" \"xcon-userid\" \"xmlrpc.beep\" \"xmlrpc.beeps\" \"xmpp\"\n            \"z39.50r\" \"z39.50s\")))\n    (append nyxt-schemes iana-schemes)))\n\n(export-always 'valid-scheme-p)\n(defun valid-scheme-p (scheme)\n  \"Whether SCHEME is supported.\"\n  (find scheme (browser-schemes *browser*) :test #'string=))\n\n(export-always 'valid-url-p)\n(defun valid-url-p (url &key (check-tld-p t))\n  \"Return non-nil when URL is a valid URL.\nWhen CHECK-TLD-P is non-nil, check if the host is a known TLD.\"\n  (let ((%url (ignore-errors (quri:uri url))))\n    (and %url\n         (valid-scheme-p (quri:uri-scheme %url))\n         (if (and check-tld-p (quri:uri-http-p %url))\n             (or (quri:ip-addr-p (quri:uri-host %url))\n                 (valid-tld-p (quri:uri-domain %url)))\n             t))))\n\n(-> url-empty-p ((or quri:uri string null)) boolean)\n(export-always 'url-empty-p)\n(defun url-empty-p (url)\n  \"Check whether the given URL is empty (renders to empty string).\"\n  (the (values boolean &optional)\n       (uiop:emptyp (if (quri:uri-p url) (quri:render-uri url) url))))\n\n(-> empty-path-url-p (quri:uri) boolean)\n(export-always 'empty-path-url-p)\n(defun empty-path-url-p (url)\n  \"Whether the URL is a root one, having no path or an empty path.\"\n  (or (string= (quri:uri-path url) \"/\")\n      (null (quri:uri-path url))))\n\n(-> host-only-url-p (quri:uri) boolean)\n(export-always 'host-only-url-p)\n(defun host-only-url-p (url)\n  \"Check whether the URL only has a host, and none other URL elements.\"\n  (every #'null\n         ;; FIXME: Check path too? `empty-path-url-p'?\n         (list (quri:uri-query url)\n               (quri:uri-fragment url)\n               (quri:uri-userinfo url))))\n\n(-> schemeless-url (quri:uri) string)\n(defun schemeless-url (url)             ; Inspired by `quri:render-uri'.\n  \"Return URL without its scheme (e.g. it removes 'https://').\"\n  ;; Warning: We can't just set `quri:uri-scheme' to nil because that would\n  ;; change the port (e.g. HTTP defaults to 80, HTTPS to 443).\n  (format nil\n          \"~@[~A~]~@[~A~]~@[?~A~]~@[#~A~]\"\n          (quri:uri-authority url)\n          (or (quri:uri-path url) \"/\")\n          (quri:uri-query url)\n          (quri:uri-fragment url)))\n\n(export-always 'url<)\n(-> url< (quri:uri quri:uri) (or null fixnum))\n(defun url< (url1 url2)\n  \"Like `string<' but ignore the URL scheme.\nThis way, HTTPS and HTTP is ignored when comparing URLs.\"\n  (string< (schemeless-url url1)\n           (schemeless-url url2)))\n\n(export-always 'url-equal)\n(-> url-equal (quri:uri quri:uri) boolean)\n(defun url-equal (url1 url2)\n  \"Like `quri:uri=' but ignoring the scheme.\nURLs are equal up to `scheme='.\nAuthority is compared case-insensitively (RFC 3986).\"\n  (the (values boolean &optional)\n       (url-eqs url1\n                url2\n                (list #'scheme=\n                      (lambda (url1 url2) (equal (or (quri:uri-path url1) \"/\")\n                                                 (or (quri:uri-path url2) \"/\")))\n                      (lambda (url1 url2) (equal (quri:uri-query url1)\n                                                 (quri:uri-query url2)))\n                      (lambda (url1 url2) (equal (quri:uri-fragment url1)\n                                                 (quri:uri-fragment url2)))\n                      (lambda (url1 url2) (equalp (quri:uri-authority url1)\n                                                  (quri:uri-authority url2)))))))\n\n(-> symbol->param-name (symbol) string)\n(defun symbol->param-name (symbol)\n  \"Turn the provided SYMBOL into a reasonable query URL parameter name.\"\n  (let ((*package* (find-package :nyxt))\n        (*print-case* :downcase))\n    (if (keywordp symbol)\n        (format nil \"~(~a~)\" symbol)\n        (format nil \"~s\" symbol))))\n\n(-> value->param-value (t) (values string &optional))\n(defun value->param-value (value)\n  \"Turn VALUE into a representation readable by `query-params->arglist'.\"\n  (if (stringp value)\n      value\n      ;; As to notify query-params->arglist.\n      (str:concat +escape+ (let ((*package* (find-package :nyxt)))\n                             (prin1-to-string value)))))\n\n(export-always 'nyxt-url)\n(-> nyxt-url (t &rest t &key &allow-other-keys) string)\n(defun nyxt-url (fn &rest args &key &allow-other-keys)\n  \"Return a nyxt scheme URL encoding a call to FN with ARGS.\nIt is useful to request `internal-page's.\n\nARGS is an arbitrary keyword arguments list that is translated to a URL query.\"\n  (let* ((query (quri:url-encode-params\n                 (mapcar (lambda (pair)\n                           (cons (symbol->param-name (first pair))\n                                 (value->param-value (rest pair))))\n                         (alex:plist-alist args))\n                 :space-to-plus t))\n         (url (quri:render-uri\n               (quri:make-uri\n                :scheme \"nyxt\"\n                :path (symbol->param-name fn)\n                :query (unless (uiop:emptyp query) query)))))\n    (if (internal-page-symbol-p fn)\n        url\n        (error \"URL ~a is undefined.\" url))))\n\n(export-always 'internal-page-name)\n(-> internal-page-name ((or string quri:uri)) t)\n(defun internal-page-name (url)\n  (when-let* ((%url (quri:uri url))\n              (_ (string= \"nyxt\" (quri:uri-scheme %url))))\n    ;; As to account for nyxt:foo and nyxt://foo.\n    (uiop:safe-read-from-string (str:upcase (or (quri:uri-path %url)\n                                                (quri:uri-host %url)))\n                                :package :nyxt)))\n\n(export-always 'internal-url-p)\n(defun internal-url-p (url)\n  \"Whether the URL is the `internal-page' one.\"\n  ;; FIXME: Too simple. Maybe check for command presence too?\n  (string= \"nyxt\" (quri:uri-scheme (url url))))\n\n(-> query-params->arglist ((types:association-list string string)) (values list &optional))\n(defun query-params->arglist (params)\n  \"Process the PARAMS (an alist of strings, as returned by QURI) to a regular Lisp argument plist.\"\n  (mappend (lambda (pair)\n             (let* ((key (intern (str:upcase (first pair)) :keyword))\n                    (value (if (str:starts-with-p +escape+ (rest pair))\n                               (uiop:safe-read-from-string (subseq (rest pair) 1)\n                                                           :package (find-package :nyxt))\n                               (rest pair))))\n               (list key value)))\n           params))\n\n(define-internal-scheme \"nyxt\"\n    ;; Internal URLs are loaded via `on-signal-load-finished'.\n    (lambda (url)\n      (declare (ignore url))\n      \"\"))\n\n(define-internal-scheme \"nyxt-resource\"\n    (lambda (url)\n      (nth-value 0 (gethash (quri:uri-path (url url)) *static-data*))))\n\n(-> lisp-url (&rest t &key (:id string)\n              (:buffer t)\n              (:callback (or function symbol))\n              (:title (maybe string)))\n    (values quri:uri &optional))\n(defun lisp-url (&key (id (princ-to-string (new-id)))\n                   (buffer (alex:required-argument 'buffer))\n                   (callback (alex:required-argument 'callback))\n                   title)\n  (sera:synchronized ((lisp-url-callbacks buffer))\n    (log:debug \"Registering callback ~a in buffer ~a\" id buffer)\n    (setf (gethash id (lisp-url-callbacks buffer)) callback))\n  (quri:make-uri :scheme \"lisp\"\n                 :path id\n                 :query `((\"title\" . ,title) (\"buffer\" . ,(id buffer)))))\n\n(export-always 'nyxt/ps::lisp-eval :nyxt/ps)\n(ps:defpsmacro nyxt/ps::lisp-eval ((&key (buffer '(nyxt:current-buffer)) title)\n                                   &body body)\n  \"Request a URL that evaluates BODY in BUFFER.\nTITLE is purely informative.\"\n  `(fetch\n    (ps:lisp (quri:render-uri\n              (lisp-url :buffer ,buffer\n                        :title ,title\n                        :callback ,(if (and (sera:single body)\n                                            (member (first (first body))\n                                                    '(lambda function)))\n                                       (first body)\n                                       `(lambda () ,@body)))))\n    (ps:create :mode \"no-cors\")))\n\n(define-internal-scheme \"lisp\"\n    (lambda (url)\n      (when-let* ((%url (quri:uri url))\n                  (request-id (quri:uri-path %url))\n                  (query (quri:uri-query-params %url))\n                  (title (assoc-value query \"title\" :test 'equal))\n                  (buffer-id (assoc-value query \"buffer\" :test 'equal))\n                  (buffer (find (read-from-string buffer-id)\n                                (internal-buffer-list :all t)\n                                :key 'id)))\n        (log:debug \"Evaluate Lisp callback ~a in buffer ~a: ~a\"\n                   request-id buffer title)\n        (values\n         (if-let ((callback (sera:synchronized ((lisp-url-callbacks buffer))\n                              (gethash request-id (lisp-url-callbacks buffer)))))\n           (let ((callback-output (with-current-buffer buffer (run callback))))\n             ;; Objects and other complex structures make cl-json choke.\n             ;; TODO: Maybe encode it to the format that `cl-json' supports,\n             ;; then we can override the encoding and decoding methods and allow\n             ;; arbitrary objects (like buffers) in the nyxt:// URL arguments..\n             (when (or (scalar-p callback-output)\n                       (and (sequence-p callback-output)\n                            (every #'scalar-p callback-output)))\n               (j:encode callback-output)))\n           (log:warn \"Request ~a isn't bound to a callback in buffer ~a\"\n                     %url buffer))\n         \"application/json\")))\n  (lambda (c) (log:debug \"Error when evaluating lisp URL: ~a\" c)))\n\n(-> path= (quri:uri quri:uri) boolean)\n(defun path= (url1 url2)\n  \"Return non-nil when URL1 and URL2 have the same path.\"\n  ;; See https://github.com/fukamachi/quri/issues/48.\n  (equalp (string-right-trim \"/\" (or (quri:uri-path url1) \"\"))\n          (string-right-trim \"/\" (or (quri:uri-path url2) \"\"))))\n\n(-> scheme= (quri:uri quri:uri) boolean)\n(defun scheme= (url1 url2)\n  \"Return non-nil when URL1 and URL2 have the same scheme.\nHTTP and HTTPS belong to the same equivalence class.\"\n  (or (equalp (quri:uri-scheme url1) (quri:uri-scheme url2))\n      (and (quri:uri-http-p url1) (quri:uri-http-p url2))))\n\n(-> domain= (quri:uri quri:uri) boolean)\n(defun domain= (url1 url2)\n  \"Return non-nil when URL1 and URL2 have the same domain.\"\n  (equalp (quri:uri-domain url1) (quri:uri-domain url2)))\n\n(-> host= (quri:uri quri:uri) boolean)\n(defun host= (url1 url2)\n  \"Return non-nil when URL1 and URL2 have the same host.\nThis is a more restrictive requirement than `domain='.\"\n  (equalp (quri:uri-host url1) (quri:uri-host url2)))\n\n(-> url-eqs (quri:uri quri:uri list) boolean)\n(defun url-eqs (url1 url2 eq-fn-list)\n  \"Return non-nil when URL1 and URL2 are \\\"equal\\\" as dictated by EQ-FN-LIST.\n\nEQ-FN-LIST is a list of functions that take URL1 and URL2 as arguments and\nreturn a boolean.  It defines an equivalence relation induced by EQ-FN-LIST.\n`quri:uri=' and `url-equal' are examples of equivalence relations.\"\n  ;; (and (fn1 url1 url2) (fn2 url1 url2) ...) stops as soon as any fn returns\n  ;; nil, unlike the solution below.\n  (every #'identity (mapcar (lambda (fn) (funcall fn url1 url2)) eq-fn-list)))\n\n(-> match-scheme (string &rest string) (function (quri:uri) boolean))\n(export-always 'match-scheme)\n(defun match-scheme (scheme &rest other-schemes)\n  \"Return a predicate for URL designators matching one of SCHEME or OTHER-SCHEMES.\"\n  (lambda (url-designator)\n    (some (curry #'string= (quri:uri-scheme (url url-designator)))\n          (cons scheme other-schemes))))\n\n(-> match-host (string &rest string) (function (quri:uri) boolean))\n(export-always 'match-host)\n(defun match-host (host &rest other-hosts)\n  \"Return a predicate for URL designators matching one of HOST or OTHER-HOSTS.\"\n  (lambda (url-designator)\n    (some (curry #'string= (quri:uri-host (url url-designator)))\n          (cons host other-hosts))))\n\n(-> match-domain (string &rest string) (function (quri:uri) boolean))\n(export-always 'match-domain)\n(defun match-domain (domain &rest other-domains)\n  \"Return a predicate for URL designators matching one of DOMAIN or OTHER-DOMAINS.\"\n  (lambda (url-designator)\n    (some (curry #'string= (quri:uri-domain (url url-designator)))\n          (cons domain other-domains))))\n\n(-> match-port (integer &rest integer) (function (quri:uri) boolean))\n(export-always 'match-port)\n(defun match-port (port &rest other-ports)\n  \"Return a predicate for URL designators matching one of PORT or OTHER-PORTS.\"\n  (lambda (url-designator)\n    (some (curry #'eq (quri:uri-port (url url-designator)))\n          (cons port other-ports))))\n\n(-> match-file-extension (string &rest string) (function (quri:uri) boolean))\n(export-always 'match-file-extension)\n(defun match-file-extension (extension &rest other-extensions)\n  \"Return a predicate for URL designators matching one of EXTENSION or OTHER-EXTENSIONS.\"\n  (lambda (url-designator)\n    (some (curry #'string= (pathname-type\n                            (or (quri:uri-path (url url-designator)) \"\")))\n          (cons extension other-extensions))))\n\n(-> match-regex (string &rest string) (function (quri:uri) boolean))\n(export-always 'match-regex)\n(defun match-regex (regex &rest other-regex)\n  \"Return a predicate for URL designators matching one of REGEX or OTHER-REGEX.\"\n  (lambda (url-designator)\n    (some (rcurry #'cl-ppcre:scan (render-url (url url-designator)))\n          (cons regex other-regex))))\n\n(-> match-url (string &rest string) (function (quri:uri) boolean))\n(export-always 'match-url)\n(defun match-url (one-url &rest other-urls)\n  \"Return a predicate for URLs exactly matching ONE-URL or OTHER-URLS.\"\n  (lambda (url-designator)\n    (some (rcurry #'string= (render-url (url url-designator)))\n          (mapcar (lambda (u) (quri:url-decode u :lenient t))\n                  (cons one-url other-urls)))))\n"
  },
  {
    "path": "source/user-classes.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt)\n\n(defclass user-mixin-class ()\n  ((customize-hook :initform (make-instance 'hooks:hook-any)\n                   :accessor customize-hook\n                   :documentation \"An internal hook to add customization handlers to.\n\nReserved for `define-configuration'.\n\nPrefer `define-configuration' and `customize-instance' instead.\"))\n  (:documentation \"Classes using this metaclass will call `customize-instance'\non instantiation.\nThis is useful to expose a class configuration knob to the user.\n\nThis class is also for portable configuration management. Imagine some\nimplementation has structs totally separate from classes, and thus configuring\nstructs may be possible, but would require adding yet another class. Say,\nuser-structure-class. With user-mixin-class, that would be a one-liner:\n\n(defclass user-structure-class (impl:built-in-structure-class user-mixin-class) ())\n\nwhile having the customize-hook and other configuration functionality in 3+\nclasses (user-class, user-funcallable-class, and user-structure-class in case of\nthis imaginary implementation) would already be 3x+ code duplication...\"))\n\n(defclass user-class (standard-class user-mixin-class)\n  ()\n  (:documentation \"User-configurable value class.\nCan be configured using `customize-instance' and `customize-hook'.\"))\n(export-always 'user-class)\n\n(defmethod closer-mop:validate-superclass ((class user-class) (superclass standard-class)) t)\n(defmethod closer-mop:validate-superclass ((superclass standard-class) (class user-class)) t)\n(defmethod closer-mop:validate-superclass ((class user-class) (superclass user-mixin-class)) t)\n(defmethod closer-mop:validate-superclass ((superclass user-mixin-class) (class user-class)) t)\n\n(defclass user-funcallable-class (closer-mop:funcallable-standard-class user-mixin-class)\n  ()\n  (:documentation \"User-configurable class that behaves like function.\nOne can use `funcall' on it, thus funcallable.\nCan be configured using `customize-instance' and `customize-hook'.\"))\n(export-always 'user-funcallable-class)\n\n(defmethod closer-mop:validate-superclass ((class user-funcallable-class) (superclass closer-mop:funcallable-standard-class)) t)\n(defmethod closer-mop:validate-superclass ((superclass closer-mop:funcallable-standard-class) (class user-funcallable-class)) t)\n(defmethod closer-mop:validate-superclass ((class user-funcallable-class) (superclass user-mixin-class)) t)\n(defmethod closer-mop:validate-superclass ((superclass user-mixin-class) (class user-funcallable-class)) t)\n\n(export-always 'customize-instance)\n(defgeneric customize-instance (object &key &allow-other-keys)\n  (:method ((class t) &key) t)\n  (:documentation \"Specialize this method to customize the default values and\nbehavior of some CLASS instance.\n\nThis method is run after the instance has been initialized (in particular, after\nthe `initialize-instance' :after method).\n\nThe standard method is reserved for user configuration.\n\nDo not specialize the standard method in public code, prefer\n`initialize-instance :after' instead to initialize slots, and\n`customize-instance :after' for code that relies on finalized slot values.\"))\n\n(defmethod make-instance\n  :around ((class user-mixin-class) &rest initargs &key &allow-other-keys)\n  (sera:lret ((initialized-object (call-next-method)))\n    (mapcar (lambda (class)\n              (hooks:run-hook (slot-value class 'customize-hook) initialized-object))\n            (sera:filter #'user-class-p (cons class (mopu:superclasses class))))\n    (apply #'customize-instance initialized-object initargs)))\n\n(defun user-class-p (class-specifier)\n  (let ((metaclass (cond\n                     ((symbolp  class-specifier)\n                      (find-class class-specifier))\n                     ((closer-mop:classp class-specifier)\n                      class-specifier)\n                     (t (class-of class-specifier)))))\n    (or (typep metaclass 'user-class)\n        (typep metaclass 'user-funcallable-class))))\n\n(defclass interface-class (standard-class) ()\n  (:documentation \"An interface class exists solely for the purpose of\ndereferencing other classes through its superclasses.\nIt cannot have direct slots.\n\nThis is useful when you do not know in advance which classes you need.\n\nExample:\n\nIn some early file:\n\n(defclass renderer-browser () ()\n  (:metaclass interface-class))\n\nIn a later file, when you've defined `gtk-browser':\n\n\\(handler-bind ((warning #'muffle-warning))\n  (defclass renderer-browser (gtk-browser)\n    ()\n    (:metaclass interface-class)))\"))\n(export-always 'interface-class)\n;; TODO: Is there a way to customize the metaclass so that redefinitions do not\n;; trigger a warning?\n\n(defmethod closer-mop:validate-superclass ((class interface-class)\n                                           (superclass standard-class))\n  t)\n(defmethod closer-mop:validate-superclass ((superclass standard-class)\n                                           (class interface-class))\n  t)\n(defmethod closer-mop:validate-superclass ((class interface-class)\n                                           (superclass user-class))\n  t)\n(defmethod closer-mop:validate-superclass ((superclass user-class)\n                                           (class interface-class))\n  t)\n\n(defmethod initialize-instance :after ((class interface-class) &key)\n  (when (closer-mop:class-direct-slots class)\n    (error \"Interface class cannot have direct slots.\"))\n  class)\n(defmethod reinitialize-instance :after ((class interface-class) &key)\n  (when (closer-mop:class-direct-slots class)\n    (error \"Interface class cannot have direct slots.\"))\n  class)\n\n(define-method-combination cascade ()\n  ((before (:before))\n   (around (:around))\n   (after (:after))\n   (primary ()))\n  \"Cascade upwards in the hierarchy from the child to all parent methods.\nAllows for composed object constructors/destructors, for instance.\"\n  ;; TODO: Allow cascading down to children instead?\n  (flet ((call-methods (methods)\n           (mapcar (lambda (method)\n                     `(call-method ,method))\n                   methods)))\n    (if (and (null before)\n             (null around)\n             (null after)\n             (sera:single primary))\n        `(call-method ,(first primary))\n        (let ((form `(prog1\n                         (progn\n                           ,@(call-methods before)\n                           ,@(call-methods primary))\n                       ,@(call-methods (reverse after)))))\n          (if around\n              `(call-method ,(first around)\n                            (,@(rest around)\n                             (make-method ,form)))\n              form)))))\n"
  },
  {
    "path": "source/user-files.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt)\n\n(define-class nyxt-profile (files:profile)\n  ((files:name \"nyxt\"))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:documentation \"With the default profile all data is persisted to the\nstandard locations.\"))\n\n(defvar *nyxt-profile* (make-instance 'nyxt-profile))\n\n(define-class nyxt-file (files:gpg-file)\n  ((files:profile *nyxt-profile*)\n   (files:on-external-modification 'files:reload)\n   (editable-p\n    t\n    :type boolean\n    :documentation \"Whether the file can be edited using a text editor.\nIt's not always the case, take the socket for instance.\"))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:documentation \"All Nyxt files.\nBy default, a file that fails to be loaded is automatically backed up.\nIf the file is modified externally, Nyxt automatically reloads it.\"))\n\n(define-class nyxt-remote-file (nyxt-file files:remote-file)\n  ()\n  (:export-class-name-p t)\n  (:documentation \"A `files:remote-file' with specialized methods.\"))\n\n(defmethod files:fetch ((profile nyxt-profile) (file nyxt-remote-file) &key)\n  (dex:get (files:url file)))\n\n(define-class nyxt-data-directory (files:data-file nyxt-file)\n  ((files:base-path #p\"\"))\n  (:export-class-name-p t)\n  (:documentation \"Directory for Nyxt data (history, bookmarks etc.) files.\"))\n\n(define-class nyxt-temporary-directory (files:data-file nyxt-file)\n  ((files:base-path #p\"\"))\n  (:export-class-name-p t)\n  (:documentation \"File for a /tmp/`profile'-name/ directory.\"))\n\n(defmethod files:resolve ((profile nyxt-profile) (path nyxt-temporary-directory))\n  \"Expand all data paths inside a temporary directory.\"\n  (uiop:ensure-pathname\n   (uiop:merge-pathnames* (files:name profile) (uiop:temporary-directory))\n   :ensure-directory t))\n\n(define-class nyxt-lisp-file (files:gpg-lisp-file nyxt-file)\n  ()\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:documentation \"Nyxt Lisp files.\"))\n\n(defmethod files:resolve ((profile files:profile) (file nyxt-file))\n  (sera:path-join (uiop:ensure-directory-pathname (files:name profile))\n                  (call-next-method)))\n\n(defmethod files:read-file :around ((profile nyxt-profile) (file nyxt-file) &key)\n  (unless (typep file 'files:virtual-file)\n    (let ((path (files:expand file)))\n      (unless (files:nil-pathname-p path)\n        (log:info \"Loading ~s.\" path)\n        (if *run-from-repl-p*\n            (call-next-method)\n            (handler-case (call-next-method)\n              (error (c)\n                (log:info \"Failed to load ~s: ~a\" path c)\n                (handler-case\n                    (let ((backup (files:backup path)))\n                      (log:info \"Erroring file backed up at ~s.\" backup))\n                  (error (c)\n                    (log:info \"Failed to back up file: ~a\" c)\n                    nil))\n                nil)))))))\n\n(defmethod files:write-file :around ((profile nyxt-profile) (file nyxt-file)\n                                     &key &allow-other-keys)\n  (if *run-from-repl-p*\n      (call-next-method)\n      (handler-case (call-next-method)\n        (error (c)\n          (log:info \"Failed to save ~s: ~a\" (files:expand file) c)\n          nil))))\n\n(defmethod files:serialize ((profile nyxt-profile)\n                            (file nyxt-lisp-file) stream &key)\n  ;; We need to make sure current package is :nyxt so that symbols are printed\n  ;; with consistent namespaces.\n  (let ((*package* (find-package :nyxt))\n        (*print-length* nil))\n    (s-serialization:serialize-sexp (files:content file) stream)))\n\n(defmethod files:deserialize ((profile nyxt-profile)\n                              (file nyxt-lisp-file) raw-content &key)\n  ;; We need to make sure current package is :nyxt so that symbols are printed\n  ;; with consistent namespaces.\n  (let ((*package* (find-package :nyxt)))\n    (s-serialization:deserialize-sexp raw-content)))\n\n(defmethod prompter:object-attributes ((file files:file)\n                                       (source prompter:source))\n  `((\"Path\" ,(uiop:native-namestring (files:expand file)) (:width 3))\n    (\"Exists?\" ,(if (uiop:file-exists-p\n                     (uiop:ensure-pathname (files:expand file)))\n                    \"yes\"\n                    \"no\")\n               (:width 1))\n    (\"Type\" ,(string (sera:class-name-of file)) (:width 1))\n    (\"Name\" ,(files:name file) (:width 2))))\n\n(define-class user-file-source (prompter:source)\n  ((prompter:name \"User files\")\n   (prompter:active-attributes-keys\n    '(\"Path\" \"Exists?\" \"Type\" \"Name\")\n    :accessor nil)\n   (prompter:constructor\n    (let ((path-map (make-hash-table :test 'equal)))\n      (dolist (file (files:all-files))\n        (and-let* ((file)\n                   ((editable-p file))\n                   (full-path (files:expand file)))\n          (when (and (funcall (alex:disjoin #'nyxt-subpackage-p\n                                            #'nyxt-user-subpackage-p)\n                              (symbol-package (sera:class-name-of file)))\n                     (not (uiop:directory-pathname-p full-path)))\n            (setf (gethash full-path path-map) file))))\n      (alex:hash-table-values path-map)))))\n\n(export-always 'xdg-download-dir)\n(defun xdg-download-dir ()\n  \"Get the directory for user downloads.\nTries hard to find the XDG directory or at least ~/Downloads one.\"\n  (let ((dir (ignore-errors (uiop:run-program '(\"xdg-user-dir\" \"DOWNLOAD\")\n                                              :output '(:string :stripped t)))))\n    (when (or (null dir) (uiop:pathname-equal dir (user-homedir-pathname)))\n      (setf dir (uiop:getenv \"XDG_DOWNLOAD_DIR\")))\n    (unless dir\n      (setf dir (uiop:merge-pathnames* #p\"Downloads/\" (user-homedir-pathname))))\n    (uiop:ensure-pathname dir :ensure-directory t)))\n\n(define-class download-directory (nyxt-file)\n  ((files:base-path (xdg-download-dir))\n   (files:name \"downloads\"))\n  (:export-class-name-p t))\n"
  },
  {
    "path": "source/user-interface.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n;;;; Implementations of update methods for user-interface library widgets\n\n(in-package :nyxt)\n\n(defmethod user-interface:update ((paragraph user-interface:paragraph))\n  (ffi-buffer-evaluate-javascript-async\n   (user-interface:buffer paragraph)\n   (ps:ps\n     (setf (ps:chain document\n                     (get-element-by-id (ps:lisp (user-interface:id paragraph)))\n                     text-content)\n           (ps:lisp (user-interface:text paragraph))))))\n\n(defmethod user-interface:update ((progress-bar user-interface:progress-bar))\n  (ffi-buffer-evaluate-javascript-async\n   (user-interface:buffer progress-bar)\n   (ps:ps\n     (setf (ps:chain\n            document\n            (get-element-by-id (ps:lisp (user-interface:id progress-bar)))\n            style\n            width)\n           (ps:lisp (format nil \"~,1f%\"\n                            (user-interface:percentage progress-bar)))))))\n\n(defmethod user-interface:update ((button user-interface:button))\n  (ffi-buffer-evaluate-javascript-async\n   (user-interface:buffer button)\n   (ps:ps\n     (setf (ps:chain document\n                     (get-element-by-id (ps:lisp (user-interface:id button)))\n                     text-content)\n           (ps:lisp (user-interface:text button)))\n     (setf (ps:chain document\n                     (get-element-by-id (ps:lisp (user-interface:id button)))\n                     onclick)\n           (ps:lisp (user-interface:action button))))))\n"
  },
  {
    "path": "source/utilities.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(uiop:define-package :nyxt/utilities\n  (:use :cl)\n  (:import-from :serapeum #:export-always #:->))\n\n(in-package :nyxt/utilities)\n(serapeum:eval-always\n  (trivial-package-local-nicknames:add-package-local-nickname\n   :alex :alexandria-2 :nyxt/utilities)\n  (trivial-package-local-nicknames:add-package-local-nickname\n   :sera :serapeum))\n\n(export-always '+newline+)\n(alex:define-constant +newline+ (string #\\newline)\n  :test #'equal\n  :documentation \"String containing newline.\nUseful for functions operating on strings, like `str:concat'.\")\n\n(export-always '+escape+)\n(alex:define-constant +escape+ (string #\\escape)\n  :test #'equal\n  :documentation \"String containing ASCII escape (#x1B) char.\nUseful when concatenating escaped strings, like in nyxt: URLs.\")\n\n(export-always 'new-id)\n(defun new-id ()\n  \"Generate a new unique numeric ID.\"\n  (parse-integer (symbol-name (gensym \"\"))))\n\n(export-always 'destroy-thread*)\n(defun destroy-thread* (thread)\n  \"Like `bt:destroy-thread' but does not raise an error.\nParticularly useful to avoid errors on already terminated threads.\"\n  (ignore-errors (bt:destroy-thread thread)))\n\n(export-always 'funcall*)\n(defun funcall* (f &rest args)\n  \"Like `funcall' but does nothing when F is nil.\"\n  (when f (apply #'funcall f args)))\n\n(export-always 'prini)\n(defun prini (value stream &rest keys\n              &key (case :downcase) (pretty t) (circle nil)\n                (readably nil) (package *package*) &allow-other-keys)\n  \"PRINt for Interface: a printing primitive with the best aesthetics for Nyxt.\n`write'-s the VALUE to STREAM with CASE, PRETTY, CIRCLE, and READABLY set to the\nmost intuitive values.\"\n  (let ((*print-case* case)\n        (*print-pretty* pretty)\n        (*print-circle* circle)\n        (*print-readably* readably)\n        (*package* (find-package package)))\n    (remf keys :package)\n    (apply #'write value :stream stream keys)))\n\n(export-always 'prini-to-string)\n(defun prini-to-string (value &rest keys\n                        &key (case :downcase) (pretty t) (circle nil)\n                          (readably nil) (package *package*) &allow-other-keys)\n  \"A string-returning version of `prini'.\"\n  (declare (ignorable case pretty circle readably package))\n  (with-output-to-string (s)\n    (apply #'prini value s keys)))\n\n(-> documentation-line (t &optional symbol t)\n    t)\n(export-always 'documentation-line)\n(defun documentation-line (object &optional (type t) default)\n  \"Return the first line of OBJECT `documentation' with TYPE.\nIf there's no documentation, return DEFAULT.\"\n  (or (first (sera:lines (documentation object type) :count 1))\n      default))\n\n(-> last-word (string) string)\n(export-always 'last-word)\n(defun last-word (s)\n  \"Last substring of alphanumeric characters, or empty if none.\"\n  (let ((words (sera:words s)))\n    (the (values string &optional)\n         (if words (alex:last-elt words) \"\"))))\n\n(export-always 'make-ring)\n(defun make-ring (&key (size 1000))\n  \"Return a new ring buffer.\"\n  (containers:make-ring-buffer size :last-in-first-out))\n\n(export-always 'safe-read)\n(defun safe-read (&optional\n                    (input-stream *standard-input*)\n                    (eof-error-p t)\n                    (eof-value nil)\n                    (recursive-p nil))\n  \"Like `read' with standard IO syntax but does not accept reader macros ('#.').\nUIOP has `uiop:safe-read-from-string' but no `read' equivalent.\nThis is useful if you do not trust the input.\"\n  (let ((package *package*))\n    (uiop:with-safe-io-syntax (:package package)\n      (read input-stream eof-error-p eof-value recursive-p))))\n\n(export-always 'safe-sort)\n(defun safe-sort (s &key (predicate #'string-lessp) (key #'string))\n  \"Sort sequence S of objects by KEY using PREDICATE.\"\n  (sort (copy-seq s) predicate :key key))\n\n(export-always 'safe-slurp-stream-forms)\n(defun safe-slurp-stream-forms (stream)\n  \"Like `uiop:slurp-stream-forms' but wrapped in `uiop:with-safe-io-syntax' and\npackage set to current package.\"\n  (let ((package *package*))\n    (uiop:with-safe-io-syntax (:package package)\n      (uiop:slurp-stream-forms stream))))\n\n(export-always 'has-method-p)\n(defun has-method-p (object generic-function)\n  \"Return non-nil if OBJECT has GENERIC-FUNCTION specialization.\"\n  (some (lambda (method)\n          (subtypep (type-of object)\n                    (class-name\n                     (first (closer-mop:method-specializers method)))))\n        (closer-mop:generic-function-methods generic-function)))\n\n(export-always 'smart-case-test)\n(-> smart-case-test (string) function)\n(defun smart-case-test (string)\n  \"Get the string-comparison test based on STRING.\nIf the string is all lowercase, then the search is likely case-insensitive.\nIf there's any uppercase character, then it's case-sensitive.\"\n  (if (str:downcasep string) #'string-equal #'string=))\n\n(setf spinneret:*suppress-inserted-spaces* t)\n\n(-> system-depends-on-all ((or string asdf:system)) (cons string *))\n(defun system-depends-on-all (system)\n  \"List SYSTEM dependencies recursively, even if SYSTEM is an inferred system.\"\n  (let (depends)\n    (labels ((deps (system)\n               \"Return the list of system dependencies as strings.\"\n               (mapcar (trivia:lambda-match\n                         ((list _ s _)  ; e.g. (:VERSION \"asdf\" \"3.1.2\")\n                          (princ-to-string s))\n                         (s s))\n                       (ignore-errors\n                        (asdf:system-depends-on (asdf:find-system system nil)))))\n             (subsystem? (system parent-system)\n               \"Whether PARENT-SYSTEM is a parent of SYSTEM\nfollowing the ASDF naming convention.  For instance FOO is a parent of FOO/BAR.\"\n               (alexandria:when-let ((match? (search system parent-system)))\n                 (zerop match?)))\n             (iter (systems)\n               (cond\n                 ((null systems)\n                  depends)\n                 ((subsystem? (first systems) system)\n                  (iter (append (deps (first systems)) (rest systems))))\n                 ((find (first systems) depends :test 'equalp)\n                  (iter (rest systems)))\n                 (t\n                  (when (asdf:find-system (first systems) nil)\n                    (push (first systems) depends))\n                  (iter (union (rest systems) (deps (first systems))))))))\n      (iter (list (if (typep system 'asdf:system)\n                      (asdf:coerce-name system)\n                      system))))))\n"
  },
  {
    "path": "source/window.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt)\n\n(hooks:define-hook-type window-buffer (function (window buffer))\n  \"Hook acting on `window' and `buffer'.\")\n\n(export-always 'renderer-window)\n(defclass renderer-window ()\n  ()\n  (:metaclass interface-class)\n  (:documentation \"Renderer-specific window widget.\nShould be redefined by the renderer.\"))\n\n(define-class window (renderer-window)\n  ((id\n    (new-id)\n    :type unsigned-byte\n    :documentation \"Unique identifier for a window.\")\n   (active-buffer\n    (make-instance 'buffer)\n    :documentation \"The current buffer of the window.\nNot to be confused with `current-buffer' or `focused-buffer'.\")\n   (active-prompt-buffers\n    '()\n    :export nil\n    :documentation \"The stack of current prompt buffers.\")\n   (prompt-buffer-ready-channel\n    (make-channel)\n    :export nil\n    :documentation \"A channel one may listen to if waiting\nfor the prompt buffer to be available.\nYou should not rely on the value of this channel.\nThe channel is popped when a prompt buffer is hidden.\")\n   (fullscreen-p\n    nil\n    :export nil\n    :type boolean\n    :documentation \"Whether the window is displayed in fullscreen.\")\n   (maximized-p\n    nil\n    :export nil\n    :type boolean\n    :documentation \"Whether the window is maximized.\")\n   (status-buffer\n    (make-instance 'status-buffer)\n    :type status-buffer\n    :documentation \"The `status-buffer' instance for this window.\n\nTo modify the status buffer appearance and behavior, subclass it and specialize\nthe generic functions on `status-buffer'.  Finally set the `window'\n`status-buffer' slot to an instance of this subclass.\")\n   (message-buffer\n    (make-instance 'message-buffer)\n    :type message-buffer\n    :documentation \"The `message-buffer' instance for this window.\")\n   (window-set-buffer-hook\n    (make-instance 'hook-window-buffer)\n    :type hook-window-buffer\n    :documentation \"Hook run before `ffi-window-set-buffer' takes effect.\nThe handlers take the window and the buffer as argument.\")\n   (window-delete-hook\n    (make-instance 'hook-window)\n    :type hook-window\n    :documentation \"Hook run after `ffi-window-delete' takes effect.\nThe handlers take the window as argument.\"))\n  (:export-class-name-p t)\n  (:export-accessor-names-p t)\n  (:documentation \"A window is a view where buffers are displayed.\")\n  (:metaclass user-class))\n\n(defmethod initialize-instance :after ((window window) &key (browser *browser*)\n                                       &allow-other-keys)\n  (setf (window (status-buffer window)) window)\n  (setf (window (message-buffer window)) window)\n  (when browser\n    (setf (id window) (new-id))\n    (setf (slot-value browser 'last-active-window) window))\n  window)\n\n(defmethod print-object ((window window) stream)\n  (print-unreadable-object (window stream :type t)\n    (format stream \"~a ~a\" (id window) (titler window))))\n\n(defmethod titler ((window window))\n  \"Return the title of WINDOW.\"\n  (str:concat (title (active-buffer window)) \" － Nyxt\"))\n\n(defmethod (setf active-buffer) :around (value (window window))\n  (declare (ignore value))\n  (call-next-method)\n  (set-window-title))\n\n(hooks:define-hook-type window (function (window))\n  \"Hook acting on `window's.\")\n\n(export-always 'window-make)\n(defun window-make (browser)\n  \"Create a new window in BROWSER.\"\n  (let ((window (make-instance 'window)))\n    (setf (gethash (id window) (windows browser)) window)\n    (unless (slot-value browser 'last-active-window)\n      (setf (slot-value browser 'last-active-window) window))\n    (hooks:run-hook (window-make-hook browser) window)\n    window))\n\n(define-class window-source (prompter:source)\n  ((prompter:name \"Windows\")\n   (prompter:enable-marks-p t)\n   (prompter:constructor (window-list))\n   (prompter:actions-on-return (lambda-mapped-command ffi-window-delete))))\n\n(defmethod prompter:object-attributes ((window window) (source window-source))\n  (declare (ignore source))\n  `((\"ID\" ,(id window) (:width 1))\n    (\"Active buffer\" ,(title (active-buffer window)) (:width 3))))\n\n(define-command delete-window ()\n  \"Delete the queried window(s).\"\n  (prompt :prompt \"Delete window(s)\" :sources 'window-source))\n\n(define-command delete-current-window (&optional (window (current-window)))\n  \"Delete WINDOW, or the current window, when omitted.\"\n  (ffi-window-delete window))\n\n(define-command make-window (&optional buffer)\n  \"Create a new window.\"\n  (let ((window (window-make *browser*))\n        (buffer (or buffer (make-buffer))))\n    (ffi-window-set-buffer window buffer)\n    (values window buffer)))\n\n(define-command toggle-fullscreen (&optional (window (current-window)))\n  \"Toggle fullscreen state of window.\"\n  (if (fullscreen-p window)\n      (ffi-window-unfullscreen window)\n      (ffi-window-fullscreen window)))\n\n(define-command toggle-maximize (&optional (window (current-window)))\n  \"Toggle maximized state of window.\"\n  (if (maximized-p window)\n      (ffi-window-unmaximize window)\n      (ffi-window-maximize window)))\n\n(export-always 'enable-status-buffer)\n(defun enable-status-buffer (&optional (window (current-window)))\n  (setf (ffi-height (status-buffer window)) (height (status-buffer window))))\n\n(export-always 'disable-status-buffer)\n(defun disable-status-buffer (&optional (window (current-window)))\n  (setf (ffi-height (status-buffer window)) 0))\n\n(export-always 'enable-message-buffer)\n(defun enable-message-buffer (&optional (window (current-window)))\n  (setf (ffi-height (message-buffer window)) (height (message-buffer window))))\n\n(export-always 'disable-message-buffer)\n(defun disable-message-buffer (&optional (window (current-window)))\n  (setf (ffi-height (message-buffer window)) 0))\n\n(define-command toggle-toolbars (&optional (window (current-window)))\n  \"Toggle the visibility of the message and status buffers.\"\n  (toggle-status-buffer window)\n  (toggle-message-buffer window))\n\n(define-command toggle-status-buffer (&optional (window (current-window)))\n  \"Toggle the visibility of the status buffer.\"\n  (if (zerop (ffi-height (status-buffer window)))\n      (enable-status-buffer window)\n      (disable-status-buffer window)))\n\n(define-command toggle-message-buffer (&optional (window (current-window)))\n  \"Toggle the visibility of the message buffer.\"\n  (if (zerop (ffi-height (message-buffer window)))\n      (enable-message-buffer window)\n      (disable-message-buffer window)))\n"
  },
  {
    "path": "tests/benchmarks/package.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(benchmark:define-benchmark-package nyxt/benchmarks\n  (:import-from :nyxt))\n\n(unless lparallel:*kernel* (setf lparallel:*kernel*\n                                 (lparallel:make-kernel (or (serapeum:count-cpus) 1))))\n"
  },
  {
    "path": "tests/benchmarks/prompter.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package nyxt/benchmarks)\n\n(define-benchmark measure-score-suggestion-docstring ()\n  \"Measure the time needed to match against all Nyxt command docstrings.\nInputs are random character sequences taken from the docstrings.\"\n  (let* ((suggestions (loop for command in nyxt::*command-list*\n                            unless (uiop:emptyp (documentation command 'function))\n                              collect (documentation command 'function)))\n         (inverse-probability 3)\n         (inputs (mapcar (lambda (suggestion)\n                           (remove-if (lambda (c)\n                                        (declare (ignore c))\n                                        (> (random inverse-probability) 0))\n                                      suggestion))\n                         suggestions))\n         (sum 0.0))\n    (loop repeat 10\n          do (with-benchmark-sampling\n               (dolist (input inputs)\n                 (dolist (suggestion suggestions)\n                   (incf sum (prompter::score-suggestion-string input suggestion))))))))\n"
  },
  {
    "path": "tests/define-configuration.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test simple-configuration ()\n  (let ((test-url (quri:uri \"about:blank\")))\n    (nyxt:define-configuration nyxt:browser\n      ((nyxt:default-new-buffer-url test-url)))\n    (assert-equality #'quri:uri=\n                     test-url\n                     (nyxt:default-new-buffer-url (make-instance 'browser)))\n    (nyxt:clean-configuration)\n    (assert-equality #'quri:uri=\n                     (quri:uri (nyxt-url 'new))\n                     (nyxt:default-new-buffer-url (make-instance 'browser)))\n    (nyxt:clean-configuration)))\n\n(define-test overwritten-configuration ()\n  (let ((test-first-url (quri:uri \"https://example.com/first\"))\n        (test-second-url (quri:uri \"https://example.com/second\")))\n    (nyxt:define-configuration nyxt:browser\n      ((nyxt:default-new-buffer-url test-first-url)))\n    (nyxt:define-configuration nyxt:browser\n      ((nyxt:default-new-buffer-url test-second-url)))\n    (assert-equality #'quri:uri=\n                     test-second-url\n                     (nyxt:default-new-buffer-url (make-instance 'browser)))\n    (nyxt:clean-configuration)))\n\n(define-test slot-default ()\n  (let ((test-url (quri:uri \"about:blank\")))\n    (nyxt:define-configuration nyxt:browser\n      ((nyxt:default-new-buffer-url test-url)))\n    (assert-equality #'quri:uri=\n                     test-url\n                     (nyxt:default-new-buffer-url (make-instance 'browser)))\n    (nyxt:clean-configuration)\n    (nyxt:define-configuration nyxt:browser\n      ((nyxt:default-new-buffer-url nyxt:%slot-default%)))\n    (assert-equality #'quri:uri=\n                     (quri:uri (nyxt-url 'new))\n                     (nyxt:default-new-buffer-url (make-instance 'browser)))\n    (nyxt:clean-configuration)))\n\n(define-test test-slot-value ()\n  (nyxt:define-configuration nyxt:browser\n    ((nyxt:before-exit-hook (hooks:add-hook nyxt:%slot-value%\n                                            (make-instance 'hooks:handler\n                                                           :fn (lambda () (print 'dummy1))\n                                                           :name 'dummy1)))))\n  (nyxt:define-configuration nyxt:browser\n    ((nyxt:before-exit-hook (hooks:add-hook nyxt:%slot-value%\n                                            (make-instance 'hooks:handler\n                                                           :fn (lambda () (print 'dummy2))\n                                                           :name 'dummy2)))))\n  (assert-eql 2\n              (length (hooks:handlers (nyxt:before-exit-hook (make-instance 'browser))))))\n"
  },
  {
    "path": "tests/mode/annotate.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test toggle-annotate-mode ()\n  (let ((buffer (make-instance 'modable-buffer)))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/annotate:annotate-mode buffer))\n      (assert-true (disable-modes* 'nyxt/mode/annotate:annotate-mode buffer)))))\n"
  },
  {
    "path": "tests/mode/autofill.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test toggle-autofill-mode ()\n  (let ((buffer (make-instance 'modable-buffer)))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/autofill:autofill-mode buffer))\n      (assert-true (disable-modes* 'nyxt/mode/autofill:autofill-mode buffer)))))\n"
  },
  {
    "path": "tests/mode/base.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test toggle-base-mode ()\n  (let ((buffer (make-instance 'modable-buffer)))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt:base-mode buffer))\n      (assert-true (disable-modes* 'nyxt:base-mode buffer)))))\n"
  },
  {
    "path": "tests/mode/blocker.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test toggle-blocker-mode ()\n  (let ((buffer (make-instance 'modable-buffer)))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/blocker:blocker-mode buffer))\n      (assert-true (disable-modes* 'nyxt/mode/blocker:blocker-mode buffer)))))\n"
  },
  {
    "path": "tests/mode/bookmark.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test toggle-bookmark-mode ()\n  (let ((buffer (make-instance 'modable-buffer)))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/bookmark:bookmark-mode buffer))\n      (assert-true (disable-modes* 'nyxt/mode/bookmark:bookmark-mode buffer)))))\n"
  },
  {
    "path": "tests/mode/bookmarklets.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test toggle-bookmarklets-mode ()\n  (let ((buffer (make-instance 'modable-buffer)))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/bookmarklets:bookmarklets-mode buffer))\n      (assert-true (disable-modes* 'nyxt/mode/bookmarklets:bookmarklets-mode buffer)))))\n"
  },
  {
    "path": "tests/mode/buffer-listing.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test toggle-buffer-listing-mode ()\n  (let ((buffer (make-instance 'modable-buffer)))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/buffer-listing:buffer-listing-mode buffer))\n      (assert-true (disable-modes* 'nyxt/mode/buffer-listing:buffer-listing-mode buffer)))))\n"
  },
  {
    "path": "tests/mode/certificate-exception.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test toggle-certificate-exception-mode ()\n  (let ((buffer (make-instance 'network-and-modable-buffer)))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/certificate-exception:certificate-exception-mode\n                                  buffer))\n      (assert-true (disable-modes* 'nyxt/mode/certificate-exception:certificate-exception-mode\n                                   buffer)))))\n"
  },
  {
    "path": "tests/mode/cruise-control.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test toggle-cruise-control-mode ()\n  (let ((buffer (make-instance 'modable-buffer)))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/cruise-control:cruise-control-mode buffer))\n      (assert-true (disable-modes* 'nyxt/mode/cruise-control:cruise-control-mode buffer)))))\n"
  },
  {
    "path": "tests/mode/document.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test toggle-document-mode ()\n  (let ((buffer (make-instance 'modable-buffer)))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/document:document-mode buffer))\n      (assert-true (disable-modes* 'nyxt/mode/document:document-mode buffer)))))\n"
  },
  {
    "path": "tests/mode/download.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test toggle-download-mode ()\n  (let ((buffer (make-instance 'modable-buffer)))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/download:download-mode buffer))\n      (assert-true (disable-modes* 'nyxt/mode/download:download-mode buffer)))))\n"
  },
  {
    "path": "tests/mode/emacs.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test toggle-emacs-mode ()\n  (let* ((buffer (make-instance 'input-and-modable-buffer))\n         (default-keyscheme (nkeymaps:name (keyscheme buffer))))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/emacs:emacs-mode buffer))\n      (assert-string= \"emacs\"\n                      (nkeymaps:name (keyscheme buffer)))\n      (assert-true (disable-modes* 'nyxt/mode/emacs:emacs-mode buffer))\n      (assert-string= default-keyscheme\n                      (nkeymaps:name (keyscheme buffer))))))\n"
  },
  {
    "path": "tests/mode/expedition.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test toggle-expedition-mode ()\n  (let ((buffer (make-instance 'modable-buffer)))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/expedition:expedition-mode buffer))\n      (assert-true (disable-modes* 'nyxt/mode/expedition:expedition-mode buffer)))))\n"
  },
  {
    "path": "tests/mode/file-manager.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test toggle-file-manager-mode ()\n  (let ((buffer (make-instance 'modable-buffer)))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/file-manager:file-manager-mode buffer))\n      (assert-true (disable-modes* 'nyxt/mode/file-manager:file-manager-mode buffer)))))\n"
  },
  {
    "path": "tests/mode/force-https.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test toggle-force-https-mode ()\n  (let ((buffer (make-instance 'network-and-modable-buffer)))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/force-https:force-https-mode buffer))\n      (assert-true (disable-modes* 'nyxt/mode/force-https:force-https-mode buffer)))))\n"
  },
  {
    "path": "tests/mode/help.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test toggle-help-mode ()\n  (let ((buffer (make-instance 'modable-buffer)))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/help:help-mode buffer))\n      (assert-true (disable-modes* 'nyxt/mode/help:help-mode buffer)))))\n"
  },
  {
    "path": "tests/mode/hint-prompt-buffer.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test toggle-hint-prompt-buffer-mode ()\n  (let ((buffer (make-instance 'modable-buffer)))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/hint-prompt-buffer:hint-prompt-buffer-mode\n                                  buffer))\n      (assert-true (disable-modes* 'nyxt/mode/hint-prompt-buffer:hint-prompt-buffer-mode\n                                   buffer)))))\n"
  },
  {
    "path": "tests/mode/hint.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test toggle-hint-mode ()\n  (let ((buffer (make-instance 'modable-buffer)))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/hint:hint-mode buffer))\n      (assert-true (disable-modes* 'nyxt/mode/hint:hint-mode buffer)))))\n"
  },
  {
    "path": "tests/mode/history.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test toggle-history-mode ()\n  (let ((buffer (make-instance 'modable-buffer)))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/history:history-mode buffer))\n      (assert-true (disable-modes* 'nyxt/mode/history:history-mode buffer)))))\n"
  },
  {
    "path": "tests/mode/input-edit.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test toggle-input-edit-mode ()\n  (let ((buffer (make-instance 'modable-buffer)))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/input-edit:input-edit-mode buffer))\n      (assert-true (disable-modes* 'nyxt/mode/input-edit:input-edit-mode buffer)))))\n"
  },
  {
    "path": "tests/mode/keyscheme.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test toggle-keyscheme-mode ()\n  (let ((buffer (make-instance 'input-and-modable-buffer)))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/keyscheme:keyscheme-mode buffer))\n      (assert-true (disable-modes* 'nyxt/mode/keyscheme:keyscheme-mode buffer)))))\n"
  },
  {
    "path": "tests/mode/macro-edit.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test toggle-macro-edit-mode ()\n  (let ((buffer (make-instance 'modable-buffer)))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/macro-edit:macro-edit-mode buffer))\n      (assert-true (disable-modes* 'nyxt/mode/macro-edit:macro-edit-mode buffer)))))\n"
  },
  {
    "path": "tests/mode/message.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test toggle-message-mode ()\n  (let ((buffer (make-instance 'modable-buffer)))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/message:message-mode buffer))\n      (assert-true (disable-modes* 'nyxt/mode/message:message-mode buffer)))))\n"
  },
  {
    "path": "tests/mode/no-image.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test toggle-no-image-mode ()\n  (let ((buffer (make-instance 'modable-buffer)))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/no-image:no-image-mode buffer))\n      (assert-true (disable-modes* 'nyxt/mode/no-image:no-image-mode buffer)))))\n"
  },
  {
    "path": "tests/mode/no-script.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test toggle-no-script-mode ()\n  (let ((buffer (make-instance 'modable-buffer)))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/no-script:no-script-mode buffer))\n      (assert-true (disable-modes* 'nyxt/mode/no-script:no-script-mode buffer)))))\n"
  },
  {
    "path": "tests/mode/no-sound.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test toggle-no-sound-mode ()\n  (let ((buffer (make-instance 'modable-buffer)))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/no-sound:no-sound-mode buffer))\n      (assert-true (disable-modes* 'nyxt/mode/no-sound:no-sound-mode buffer)))))\n"
  },
  {
    "path": "tests/mode/no-webgl.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test toggle-no-webgl-mode ()\n  (let ((buffer (make-instance 'modable-buffer)))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/no-webgl:no-webgl-mode buffer))\n      (assert-true (disable-modes* 'nyxt/mode/no-webgl:no-webgl-mode buffer)))))\n"
  },
  {
    "path": "tests/mode/passthrough.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test toggle-passthrough-mode ()\n  (let ((buffer (make-instance 'input-and-modable-buffer)))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/passthrough:passthrough-mode buffer))\n      (assert-true (disable-modes* 'nyxt/mode/passthrough:passthrough-mode buffer)))))\n"
  },
  {
    "path": "tests/mode/password.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test toggle-password-mode ()\n  (let ((buffer (make-instance 'modable-buffer)))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/password:password-mode buffer))\n      (assert-true (disable-modes* 'nyxt/mode/password:password-mode buffer)))))\n"
  },
  {
    "path": "tests/mode/process.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n(use-nyxt-package-nicknames)\n\n(define-test toggle-process-mode ()\n  (let ((buffer (make-instance 'modable-buffer)))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/process:process-mode buffer))\n      (assert-true (disable-modes* 'nyxt/mode/process:process-mode buffer)))))\n\n(define-test run-action-ad-eternum ()\n  ;; Runs twice to ensure that mode instances which have not been garbage\n  ;; collected still behave as intended.\n  (dotimes (_ 2)\n    (let* ((action-run-p (lpara:promise))\n           (mode (make-instance 'nyxt/mode/process:process-mode\n                                :buffer (make-instance 'modable-buffer)\n                                :path-url (quri:uri \"test\")\n                                :firing-condition t\n                                :action\n                                (lambda (path-url mode)\n                                  (declare (ignore path-url mode))\n                                  (lpara:fulfill action-run-p t)))))\n      (enable mode)\n      ;; Wait for action-run-p to be fulfilled for a maximum of 2 seconds.\n      (loop repeat 20\n            until (lpara:fulfilledp action-run-p)\n            do (sleep 0.1))\n      (disable mode)\n      (assert-true (lpara:force action-run-p)))))\n\n(define-test null-action ()\n  (let ((mode (make-instance 'nyxt/mode/process:process-mode\n                             :buffer (make-instance 'modable-buffer)\n                             :path-url (quri:uri \"test\")\n                             :action nil)))\n    (enable mode)\n    (assert-true (null (nyxt/mode/process::thread mode)))\n    (disable mode)))\n\n(define-test null-firing-condition ()\n  (let ((mode (make-instance 'nyxt/mode/process:process-mode\n                             :buffer (make-instance 'modable-buffer)\n                             :path-url (quri:uri \"test\")\n                             :firing-condition nil)))\n    (enable mode)\n    (assert-true (null (nyxt/mode/process::thread mode)))\n    (disable mode)))\n\n(define-test firing-condition ()\n  (let* ((run-action-count 0)\n         (repeat-action-count (random 10))\n         (mode (make-instance 'nyxt/mode/process:process-mode\n                              :buffer (make-instance 'modable-buffer)\n                              :path-url (quri:uri \"test\")\n                              :action\n                              (lambda (path-url mode)\n                                (declare (ignore path-url mode))\n                                (incf run-action-count))\n                              :firing-condition\n                              (lambda (path-url mode)\n                                (declare (ignore path-url mode))\n                                (if (= run-action-count repeat-action-count) :return t)))))\n    (enable mode)\n    (bt:join-thread (nyxt/mode/process::thread mode))\n    (disable mode)\n    (assert-eq repeat-action-count\n               run-action-count)))\n\n(define-test cleanup ()\n  (let* ((clean-p)\n         (mode (make-instance 'nyxt/mode/process:process-mode\n                              :buffer (make-instance 'modable-buffer)\n                              :path-url (quri:uri \"test\")\n                              :cleanup\n                              (lambda (path-url mode)\n                                (declare (ignore path-url mode))\n                                (setf clean-p t)))))\n    (nyxt/mode/process::call-cleanup mode)\n    (assert-true clean-p)))\n\n(define-test thread-handling ()\n  (let ((mode (make-instance 'nyxt/mode/process:process-mode\n                             :buffer (make-instance 'modable-buffer)\n                             :path-url (quri:uri \"test\")\n                             :action\n                             (lambda (path-url mode)\n                               (declare (ignore path-url mode))))))\n    ;; Ensure that re-enabling the mode doesn't overwrite an alive thread.\n    (assert-eq (nyxt/mode/process::thread (enable mode))\n               (nyxt/mode/process::thread (enable mode)))\n    (disable mode)))\n"
  },
  {
    "path": "tests/mode/prompt-buffer.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test toggle-prompt-buffer-mode ()\n  (let ((buffer (make-instance 'modable-buffer)))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/prompt-buffer:prompt-buffer-mode buffer))\n      (assert-true (disable-modes* 'nyxt/mode/prompt-buffer:prompt-buffer-mode buffer)))))\n"
  },
  {
    "path": "tests/mode/proxy.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test toggle-proxy-mode ()\n  (let ((buffer (make-instance 'modable-buffer)))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/proxy:proxy-mode buffer))\n      (assert-true (disable-modes* 'nyxt/mode/proxy:proxy-mode buffer)))))\n"
  },
  {
    "path": "tests/mode/reading-line.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test toggle-reading-line-mode ()\n  (let ((buffer (make-instance 'modable-buffer)))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/reading-line:reading-line-mode buffer))\n      (assert-true (disable-modes* 'nyxt/mode/reading-line:reading-line-mode buffer)))))\n"
  },
  {
    "path": "tests/mode/reduce-tracking.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n;; Disable renderer specific methods.\n(defmethod enable :after ((mode nyxt/mode/reduce-tracking:reduce-tracking-mode) &key) nil)\n(defmethod disable :after ((mode nyxt/mode/reduce-tracking:reduce-tracking-mode) &key) nil)\n\n(define-test toggle-reduce-tracking-mode ()\n  (let ((buffer (make-instance 'network-and-modable-buffer))\n        (url-pre (quri:uri \"https://example.com/query?foo=bar&twclid=1&redirect=https://example.org/foo&s_cid=123\"))\n        (url-post (quri:uri \"https://example.com/query?foo=bar&redirect=https://example.org/foo\")))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/reduce-tracking:reduce-tracking-mode\n                                  buffer))\n      (assert-equality #'quri:uri=\n                       url-post\n                       (url (nhooks:run-hook (request-resource-hook buffer)\n                                             (make-instance 'request-data\n                                                            :url url-pre))))\n      (assert-true (disable-modes* 'nyxt/mode/reduce-tracking:reduce-tracking-mode\n                                   buffer)))))\n"
  },
  {
    "path": "tests/mode/repeat.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n;; TODO enabling repeat-mode invokes a prompt.  That depends on\n;; prompt-buffer-generic-history, which is a browser object.\n(define-test toggle-repeat-mode ()\n  (let ((buffer (make-instance 'modable-buffer)))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/repeat:repeat-mode buffer))\n      (assert-true (disable-modes* 'nyxt/mode/repeat:repeat-mode buffer)))))\n"
  },
  {
    "path": "tests/mode/search-buffer.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test toggle-search-buffer-mode ()\n  (let ((buffer (make-instance 'modable-buffer)))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/search-buffer:search-buffer-mode buffer))\n      (assert-true (disable-modes* 'nyxt/mode/search-buffer:search-buffer-mode buffer)))))\n\n;; TODO Test the fact that matches are marked from the back.\n\n(defmacro with-dom (&body body)\n  `(let ((spinneret:*suppress-inserted-spaces* t))\n    (nyxt/dom:named-html-parse (spinneret:with-html-string (:body ,@body)))))\n\n(defun search-dom (pattern node)\n  (nyxt/mode/search-buffer:search-document pattern\n                                           :buffer (make-instance 'document-buffer)\n                                           :node node\n                                           :mark-p nil))\n\n(defvar *query* \"match\"\n  \"Default query pattern for search tests.\")\n\n(define-test match-in-excluded-nodes ()\n  (assert-false (search-dom *query*\n                            (with-dom (:comment *query*) (:style *query*)\n                              (:script *query*) (:noscript *query*)))))\n\n(define-test match-in-simple-node ()\n  ;; A simple node is a node that has a single text node child.\n  (let* ((text-node \"foo match bar match baz\")\n         (matches (search-dom *query* (with-dom (:a text-node)))))\n    (assert= 2 (length matches))\n    (loop for match in matches\n          do (assert-string= text-node\n                             (nyxt/mode/search-buffer::body match))\n          do (assert-string= text-node\n                             (plump:text (first (nyxt/mode/search-buffer::nodes match)))))))\n\n(define-test match-in-node ()\n  (let ((matches (search-dom *query*\n                             (with-dom (:a (:b \"foo\") \"match\" (:b \"bar\") (:b \"match\"))))))\n    (assert= 2 (length matches))\n    (loop for match in matches\n          do (assert-string= *query*\n                             (nyxt/mode/search-buffer::body match))\n          do (assert-string= *query*\n                             (plump:text (first (nyxt/mode/search-buffer::nodes match)))))))\n\n(define-test match-spanning-sibling-nodes ()\n  ;; Match\n  (let ((matches (search-dom *query*\n                             (with-dom (:a \"m\") (:b \"a\") (:a \"t\") (:b \"c\") (:a \"h\")\n                               (:a \"foo\") (:b \"mat\") (:a \"ch\") (:b \"bar\")\n                               (:a \"m\") (:b \"\") (:a \"\") (:b \"atch\")))))\n    (assert= 3 (length matches))\n    (mapcar (lambda (match) (assert-string= *query* (nyxt/mode/search-buffer::body match)))\n            matches)\n    (assert= 5 (length (nyxt/mode/search-buffer::nodes (first matches))))\n    (assert= 2 (length (nyxt/mode/search-buffer::nodes (second matches))))\n    (assert= 2 (length (nyxt/mode/search-buffer::nodes (third matches)))))\n  ;; Non-match\n  (let ((matches (search-dom *query* (with-dom (:a \"m\") (:b \"foo\") (:a \"atch\")))))\n    (assert= 0 (length matches))))\n\n(define-test match-spanning-nested-nodes ()\n  ;; Match\n  (let ((matches (search-dom *query*\n                             (with-dom (:a \"ma\" (:b \"tc\" (:i \"h\")) \"bar\")\n                               (:a \"foo\" (:b \"bar\" (:i \"ma\") \"tc\") \"h\")\n                               (:a \"m\" (:b \"\" (:i \"\" (:a \"atch\"))))))))\n    (assert= 3 (length matches))\n    (mapcar (lambda (match) (assert-string= *query* (nyxt/mode/search-buffer::body match)))\n            matches)\n    (assert= 3 (length (nyxt/mode/search-buffer::nodes (first matches))))\n    (assert= 3 (length (nyxt/mode/search-buffer::nodes (second matches))))\n    (assert= 2 (length (nyxt/mode/search-buffer::nodes (third matches)))))\n  ;; Non-match\n  (let ((matches (search-dom *query* (with-dom (:a \"m\" (:b \"foo\" (:i \"atch\")))))))\n    (assert= 0 (length matches))))\n\n(define-test search-all ()\n  (let ((search-all (curry 'nyxt/mode/search-buffer::search-all\n                           *query*)))\n    (assert-false (nyxt/mode/search-buffer::search-all \"\" *query*))\n    (assert-false (funcall search-all \"foo\"))\n    (assert-equal '((0 5) (10 15))\n                  (funcall search-all (str:concat *query* \" foo \" *query*)))))\n\n(defun assert-str-empty (str)\n  \"Assert whether STR is nil or the empty string.\"\n  (assert-true (str:empty? str)))\n\n(define-test search-contiguous ()\n  (let ((search-contiguous (curry 'nyxt/mode/search-buffer::search-contiguous\n                                  *query*)))\n    (assert-str-empty (nyxt/mode/search-buffer::search-contiguous \"\" \"foo\"))\n    (assert-str-empty (nyxt/mode/search-buffer::search-contiguous \"foo\" \"\"))\n    (assert-error 'error (funcall search-contiguous \"mat\" :found-pattern \"h\"))\n    (assert-equal (values \"m\" '(4 5))\n                  (funcall search-contiguous \"foo m\"))\n    (assert-equal (values \"matc\" '(4 8))\n                  (funcall search-contiguous \"foo matc\"))\n    (assert-str-empty (funcall search-contiguous \"foo match\"))\n    (assert-equal (values \"match\" '(4 9))\n                  (funcall search-contiguous \"foo match\" :full-match-p t))\n    (assert-equal (values \"ma\" '(0 1))\n                  (funcall search-contiguous \"a\" :found-pattern \"m\"))\n    (assert-str-empty (funcall search-contiguous \"foo a\" :found-pattern \"m\"))\n    (assert-str-empty (funcall search-contiguous \"a foo\" :found-pattern \"m\"))\n    (assert-equal (values \"mat\" '(0 1))\n                  (funcall search-contiguous \"t\" :found-pattern \"ma\"))\n    (assert-str-empty (funcall search-contiguous \"foo t\" :found-pattern \"ma\"))\n    (assert-str-empty (funcall search-contiguous \"t foo\" :found-pattern \"ma\"))\n    (assert-equal (values \"matc\" '(0 1))\n                  (funcall search-contiguous \"c\" :found-pattern \"mat\"))\n    (assert-str-empty (funcall search-contiguous \"foo c\" :found-pattern \"mat\"))\n    (assert-str-empty (funcall search-contiguous \"c foo\" :found-pattern \"mat\"))\n    (assert-equal (values *query* '(0 1))\n                  (funcall search-contiguous \"h foo\" :found-pattern \"matc\"))\n    (assert-str-empty (funcall search-contiguous \"foo h\" :found-pattern \"matc\"))))\n"
  },
  {
    "path": "tests/mode/small-web.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test toggle-small-web-mode ()\n  (let ((buffer (make-instance 'modable-buffer)))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/small-web:small-web-mode buffer))\n      (assert-true (disable-modes* 'nyxt/mode/small-web:small-web-mode buffer)))))\n"
  },
  {
    "path": "tests/mode/spell-check.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test toggle-spell-check-mode ()\n  (let ((buffer (make-instance 'modable-buffer)))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/spell-check:spell-check-mode buffer))\n      (assert-true (disable-modes* 'nyxt/mode/spell-check:spell-check-mode buffer)))))\n"
  },
  {
    "path": "tests/mode/style.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test toggle-style-mode ()\n  (let ((buffer (make-instance 'modable-buffer)))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/style:style-mode buffer))\n      (assert-true (disable-modes* 'nyxt/mode/style:style-mode buffer)))))\n"
  },
  {
    "path": "tests/mode/user-script.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test toggle-user-script-mode ()\n  (let ((buffer (make-instance 'modable-buffer)))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/user-script:user-script-mode buffer))\n      (assert-true (disable-modes* 'nyxt/mode/user-script:user-script-mode buffer)))))\n"
  },
  {
    "path": "tests/mode/vi.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test toggle-vi-modes ()\n  (let* ((buffer (make-instance 'input-and-modable-buffer))\n         (default-keyscheme (nkeymaps:name (keyscheme buffer))))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/vi:vi-normal-mode buffer))\n      (assert-string= \"vi-normal\"\n                      (nkeymaps:name (keyscheme buffer)))\n      (assert-true (disable-modes* 'nyxt/mode/vi:vi-normal-mode buffer))\n      (assert-string= default-keyscheme\n                      (nkeymaps:name (keyscheme buffer)))\n\n      (assert-true (enable-modes* 'nyxt/mode/vi:vi-insert-mode buffer))\n      (assert-string= \"vi-insert\"\n                      (nkeymaps:name (keyscheme buffer)))\n      (assert-true (disable-modes* 'nyxt/mode/vi:vi-insert-mode buffer))\n      (assert-string= default-keyscheme\n                      (nkeymaps:name (keyscheme buffer))))))\n"
  },
  {
    "path": "tests/mode/visual.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n;; TODO visual-mode relies on hint-mode to be initialized, which accounts for a\n;; bad design.\n(define-test toggle-visual-mode ()\n  (let ((buffer (make-instance 'input-and-modable-buffer)))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/visual:visual-mode buffer))\n      (assert-true (disable-modes* 'nyxt/mode/visual:visual-mode buffer)))))\n"
  },
  {
    "path": "tests/mode/watch.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test toggle-watch-mode ()\n  (let ((buffer (make-instance 'modable-buffer)))\n    (with-current-buffer buffer\n      (assert-true (enable-modes* 'nyxt/mode/watch:watch-mode buffer))\n      (assert-true (disable-modes* 'nyxt/mode/watch:watch-mode buffer)))))\n"
  },
  {
    "path": "tests/mode.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-class network-and-modable-buffer (network-buffer modable-buffer) ())\n\n(define-class context-and-modable-buffer (context-buffer modable-buffer) ())\n\n(define-class input-and-modable-buffer (input-buffer modable-buffer) ())\n\n(define-test enable-modes-args-honored ()\n  (let ((*browser* (make-instance 'browser))\n        (buffer (make-instance 'modable-buffer))\n        (arg-value (random 300)))\n    (with-current-buffer buffer\n      (setf (url (current-buffer)) (quri:uri \"test\"))\n      (assert-true arg-value\n                   (nyxt/mode/repeat:repeat-interval\n                    (first (modes (first (enable-modes* 'nyxt/mode/watch:watch-mode\n                                                        buffer\n                                                        :repeat-interval arg-value)))))))))\n"
  },
  {
    "path": "tests/package.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/tests\n  (:use :lisp-unit2))\n\n;; KLUDGE Temporary workarounds to deal with the absence of data profiles.\n\n(defmethod nfiles:resolve ((profile nyxt:nyxt-profile) (file nyxt:history-file))\n  (serapeum:path-join (uiop:ensure-directory-pathname (nfiles:name profile))\n                      (call-next-method)))\n\n(uiop:delete-directory-tree (nfiles:expand (make-instance 'nyxt:nyxt-data-directory))\n                            :validate t :if-does-not-exist :ignore)\n"
  },
  {
    "path": "tests/prompt-buffer.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(unless lparallel:*kernel* (setf lparallel:*kernel*\n                                 (lparallel:make-kernel (or (serapeum:count-cpus) 1))))\n\n(define-class test-source (prompter:source)\n  ;; The number of suggestions is only relevant to assess the algorithm's\n  ;; performance, so it suffices to use a single suggestion for general testing.\n  ((prompter:name \"Test\")\n   (prompter:constructor '(\"suggestion\"))))\n\n(defun set-test-source-object-attributes (widths)\n  (handler-bind ((warning #'muffle-warning))\n    (defmethod prompter:object-attributes ((object string) (source test-source))\n      (loop for width in widths\n            for i from 1\n            collect `(,(str:concat \"Attribute-\" (princ-to-string i))\n                      ,(when width (make-string width))\n                      (:width ,width))))))\n\n(define-test uniform-width-attributes ()\n  (let ((size (sera:random-in-range 1 11))\n        (width (sera:random-in-range 1 11)))\n    (set-test-source-object-attributes (make-list size :initial-element width))\n    (assert-equal (make-list size :initial-element (/ width (* width size)))\n                  (nyxt::attribute-widths (make-instance 'test-source)))))\n\n(define-test fallback-on-unallocated-attributes-widths ()\n  \"Fallback to uniform width distribution when at least one ratio isn't specified.\"\n  (set-test-source-object-attributes (list 1 2 3 nil))\n  (assert-equal (make-list 4 :initial-element 1/4)\n                (nyxt::attribute-widths (make-instance 'test-source))))\n\n(define-test preserve-ratios-on-inactive-attributes ()\n  \"Preserve original proportions between attributes' width on disable.\"\n  (set-test-source-object-attributes (list 1 2 3))\n  (let ((source (make-instance 'test-source)))\n    (setf (slot-value source 'prompter:active-attributes-keys) '(\"Attribute-1\" \"Attribute-2\"))\n    (assert-equal (list 1/3 2/3)\n                  (nyxt::attribute-widths source))\n    (setf (slot-value source 'prompter:active-attributes-keys) '(\"Attribute-2\" \"Attribute-3\"))\n    (assert-equal (list 2/5 3/5)\n                  (nyxt::attribute-widths source))\n    (setf (slot-value source 'prompter:active-attributes-keys) '(\"Attribute-1\" \"Attribute-3\"))\n    (assert-equal (list 1/4 3/4)\n                  (nyxt::attribute-widths source))))\n"
  },
  {
    "path": "tests/renderer/custom-schemes.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests/renderer)\n\n(nyxt:define-internal-scheme \"test\"\n    (lambda (url)\n      (declare (ignore url))\n      (spinneret:with-html-string (:p \"pass\"))))\n\n(define-test register-custom-scheme ()\n  (let ((ready-channel (nyxt::make-channel 1)))\n    (nyxt:start :no-config t\n                :no-auto-config t\n                :headless t\n                :socket \"/tmp/nyxt-test.socket\")\n    (with-current-buffer (nyxt:make-buffer-focus :url \"test:test\")\n      (hooks:once-on (nyxt:buffer-loaded-hook (current-buffer)) (buffer)\n        (calispel:! ready-channel t))\n      (calispel:? ready-channel)\n      (sleep 1)\n      (assert-equal \"pass\"\n                    (nyxt:ps-eval (ps:chain (nyxt/ps:qs document \"p\") inner-text))))\n    (nyxt:quit)))\n\n(nyxt:define-internal-scheme \"iframe-embed\"\n    (lambda (url)\n      (declare (ignore url))\n      (spinneret:with-html-string\n        (:script\n         (ps:ps (defun check-iframe-loaded ()\n                  (ps:let* ((iframe (nyxt/ps:qs document \"iframe\"))\n                            (iframe-doc (or (ps:@ iframe content-document)\n                                            (ps:@ iframe content-window document)))\n                            (text-check (nyxt/ps:qs document \"#text-check\")))\n                    (when (= (ps:@ iframe-doc ready-state) \"complete\")\n                      (setf (ps:@ text-check inner-text) \"iframe loaded\")\n                      (ps:return-from check-iframe-loaded t))\n                    (ps:chain window (set-timeout check-iframe-loaded 100))))))\n        (:body\n         :onload \"checkIframeLoaded\"\n         (:iframe :src \"nyxt:new\")\n         (:span#text-check \"\")))))\n\n(define-test nyxt-url-not-iframe-embeddable ()\n  (let ((ready-channel (nyxt::make-channel 1)))\n    (nyxt:start :no-config t\n                :no-auto-config t\n                :headless t\n                :socket \"/tmp/nyxt-test.socket\")\n    (with-current-buffer (nyxt:make-buffer-focus :url \"iframe-embed:test\")\n      (hooks:once-on (nyxt:buffer-loaded-hook (current-buffer)) (buffer)\n        (calispel:! ready-channel t))\n      (calispel:? ready-channel)\n      ;; More than enough time for nyxt:new to load and check-iframe-loaded to fire.\n      (sleep 1)\n      (assert-equal \"\"\n                    (nyxt:ps-eval (ps:chain (nyxt/ps:qs document \"#text-check\") inner-text))))\n    (nyxt:quit)))\n"
  },
  {
    "path": "tests/renderer/package.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(nyxt:define-package :nyxt/tests/renderer\n  (:use :lisp-unit2))\n(in-package :nyxt/tests/renderer)\n\n(defmethod files:write-file ((profile nyxt:nyxt-profile) (file files:file) &key)\n  \"Don't persist test data.\"\n  nil)\n\n(defmethod files:resolve ((profile nyxt:nyxt-profile) (file nyxt:history-file))\n  \"Don't use any history.\"\n  #p\"\")\n\n(defmacro with-prompt-buffer-test (command &body body)\n  (alexandria:with-gensyms (thread)\n    `(let ((,thread (bt:make-thread (lambda () ,command))))\n       (calispel:? (prompt-buffer-ready-channel (current-window)))\n       ,@body\n       (run-action-on-return)\n       (bt:join-thread ,thread))))\n\n(defun test-set-url (url)\n  (let ((url-channel (nyxt::make-channel 1)))\n    (nyxt:start :no-config t\n                :no-auto-config t\n                :headless t\n                :socket \"/tmp/nyxt-test.socket\")\n    (hooks:once-on (nyxt:prompt-buffer-ready-hook *browser*) prompt-buffer\n      (prompter:all-ready-p prompt-buffer)\n      (nyxt:set-prompt-buffer-input url prompt-buffer)\n      (prompter:all-ready-p prompt-buffer)\n      (hooks:once-on (nyxt:buffer-loaded-hook (nyxt:current-buffer)) buffer\n        (calispel:! url-channel (nyxt:render-url (nyxt:url buffer))))\n      (nyxt/mode/prompt-buffer:run-action-on-return prompt-buffer))\n    (nyxt:run-thread \"run set-url\"\n      ;; TODO: Test if thread returns.\n      (nyxt:set-url))\n    (assert-string= url (calispel:? url-channel 5))\n    (nyxt:quit)))\n"
  },
  {
    "path": "tests/renderer/search-buffer.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests/renderer)\n\n(defvar *query* \"match\"\n  \"Default query pattern for search tests.\")\n\n(define-internal-page test-search-buffer (&key)\n    (:title \"*Search buffer test*\")\n  (let ((spinneret:*suppress-inserted-spaces* t))\n    ;; Based on nyxt/tests/renderer-offline/search-buffer.lisp\n    (spinneret:with-html-string (:body\n                                 ;; non matches\n                                 (:p \"m\") (:b \"foo\") (:i \"atch\")\n                                 (:p \"m\" (:b \"foo\" (:i \"atch\")))\n                                 ;; excluded nodes\n                                 (:comment *query*) (:style *query*)\n                                 (:script *query*) (:noscript  *query*)\n                                 ;; matches\n                                 (:p \"foo match bar match baz\")\n                                 (:p (:b \"foo\") \"match\" (:b \"bar\") (:b \"match\"))\n                                 (:p \"m\") (:b \"a\") (:i \"t\") (:b \"c\") (:i \"h\")\n                                 (:p \"foo\") (:b \"mat\") (:i \"ch\") (:b \"bar\")\n                                 (:p \"m\") (:b \"\") (:b \"\") (:b \"atch\")\n                                 (:p \"ma\" (:b \"tc\" (:i \"h\")) \"bar\")\n                                 (:p \"foo\" (:b \"bar\" (:i \"ma\") \"tc\") \"h\")\n                                 (:p \"m\" (:b \"\" (:i \"\" (:b \"atch\"))))))))\n\n(define-test search-buffer ()\n  (nyxt:start :no-config t\n              :no-auto-config t\n              :headless t\n              :socket \"/tmp/nyxt-test.socket\" )\n  (labels ((body (&optional (buffer (current-buffer)))\n             ;; The following would be easier, but it's not the same since the\n             ;; renderer inserts a CSS zoom rule in the DOM.\n             ;; (plump:serialize (elt (clss:select \"body\" (document-model buffer)) 0)\n             ;;                  nil)\n             (let ((dom (plump:parse (ffi-buffer-get-document buffer))))\n               (plump:serialize (alex:first-elt (clss:select \"body\" dom)) nil)))\n           (count-matches (&optional (buffer (current-buffer)))\n             ;; Multiple spans may correspond to a single match.\n             (let ((matches (clss:select \"span[nyxt-search-mark]\"\n                              (document-model buffer))))\n               (parse-integer (plump:attribute (alex:last-elt matches)\n                                               \"nyxt-search-mark\"))))\n           (assert-match-count (pattern expected-count\n                                &optional (buffer (current-buffer)))\n             (ffi-buffer-reload buffer)\n             (sleep 0.5)\n             (nyxt/mode/search-buffer:search-document\n              pattern\n              :buffer buffer\n              :node (elt (clss:select \"body\" (document-model buffer)) 0)\n              :mark-p t)\n             (sleep 0.5)\n             (assert= expected-count (count-matches buffer)))\n           (assert-dom-immutability (expected-dom-body\n                                     &optional (buffer (current-buffer)))\n             (nyxt/mode/search-buffer:remove-search-marks)\n             (sleep 0.5)\n             (assert-string= expected-dom-body (body buffer))))\n    (buffer-load-internal-page-focus 'test-search-buffer)\n    ;; Allow enough time to load the internal page.\n    (sleep 1)\n    (let ((initial-dom-body (body)))\n      (assert-match-count \"h\" 12)\n      (assert-dom-immutability initial-dom-body)\n      (assert-match-count \"ch\" 12)\n      (assert-dom-immutability initial-dom-body)\n      (assert-match-count \"tch\" 12)\n      (assert-dom-immutability initial-dom-body)\n      (assert-match-count \"atch\" 12)\n      (assert-dom-immutability initial-dom-body)\n      (assert-match-count \"match\" 10)\n      (assert-dom-immutability initial-dom-body)))\n  (nyxt:quit))\n"
  },
  {
    "path": "tests/renderer/set-url.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests/renderer)\n\n;; TODO: Use `with-prompt-buffer-test'.\n;; (with-prompt-buffer-test (set-url)\n;;   (update-prompt-input (current-prompt-buffer) \"foobar\"))\n\n(define-test set-offline-url ()\n  (test-set-url \"nyxt:about\"))\n"
  },
  {
    "path": "tests/test-data/hint-mode-html-document.html",
    "content": "<!-- Check that the positioning is correct when zooming in and out. -->\n<!doctype html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\n  <title>Hint test page</title>\n  <style>\n    body { margin:0; }\n    a { position:absolute; }\n    a.left { left:1px; }\n    a.center { left:50%; }\n    a.right { right:1px; }\n    a.top { top:1px; }\n    a.middle { top:50%; }\n    a.bottom { bottom:1px; }\n    a.bigger { font-size:24px; }\n  </style>\n</head>\n\n<body>\n  <a href=\"https://nyxt-browser.com/\" class=\"left top bigger\">left top</a>\n  <a href=\"https://nyxt-browser.com/\" class=\"center top\">center top</a>\n  <a href=\"https://nyxt-browser.com/\" class=\"right top\">right top</a>\n  <a href=\"https://nyxt-browser.com/\" class=\"left middle\">left middle</a>\n  <a href=\"https://nyxt-browser.com/\" class=\"center middle bigger\">center middle</a>\n  <a href=\"https://nyxt-browser.com/\" class=\"right middle\">right middle</a>\n  <a href=\"https://nyxt-browser.com/\" class=\"left bottom\">left bottom</a>\n  <a href=\"https://nyxt-browser.com/\" class=\"center bottom\">center bottom</a>\n  <a href=\"https://nyxt-browser.com/\" class=\"right bottom bigger\">right bottom</a>\n</body>\n</html>\n"
  },
  {
    "path": "tests/test-data/history.lisp",
    "content": "(\n(:url \"nyxt:new\" :title \"*New buffer*\")\n(:url \"nyxt:describe-bindings\" :title \"*Help-bindings*\")\n(:url \"nyxt:describe-function?fn=%1BNYXT%2FMODE%2FAUTOFILL%3AAUTOFILL\" :title \"*Help-AUTOFILL*\")\n)\n"
  },
  {
    "path": "tests/urls.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test parse-set-url-input ()\n  (let ((*browser* (make-instance 'browser)))\n    (flet ((make-data (data) (make-instance 'nyxt:url-or-query :data data)))\n      (assert-equality #'quri:uri=\n                       (quri:uri \"https://github.com/atlas-engineer\")\n                       (url (make-data \"github.com/atlas-engineer\")))\n      ;; Fails when omitting http://, acceptable?\n      (assert-equality #'quri:uri=\n                       (quri:uri \"http://localhost:8080\")\n                       (url (make-data \"http://localhost:8080\")))\n      (assert-equality #'quri:uri=\n                       (quri:uri \"https://127.0.0.1\")\n                       (url (make-data \"127.0.0.1\")))\n      (assert-equality #'quri:uri=\n                       (quri:uri \"about:blank\")\n                       (url (make-data \"about:blank\")))\n      (assert-equality #'quri:uri=\n                       (quri:uri \"nyxt:new\")\n                       (url (make-data \"nyxt:new\")))\n      (assert-eq :search-query\n                 (kind (make-data \"foo:blank\")))\n      (assert-equality #'quri:uri=\n                       (quri:uri \"file:///readme.org\")\n                       (url (make-data \"file:///readme.org\")))\n      (assert-eq :url\n                 (kind (make-data (namestring\n                                   (asdf:system-relative-pathname :nyxt\n                                                                  \"source/browser.lisp\")))))\n      (assert-equality #'quri:uri=\n                       (quri:uri \"https://en.wikipedia.org/w/index.php?search=foo\")\n                       (url (make-data \"wiki foo\")))\n      (let ((data1 (make-data \"ddg foo\"))\n            (data2 (make-data \"foo\")))\n        (with-slots ((d1 data) (k1 kind) (e1 search-engine) (q1 search-query)) data1\n          (with-slots ((d2 data) (k2 kind) (e2 search-engine) (q2 search-query)) data2\n            (assert-equal d1 d2)\n            (assert-equal k1 k2)\n            (assert-equal e1 e2)\n            (assert-equal q1 q2)\n            (assert-equality #'quri:uri= (url data1) (url data2)))))\n      ;; When engine doesn't compute suggestions, return the identity query.\n      (let* ((data-query (make-data \"searx foo\"))\n             (suggestions (search-suggestions data-query)))\n        (assert-equal 1 (length suggestions))\n        (assert-equal (data data-query) (data (first suggestions)))))))\n\n(define-test nyxt-urls ()\n  (assert-error 'simple-error\n                (nyxt-url 'undefined-nyxt-command :param1 \"foo\" :param2 \"bar\"))\n  (assert-false (internal-page-name \"foo:new\"))\n  (assert-false (internal-page-name \"foo://new\"))\n  (assert-equal 'new\n                (internal-page-name \"nyxt:new\"))\n  (assert-equal 'new\n                (internal-page-name \"nyxt://new\")))\n\n(define-test url-processing ()\n  ;; \"Invalid URL (empty host)\"\n  (assert-false (valid-url-p \"http://foo\"))\n  ;; \"Invalid URL (TLD == host)\"\n  (assert-false (valid-url-p \"http://algo\"))\n  ;; \"Valid URL\"\n  (assert-no-error t (valid-url-p \"http://example.org/foo/bar?query=baz#qux\"))\n  ;; \"Valid IP URL\"\n  (assert-no-error t (valid-url-p \"http://192.168.1.1\"))\n  ;;\"Valid IP URL with path\"\n  (assert-no-error t (valid-url-p \"http://192.168.1.1/foo\"))\n  ;; \"same schemeless URLs\"\n  (assert-true (nyxt::url-equal (quri:uri \"http://example.org\")\n                                (quri:uri \"https://example.org/\")))\n  ;; \"different schemeless URLs\"\n  (assert-false (nyxt::url-equal (quri:uri \"https://example.org\")\n                                 (quri:uri \"https://example.org/foo\")))\n  ;; \"schemeless URL\"\n  (assert-string= (nyxt::schemeless-url\n                   (quri:uri \"http://example.org/foo/bar?query=baz#qux\"))\n                  \"example.org/foo/bar?query=baz#qux\")\n  ;; \"comparing same URL\"\n  (assert-false (nyxt::url< (quri:uri \"http://example.org\")\n                            (quri:uri \"http://example.org\")))\n  ;; \"comparing same URL but for trailing slash\"\n  (assert-false (nyxt::url< (quri:uri \"http://example.org\")\n                            (quri:uri \"http://example.org/\")))\n  ;; \"comparing same URL but for scheme\"\n  (assert-false (nyxt::url< (quri:uri \"https://example.org\")\n                            (quri:uri \"http://example.org\")))\n  ;; \"comparing same URL but for scheme and trailing slash\"\n  (assert-false (nyxt::url< (quri:uri \"https://example.org\")\n                            (quri:uri \"http://example.org/\")))\n  ;; \"comparing different URLs (HTTPS first)\"\n  (assert-true (nyxt::url< (quri:uri \"https://example.org/a\")\n                           (quri:uri \"http://example.org/b\")))\n  ;; \"comparing different URLs (HTTP first)\"\n  (assert-true (nyxt::url< (quri:uri \"http://example.org/a\")\n                           (quri:uri \"https://example.org/b\"))))\n"
  },
  {
    "path": "tests/user-script-parsing.lisp",
    "content": ";;;; SPDX-FileCopyrightText: Atlas Engineer LLC\n;;;; SPDX-License-Identifier: BSD-3-Clause\n\n(in-package :nyxt/tests)\n\n(define-test user-scripts ()\n  (let* ((code \"// ==UserScript==\n// @name          Script Name\n// @namespace     Script\n// @description\t  A simple testing script\n// @version       1.23.456\n// @author        https://github.com/atlas-engineer\n// @homepageURL   https://github.com/atlas-engineer/nyxt\n// @run-at        document-start\n// @include       http://*/*\n// @include       https://*/*\n// @grant         none\n// @noframes\n// ==/UserScript==\")\n         (file-backed-script (make-instance\n                              'nyxt/mode/user-script:user-script\n                              :code code :base-path #p\"testing-script.user.js\"))\n         (virtual-script (make-instance 'nyxt/mode/user-script:user-script :code code)))\n    ;; Virtual user script code equality\n    (assert-string= code\n                    (nyxt/mode/user-script:code virtual-script))\n    ;; Virtual user script noframes\n    (assert-false (nyxt/mode/user-script:all-frames-p virtual-script))\n    ;; Virtual user script document start\n    (assert-eq :document-start (nyxt/mode/user-script:run-at virtual-script))\n    ;; Virtual user script @include\n    (assert-equal '(\"http://*/*\" \"https://*/*\")\n                  (nyxt/mode/user-script:include virtual-script))\n    ;; TODO:  Check the file serialization.\n    ;; Virtual user script code equality\n    (assert-equal (nyxt/mode/user-script:code virtual-script)\n                  (nyxt/mode/user-script:code file-backed-script))\n    (uiop:delete-file-if-exists (files:expand file-backed-script))))\n"
  }
]