#+TITLE:NDebug
#+SUBTITLE: A toolkit to construct interface-aware yet standard-compliant debugger hooks

NDebug provides a small set of utilities to make graphical (or, rather non-REPL-resident) Common Lisp applications easier to integrate with the standard Lisp debugger (~*debugger-hook*~, namely) and implementation-specific debugger hooks (via ~trivial-custom-debugger~), especially in a multi-threaded context.

* Getting started
** Installation

NDebug is pretty light on dependencies:
- [[https://github.com/Shinmera/dissect][Dissect]].
- [[https://github.com/phoe/trivial-custom-debugger][trivial-custom-debugger]].
- [[https://github.com/sionescu/bordeaux-threads][Bordeaux Threads]].
- [[https://github.com/trivial-gray-streams/trivial-gray-streams][Trivial Gray Streams]].

** Usage
NDebug has two API layers: CLOS API and a globals&functions API. The CLOS API is more structured and I recommend to use it in most cases, while the globals&functions API overrides the CLOS methods and allows to customize the solid base CLOS provides. globals&functions is more REPL-friendly, so you may start your debugger with this, while transitioning to the CLOS API later.

*** CLOS API

To make your application debugger-friendly, you have to specialize ~ui-display~, ~ui-cleanup~, ~query-read~ and ~query-write~ methods (the latter two are not required and will be replaced with ~*query-io*~ if not present). And then use the ~make-debugger-hook~ function to set the debugger.

Note that there are
- ~ndebug:invoke~ to invoke a restart for a condition,
- and ~ndebug:evaluate~ (only works on SBCL at the moment) to evaluate the code in the context of the condition.

#+begin_src lisp
  (defclass my-wrapper (ndebug:condition-wrapper)
    ((prompt-text :initform "[prompt text]"
                  :accessor prompt-text)
     (debug-window :initform nil
                   :accessor debug-window)))

  (defmethod ndebug:query-read ((wrapper my-wrapper))
    (prompt :text (prompt-text wrapper)))

  (defmethod ndebug:query-write ((wrapper my-wrapper) (string string))
    (setf (prompt-text wrapper) string))

  (defmethod ndebug:ui-display ((wrapper my-wrapper))
    (setf (debug-window wrapper)
          (make-windown :text-contents (dissect:present (ndebug:stack wrapper) nil)
                        :buttons (loop for restart in (ndebug:restarts wrapper)
                                       collect (make-button
                                                :label (restart-name restart)
                                                :action (lambda ()
                                                          (ndebug:invoke wrapper restart)))))))

  (defmethod ndebug:ui-cleanup ((wrapper my-wrapper))
    (delete-window (debug-window wrapper)))

  (ndebug:with-debugger-hook (:wrapper-class 'my-wrapper)
    (obviously-erroring-operation))
#+end_src

*** Globals&functions API

With globals&function API you have a bit more flexibility in how you configure the debugger. You can ~let~-bind or ~flet~-bind special variables (~ndebug:*query-read*~, ~ndebug:*query-write*~, ~ndebug:*ui-display*~, ~ndebug:*ui-cleanup*~), you can ~setf~ them, you can provide them as arguments to ~ndebug:make-debugger-hook~ or ~ndebug:with-debugger-hook~. The possibilities are endless, although it tends to look less structured than the CLOS API.

#+begin_src lisp
  (defvar *prompt-text* "[prompt text]")

  (defvar *window* nil)

  (defun show-wrapper-window (wrapper)
    (setf
     ,*window*
     (make-window
      :text-contents (dissect:present (ndebug:stack wrapper) nil)
      :buttons (loop for restart in (ndebug:restarts wrapper)
                     collect (make-button
                              :label (restart-name restart)
                              :action (lambda ()
                                        (ndebug:invoke wrapper restart)))))))

  (let ((ndebug:*query-read* (lambda (wrapper)
                               (declare (ignore wrapper))
                               (prompt :text *prompt-text*))))
    (flet ((cleanup (wrapper)
             (declare (ignore wrapper))
             (delete-window *window*)))
      (setf ndebug:*ui-cleanup* #'cleanup))
    (ndebug:with-debugger-hook
        (:ui-display #'show-wrapper-window
         :query-write (setf *prompt-text* %string%))
      (obviously-erroring-operation)))
#+end_src

** Mixing the two

The good thing about these API is that you can intermix those. So, you can subclass the ~ndebug:condition-wrapper~, define some methods on it and then, if there's some corner case (like needing a custom display or custom reading function), you can always provide an additional argument to ~make-debugger-hook~ to override the initial method.

* To-Dos
- [X] Stop depending on Swank for two-way-stream construction, depend on ~trivial-gray-streams~ instead.
  - The implementation is quite basic, but it seems to work.
- [X] (Maybe) stop depending on Lparallel and depend on Bordeaux Thread semaphores/conditions instead.
  - Semaphores it is!
- [ ] Better names for handlers?
- [X] Use methods to specialize the behavior?
- [ ] (Maybe) allow falling back to ~*query-io*~ by providing ~nil~ as both ~:query-write~ and ~:query-read~.
