;;; config/tutorial/autoload/tutorial.el -*- lexical-binding: t; -*- (defvar doom-tutorial-hist-file (expand-file-name "tutorial-progress.el" doom-cache-dir) "Directory where tutorial progress information is saved.") (defvar doom-tutorial--progress (when (file-exists-p doom-tutorial-hist-file) (with-temp-buffer (insert-file-contents doom-tutorial-hist-file) (read (current-buffer)))) "An alist of tutorials and progress information.") (defun doom-tutorial--save-progress () "Write `doom-tutorial--progress' to `doom-tutorial-hist-file'." (with-temp-buffer (insert ";; -*- mode: emacs-lisp -*-\n" ";; Tutorial progress file, automatically generated by `doom-tutorial--save-progress'.\n" "\n") (let ((print-length nil) (print-level nil) (print-quoted t)) (prin1 doom-tutorial--progress (current-buffer))) (insert ?\n) (condition-case err (write-region (point-min) (point-max) doom-tutorial-hist-file nil (unless (called-interactively-p 'interactive) 'quiet)) (file-error (lwarn '(doom-tutorial-hist-file) :warning "Error writing `%s': %s" doom-tutorial-hist-file (caddr err)))))) (defvar doom-tutorials--loaded nil "An alist of loaded tutorials.") (defun doom-tutorial-run (name) "Run the tutorial NAME." (when-let ((tutorial (cdr (assoc name doom-tutorials--loaded)))) (eval (plist-get tutorial :setup)))) (defun doom-tutorial-run-maybe (name) (unless (plist-get (cdr (assoc name doom-tutorial--progress)) :skipped) (pcase (read-char-choice (format "Do you want to run the %s tutorial? (y)es/(l)ater/(n)ever: " (propertize (symbol-name name) 'face 'bold)) '(?y ?l ?n)) (?y (doom-tutorial-run name)) (?n (plist-put (cdr (assoc name doom-tutorial--progress)) :skipped nil))))) (defun doom-tutorial-normalise-plist (somelist) (cdr (cl-reduce (lambda (result new) (if (keywordp new) (progn (push new result) (push nil result)) (push new (car result))) result) (nreverse somelist) :initial-value (list nil)))) ;;;###autoload (defmacro define-tutorial! (name &optional docstring &rest body) (declare (doc-string 2) (indent defun)) (unless (stringp docstring) (push docstring body) (setq docstring nil)) (let ((parameters (doom-tutorial-normalise-plist body))) (when (plist-get parameters :setup) (plist-put parameters :setup (append (list #'progn) (plist-get parameters :setup)))) (when (plist-get parameters :teardown) (plist-put parameters :teardown (append (list #'progn) (plist-get parameters :teardown)))) (when (plist-get parameters :pages) (plist-put parameters :pages (mapcar (lambda (page) (if (eq 'page (car page)) (eval `(doom-tutorial-page! ,@(cdr page))) page)) (plist-get parameters :pages)))) `(progn (defun ,(intern (format "doom-tutorial-%s" name)) (&optional autotriggered) ,docstring (interactive "p") (if autotriggered (doom-tutorial-run ',name) (doom-tutorial-run-maybe ',name))) (doom-tutorial-register ',name ',parameters)))) (defmacro doom-tutorial-page! (&rest body) (let ((parameters (doom-tutorial-normalise-plist body))) (dolist (strparam '(:instructions :title)) (when-let ((paramvalue (plist-get parameters strparam))) (plist-put parameters strparam (if (cl-every #'stringp paramvalue) (apply #'concat paramvalue) `(lambda () (concat ,@paramvalue)))))) (when-let ((test (plist-get parameters :test))) (plist-put parameters :test (cond ((functionp test) test) ((consp test) `(lambda () ,@test)) (_ (error "Test is invalid. %S" test))))) `(list ,@parameters))) (defun doom-tutorial-register (name parameters) (push (cons name parameters) doom-tutorials--loaded) (unless (assoc name doom-tutorial--progress) (push (list name :skipped nil :complete nil :page 0) doom-tutorial--progress)) (dolist (target (plist-get parameters :triggers)) (advice-add target :after name)) (dolist (filepattern (plist-get parameters :file-triggers)) (add-to-list 'doom-tutorials--file-triggers (cons (eval filepattern) name)))) ;;;###autoload (defun doom-tutorial-load-modules () (let (loaded-tutorials) (maphash (lambda (key _plist) (let ((tutorial-file (doom-module-path (car key) (cdr key) "tutorial.el"))) (when (file-exists-p tutorial-file) (push (cdr key) loaded-tutorials) (load tutorial-file 'noerror 'nomessage)))) doom-modules) loaded-tutorials)) (defvar doom-tutorial-workspace-name "*tutorial*") (defvar doom-tutorial--old-windowconf) (defvar doom-tutorial--scratchpad-buffer-name "*tutorial scratchpad*") (defvar doom-tutorial--scratchpad-window) (defvar doom-tutorial--instructions-buffer-name "*tutorial instructions*") (defvar doom-tutorial--cmd-log-buffer-name "*tutorial cmd-log*") (defun doom-tutorial-setup-3-window () (if (featurep! :ui workspaces) (progn (unless (+workspace-buffer-list) (+workspace-delete (+workspace-current-name))) (+workspace-switch doom-tutorial-workspace-name t)) (setq doom-tutorial--old-windowconf (current-window-configuration)) (delete-other-windows) (switch-to-buffer (doom-fallback-buffer))) ;; Setup do buffer (setq doom-tutorial--scratchpad-window (selected-window)) (switch-to-buffer (get-buffer-create doom-tutorial--scratchpad-buffer-name)) (erase-buffer) (setq-local header-line-format "Scratch pad") ;; Setup instruction buffer (split-window nil nil 'right) (select-window (next-window)) (switch-to-buffer (get-buffer-create doom-tutorial--instructions-buffer-name)) (erase-buffer) (read-only-mode 1) (hide-mode-line-mode 1) (setq-local header-line-format "Instructions") ;; Setup cmd log buffer (split-window nil (max window-min-height (/ (window-height) 3)) 'above) (switch-to-buffer (get-buffer-create doom-tutorial--cmd-log-buffer-name)) (erase-buffer) (hide-mode-line-mode 1) (setq-local header-line-format "Command log") (select-window doom-tutorial--scratchpad-window)) (defun doom-tutorial-quit () (interactive) (cond ((and (featurep! :ui workspaces) (+workspace-exists-p doom-tutorial-workspace-name)) (+workspace/delete doom-tutorial-workspace-name)) (doom-tutorial--old-windowconf (set-window-configuration doom-tutorial--old-windowconf) (setq doom-tutorial--old-windowconf nil))) (doom-tutorial--save-progress))