diff --git a/nyxt.asd b/nyxt.asd index abe929211ef..4a4ec77de6d 100644 --- a/nyxt.asd +++ b/nyxt.asd @@ -357,7 +357,15 @@ A naive benchmark on a 16 Mpbs bandwidth gives us cl-gobject-introspection bordeaux-threads) :pathname "source/" - :components ((:file "renderer/gi-gtk"))) + :components ((:file "renderer/gi-gtk")) + :in-order-to ((test-op (test-op "nyxt/gi-gtk/tests")))) + +(defsystem "nyxt/gi-gtk/tests" + :depends-on (nyxt/gi-gtk prove) + :components ((:file "tests/renderer-package")) + :perform (test-op (op c) + (nyxt-run-test c "tests/renderer-offline/") + (nyxt-run-test c "tests/renderer-online/" :network-needed-p t))) (defsystem "nyxt/qt" :depends-on (nyxt diff --git a/source/browser.lisp b/source/browser.lisp index ea80697fbe8..3f19be3bb49 100644 --- a/source/browser.lisp +++ b/source/browser.lisp @@ -150,8 +150,8 @@ The possible values are `:always-ask', `:always-restore' and `:never-restore'.") (before-exit-hook (make-instance 'hooks:hook-void) :type hooks:hook-void - :documentation "Hook run before both `*browser*' and the -renderer get terminated. The handlers take no argument.") + :documentation "Hook run before both `*browser*' and the renderer get terminated. +The handlers take no argument.") (window-make-hook (make-instance 'hook-window) :type hook-window @@ -177,6 +177,11 @@ The handlers take the buffer as argument.") :type hook-prompt-buffer :documentation "Hook run after the `prompt-buffer' class is instantiated and before initializing the prompt-buffer modes. +The handlers take the prompt-buffer as argument.") + (prompt-buffer-ready-hook + (make-instance 'hook-prompt-buffer) + :type hook-prompt-buffer + :documentation "A hook one can attach to waiting for the prompt buffer to be available. The handlers take the prompt-buffer as argument.") (before-download-hook (make-instance 'hook-download) @@ -363,6 +368,7 @@ restored." (restore-session) (load-start-urls urls) (ffi-buffer-delete dummy)) + (hooks:run-hook *after-startup-hook*) (funcall* (startup-error-reporter-function *browser*)))) ;; Catch a common case for a better error message. diff --git a/source/configuration.lisp b/source/configuration.lisp index 149ce54b1f7..78e972220fa 100644 --- a/source/configuration.lisp +++ b/source/configuration.lisp @@ -357,3 +357,39 @@ exists." (defun ensure-file-exists (pathname) (open pathname :direction :probe :if-does-not-exist :create)) + +(export-always 'on) +(defmacro on (hook args &body body) + "Attach a handler with ARGS and BODY to the HOOK. + +ARGS can be +- A symbol if there's only one argument to the callback. +- A list of arguments. +- An empty list, if the hook is void." + (let ((handler-name (gensym "on-hook-handler")) + (args (alex:ensure-list args))) + `(hooks:add-hook + ,hook (make-instance 'hooks:handler + :fn (lambda ,args + (declare (ignorable ,@args)) + ,@body) + :name (quote ,handler-name))))) + +(export-always 'once-on) +(defmacro once-on (hook args &body body) + "Attach a handler with ARGS and BODY to the HOOK. + +Remove the handler after it fires the first time. + +See `on'." + (let ((handler-name (gensym "once-on-hook-handler")) + (args (alex:ensure-list args))) + (alex:once-only (hook) + `(hooks:add-hook + ,hook (make-instance 'hooks:handler + :fn (lambda ,args + (declare (ignorable ,@args)) + (unwind-protect + (progn ,@body) + (hooks:remove-hook ,hook (quote ,handler-name)))) + :name (quote ,handler-name)))))) diff --git a/source/global.lisp b/source/global.lisp index 3184f3e7201..9a3c7dbd58e 100644 --- a/source/global.lisp +++ b/source/global.lisp @@ -13,6 +13,10 @@ This is useful when the browser is run from a REPL so that quitting does not close the connection.") +(defvar *headless-p* nil + "If non-nil, don't display anything. +This is convenient for testing purposes or to drive Nyxt programmatically.") + (export-always '*browser*) (defvar *browser* nil "The entry-point object to a complete instance of Nyxt. @@ -23,7 +27,7 @@ It can be initialized with It's possible to run multiple interfaces of Nyxt at the same time. You can let-bind *browser* to temporarily switch interface.") -(declaim (type hooks:hook-void *after-init-hook*)) +(declaim (type hooks:hook-void *after-init-hook* *after-startup-hook*)) (export-always '*after-init-hook*) (defvar *after-init-hook* (make-instance 'hooks:hook-void) "The entry-point object to configure everything in Nyxt. @@ -36,6 +40,11 @@ A handler can be added with: (hooks:add-hook *after-init-hook* 'my-foo-function)") +(export-always '*after-startup-hook*) +(defvar *after-startup-hook* (make-instance 'hooks:hook-void) + "Hook run when the browser is started and ready for interaction. +The handlers take no argument.") + (export-always '*swank-port*) (defvar *swank-port* 4006 "The port that Swank will open a new server on (default Emacs SLIME port diff --git a/source/prompt-buffer.lisp b/source/prompt-buffer.lisp index 65df4799910..7fa318bdbce 100644 --- a/source/prompt-buffer.lisp +++ b/source/prompt-buffer.lisp @@ -185,6 +185,7 @@ See also `hide-prompt-buffer'." ;; TODO: Add method that returns if there is only 1 source with no filter. (when prompt-buffer (push prompt-buffer (active-prompt-buffers (window prompt-buffer))) + (hooks:run-hook (prompt-buffer-ready-hook *browser*) prompt-buffer) (prompt-render prompt-buffer) (run-thread "Show prompt watcher" (let ((prompt-buffer prompt-buffer)) diff --git a/source/renderer/gtk.lisp b/source/renderer/gtk.lisp index af308c63d5e..df35e07e5aa 100644 --- a/source/renderer/gtk.lisp +++ b/source/renderer/gtk.lisp @@ -368,7 +368,8 @@ response. The BODY is wrapped with `with-protect'." (gtk:gtk-container-add gtk-object root-box-layout) (setf (slot-value *browser* 'last-active-window) window) - (gtk:gtk-widget-show-all gtk-object) + (unless *headless-p* + (gtk:gtk-widget-show-all gtk-object)) (connect-signal window "key_press_event" nil (widget event) (declare (ignore widget)) #+darwin @@ -1058,7 +1059,8 @@ See `gtk-browser's `modifier-translator' slot." (let ((old-buffer (active-buffer window))) (gtk:gtk-container-remove (main-buffer-container window) (gtk-object old-buffer)) (gtk:gtk-box-pack-start (main-buffer-container window) (gtk-object buffer) :expand t :fill t) - (gtk:gtk-widget-show (gtk-object buffer)) + (unless *headless-p* + (gtk:gtk-widget-show (gtk-object buffer))) (when focus (gtk:gtk-widget-grab-focus (gtk-object buffer))) (nyxt/web-extensions::tabs-on-activated old-buffer buffer) @@ -1076,7 +1078,8 @@ See `gtk-browser's `modifier-translator' slot." (push buffer (panel-buffers-right window)))) (setf (gtk:gtk-widget-size-request (gtk-object buffer)) (list (width buffer) -1)) - (gtk:gtk-widget-show (gtk-object buffer))) + (unless *headless-p* + (gtk:gtk-widget-show (gtk-object buffer)))) (define-ffi-method ffi-window-set-panel-buffer-width ((window gtk-window) (buffer panel-buffer) width) "Set the width of a panel buffer." diff --git a/source/window.lisp b/source/window.lisp index a9265cacf6a..a114f55858e 100644 --- a/source/window.lisp +++ b/source/window.lisp @@ -34,6 +34,7 @@ It's a function of the window argument that returns the title as a string.") :export nil :type boolean :documentation "Whether the window is displayed in fullscreen.") + ;; TODO: each frame should have a status buffer, not each window (status-buffer :export nil) (message-buffer-height diff --git a/tests/renderer-online/set-url.lisp b/tests/renderer-online/set-url.lisp new file mode 100644 index 00000000000..8e3e8c37775 --- /dev/null +++ b/tests/renderer-online/set-url.lisp @@ -0,0 +1,55 @@ +;;;; SPDX-FileCopyrightText: Atlas Engineer LLC +;;;; SPDX-License-Identifier: BSD-3-Clause + +(in-package :nyxt/tests/renderer) + +(plan nil) + +(class-star:define-class nyxt-user::test-data-profile (nyxt:data-profile) + ((nyxt:name :initform "test")) + (:documentation "Test profile.")) + +(defmethod nyxt:expand-data-path ((profile nyxt-user::test-data-profile) (path nyxt:data-path)) + "Don't persist data" + nil) + +(defmacro with-prompt-buffer-test (command &body body) + (alexandria:with-gensyms (thread) + `(let ((,thread (bt:make-thread (lambda () ,command)))) + (calispel:? (prompt-buffer-channel (current-window))) + ,@body + (return-selection) + (bt:join-thread ,thread)))) + +;; TODO: Use `with-prompt-buffer-test'. +;; (with-prompt-buffer-test (set-url) +;; (update-prompt-input (current-prompt-buffer) "foobar")) + +(subtest "set-url" + (let ((url-channel (nyxt::make-channel 1)) + (url "http://example.org/")) + (setf nyxt::*headless-p* t) + (on nyxt:*after-startup-hook* () + (let ((buffer-loaded-channel (nyxt::make-channel 1))) + (once-on (buffer-loaded-hook (current-buffer)) buffer + (calispel:! buffer-loaded-channel t)) + (let ((thread (run-thread "run set-url" + (calispel:? buffer-loaded-channel 10) + (nyxt:set-url)))) + (declare (ignorable thread)) + (on (prompt-buffer-ready-hook *browser*) + (prompt-buffer) + (sleep 1) + (nyxt::set-prompt-buffer-input url prompt-buffer) + (prompter:all-ready-p prompt-buffer) + (nyxt/prompt-buffer-mode:return-selection prompt-buffer) + (once-on (buffer-loaded-hook (current-buffer)) buffer + (calispel:! url-channel (nyxt:render-url (nyxt:url buffer))))) + ;; (bt:join-thread thread) ; TODO: Make sure thread always returns. + ))) + (nyxt:start :no-init t :no-auto-config t + :socket "/tmp/nyxt-test.socket" + :data-profile "test") + (prove:is (calispel:? url-channel 10) url))) + +(finalize) diff --git a/tests/renderer-package.lisp b/tests/renderer-package.lisp new file mode 100644 index 00000000000..d74f9e1de5c --- /dev/null +++ b/tests/renderer-package.lisp @@ -0,0 +1,8 @@ +;;;; SPDX-FileCopyrightText: Atlas Engineer LLC +;;;; SPDX-License-Identifier: BSD-3-Clause + +(uiop:define-package nyxt/tests/renderer + (:use #:common-lisp) + (:use #:nyxt) + (:use #:prove) + (:import-from #:class-star #:define-class))