Files
linuxplus/101-systemd-service-quiz.el
T
2026-06-16 11:10:28 -04:00

765 lines
18 KiB
EmacsLisp

;;; 06-linuxplus-quiz-mode.el --- linuxplus-quiz-mode
(defvar-local linuxplus-quiz-lesson-name nil)
(defvar-local linuxplus-quiz-questions nil)
(defvar-local linuxplus-quiz-user-answers nil)
(defvar-local linuxplus-quiz-current-question 0)
(defvar-local linuxplus-quiz-current-choice 0)
(defvar-local linuxplus-quiz-return-window-configuration nil)
(defvar-local linuxplus-quiz-return-buffer nil)
(defun linuxplus-quiz--question-type (question)
(plist-get question :type))
(defun linuxplus-quiz--question-prompt (question)
(plist-get question :prompt))
(defun linuxplus-quiz--question-answer (question)
(plist-get question :answer))
(defun linuxplus-quiz--question-answers (question)
(plist-get question :answers))
(defun linuxplus-quiz--question-choices (question)
(plist-get question :choices))
(defun linuxplus-quiz--current-question ()
(nth linuxplus-quiz-current-question linuxplus-quiz-questions))
(defun linuxplus-quiz--current-user-answer ()
(nth linuxplus-quiz-current-question linuxplus-quiz-user-answers))
(defun linuxplus-quiz--set-current-user-answer (answer)
(setf (nth linuxplus-quiz-current-question
linuxplus-quiz-user-answers)
answer))
(defun linuxplus-quiz--answer-marker (answer)
(cond
((eq answer 'true) "[t]")
((eq answer 'false) "[f]")
((eq answer 'yes) "[y]")
((eq answer 'no) "[n]")
((eq answer 'selected) "[*]")
(t "[]")))
(defun linuxplus-quiz--answer-string (answer)
(cond
((eq answer 'true) "true")
((eq answer 'false) "false")
((eq answer 'yes) "yes")
((eq answer 'no) "no")
((listp answer) (format "%S" answer))
((null answer) "unanswered")
(t (format "%S" answer))))
(defun linuxplus-quiz--correct-answer-for-display
(question)
(let ((type
(linuxplus-quiz--question-type question)))
(cond
((memq type '(true-false yes-no))
(linuxplus-quiz--answer-string
(linuxplus-quiz--question-answer question)))
((eq type 'multiple)
(format "%S"
(linuxplus-quiz--question-answers
question)))
(t "unknown"))))
(defun linuxplus-quiz--advance-question ()
(when (< linuxplus-quiz-current-question
(1- (length linuxplus-quiz-questions)))
(setq linuxplus-quiz-current-question
(1+ linuxplus-quiz-current-question))
(setq linuxplus-quiz-current-choice 0)))
(defun linuxplus-quiz--previous-question ()
(when (> linuxplus-quiz-current-question 0)
(setq linuxplus-quiz-current-question
(1- linuxplus-quiz-current-question))
(setq linuxplus-quiz-current-choice 0)))
(defun linuxplus-quiz--multiple-selected-p
(answer choice-index)
(and (listp answer)
(memq choice-index answer)))
(defun linuxplus-quiz--advance-choice ()
"Advance to next multiple-choice bracket."
(let* ((question
(linuxplus-quiz--current-question))
(choices
(linuxplus-quiz--question-choices
question)))
(when (and choices
(< linuxplus-quiz-current-choice
(1- (length choices))))
(setq linuxplus-quiz-current-choice
(1+ linuxplus-quiz-current-choice)))))
(defun linuxplus-quiz--toggle-current-choice ()
"Toggle current multiple-choice selection.
If selecting:
- add *
- advance to next bracket
If removing:
- clear *
- stay on current bracket."
(let* ((answer (copy-sequence
(or (linuxplus-quiz--current-user-answer)
nil)))
(choice linuxplus-quiz-current-choice)
(was-selected (memq choice answer)))
(if was-selected
;; REMOVE selection and stay.
(setq answer (delq choice answer))
;; ADD selection and advance.
(push choice answer)
(setq answer (sort answer #'<))
(linuxplus-quiz--set-current-user-answer
answer)
(linuxplus-quiz--advance-choice)
;; Return immediately.
(setq answer nil))
;; Removal path only reaches here.
(when answer
(setq answer (sort answer #'<))
(linuxplus-quiz--set-current-user-answer
answer))))
(defun linuxplus-quiz--answer-correct-p
(question user-answer)
(let ((type
(linuxplus-quiz--question-type question)))
(cond
((memq type '(true-false yes-no))
(eq user-answer
(linuxplus-quiz--question-answer
question)))
((eq type 'multiple)
(equal
(sort
(copy-sequence
(or user-answer nil))
#'<)
(sort
(copy-sequence
(linuxplus-quiz--question-answers
question))
#'<)))
(t nil))))
(defun linuxplus-quiz--question-index-at-point ()
"Return zero-based question index at point."
(save-excursion
(let ((found nil))
(when (re-search-backward
"^\\([0-9]+\\)\\.$"
nil t)
(setq found
(1-
(string-to-number
(match-string 1)))))
(when (and found
(>= found 0)
(< found
(length linuxplus-quiz-questions)))
found))))
(defun linuxplus-quiz--sync-current-position-from-point ()
"Update current question from cursor position."
(let ((question-index
(linuxplus-quiz--question-index-at-point)))
(when question-index
(setq linuxplus-quiz-current-question
question-index)
(unless
(eq
(linuxplus-quiz--question-type
(linuxplus-quiz--current-question))
'multiple)
(setq linuxplus-quiz-current-choice 0)))))
(defun linuxplus-quiz--invalid-input-message ()
(linuxplus-quiz--sync-current-position-from-point)
(let ((type
(linuxplus-quiz--question-type
(linuxplus-quiz--current-question))))
(cond
((eq type 'true-false)
(message
"Valid input: t for true, f for false."))
((eq type 'yes-no)
(message
"Valid input: y for yes, n for no."))
((eq type 'multiple)
(message
"Use the space bar to add/remove the * character."))
(t
(message "Invalid input.")))))
(defun linuxplus-quiz--set-answer-and-maybe-advance
(answer)
"Set ANSWER.
If unanswered, advance.
If replacing, stay."
(linuxplus-quiz--sync-current-position-from-point)
(let ((old-answer
(linuxplus-quiz--current-user-answer)))
(linuxplus-quiz--set-current-user-answer
answer)
(unless old-answer
(linuxplus-quiz--advance-question))
(linuxplus-quiz-render)))
(defun linuxplus-quiz-render ()
(let ((inhibit-read-only t))
(erase-buffer)
(insert "Linux+ Quiz\n")
(insert "===========\n\n")
(let ((current-type
(linuxplus-quiz--question-type
(linuxplus-quiz--current-question))))
(cond
((eq current-type 'true-false)
(insert
"Type 't' for true or 'f' for false inside the brackets.\n"))
((eq current-type 'yes-no)
(insert
"Type 'y' for yes or 'n' for no inside the brackets.\n"))
((eq current-type 'multiple)
(insert
"Use the space bar to add/remove the * character for multiple choice questions.\n"))
(t
(insert "Answer the question.\n"))))
(insert
"b = back, TAB = forward, x = finish, q = quit\n\n")
(dotimes (i
(length linuxplus-quiz-questions))
(let* ((question
(nth i linuxplus-quiz-questions))
(type
(linuxplus-quiz--question-type
question))
(prompt
(linuxplus-quiz--question-prompt
question))
(user-answer
(nth i linuxplus-quiz-user-answers))
(current-question-p
(= i linuxplus-quiz-current-question)))
(insert (format "%d.\n" (1+ i)))
(insert (format " %s\n" prompt))
(cond
((eq type 'true-false)
(insert
(format
"%s %s True or False\n\n"
(if current-question-p ">" " ")
(linuxplus-quiz--answer-marker
user-answer))))
((eq type 'yes-no)
(insert
(format
"%s %s Yes or No\n\n"
(if current-question-p ">" " ")
(linuxplus-quiz--answer-marker
user-answer))))
((eq type 'multiple)
(let ((choices
(linuxplus-quiz--question-choices
question)))
(dotimes
(choice-index
(length choices))
(let*
((selected
(if
(linuxplus-quiz--multiple-selected-p
user-answer
choice-index)
'selected
nil))
(choice-current-p
(and current-question-p
(= choice-index
linuxplus-quiz-current-choice))))
(insert
(format
"%s %s %s\n"
(if choice-current-p ">" " ")
(linuxplus-quiz--answer-marker
selected)
(nth choice-index choices))))))
(insert "\n")))))
(insert
"\nReminder: x = finish quiz, q = quit/return to lesson.\n"))
(linuxplus-quiz-goto-current-answer))
(defun linuxplus-quiz-goto-current-answer ()
(goto-char (point-min))
(let ((target-question
(format "^%d\\.$"
(1+
linuxplus-quiz-current-question))))
(when
(re-search-forward
target-question
nil t)
(let ((type
(linuxplus-quiz--question-type
(linuxplus-quiz--current-question))))
(cond
((memq type '(true-false yes-no))
(when
(re-search-forward
"\\[[^]]*\\]"
nil t)
(backward-char 1)))
((eq type 'multiple)
(dotimes
(_ linuxplus-quiz-current-choice)
(re-search-forward
"\\[[^]]*\\]"
nil t))
(when
(re-search-forward
"\\[[^]]*\\]"
nil t)
(backward-char 1))))))))
(defun linuxplus-quiz-answer-true ()
(interactive)
(linuxplus-quiz--sync-current-position-from-point)
(if
(eq
(linuxplus-quiz--question-type
(linuxplus-quiz--current-question))
'true-false)
(linuxplus-quiz--set-answer-and-maybe-advance
'true)
(linuxplus-quiz--invalid-input-message)))
(defun linuxplus-quiz-answer-false ()
(interactive)
(linuxplus-quiz--sync-current-position-from-point)
(if
(eq
(linuxplus-quiz--question-type
(linuxplus-quiz--current-question))
'true-false)
(linuxplus-quiz--set-answer-and-maybe-advance
'false)
(linuxplus-quiz--invalid-input-message)))
(defun linuxplus-quiz-answer-yes ()
(interactive)
(linuxplus-quiz--sync-current-position-from-point)
(if
(eq
(linuxplus-quiz--question-type
(linuxplus-quiz--current-question))
'yes-no)
(linuxplus-quiz--set-answer-and-maybe-advance
'yes)
(linuxplus-quiz--invalid-input-message)))
(defun linuxplus-quiz-answer-no ()
(interactive)
(linuxplus-quiz--sync-current-position-from-point)
(if
(eq
(linuxplus-quiz--question-type
(linuxplus-quiz--current-question))
'yes-no)
(linuxplus-quiz--set-answer-and-maybe-advance
'no)
(linuxplus-quiz--invalid-input-message)))
(defun linuxplus-quiz-toggle-multiple ()
(interactive)
(linuxplus-quiz--sync-current-position-from-point)
(if
(eq
(linuxplus-quiz--question-type
(linuxplus-quiz--current-question))
'multiple)
(progn
(linuxplus-quiz--toggle-current-choice)
(linuxplus-quiz-render))
(linuxplus-quiz--invalid-input-message)))
(defun linuxplus-quiz-next ()
(interactive)
(linuxplus-quiz--sync-current-position-from-point)
(let ((type
(linuxplus-quiz--question-type
(linuxplus-quiz--current-question))))
(if
(and
(eq type 'multiple)
(< linuxplus-quiz-current-choice
(1-
(length
(linuxplus-quiz--question-choices
(linuxplus-quiz--current-question))))))
(setq linuxplus-quiz-current-choice
(1+
linuxplus-quiz-current-choice))
(linuxplus-quiz--advance-question)))
(linuxplus-quiz-render))
(defun linuxplus-quiz-prev ()
(interactive)
(linuxplus-quiz--sync-current-position-from-point)
(let ((type
(linuxplus-quiz--question-type
(linuxplus-quiz--current-question))))
(if
(and
(eq type 'multiple)
(> linuxplus-quiz-current-choice 0))
(setq linuxplus-quiz-current-choice
(1-
linuxplus-quiz-current-choice))
(linuxplus-quiz--previous-question)))
(linuxplus-quiz-render))
(defun linuxplus-quiz-invalid-key ()
(interactive)
(linuxplus-quiz--invalid-input-message))
(defun linuxplus-quiz-return-to-lesson ()
"Return to lesson/window layout."
(interactive)
(let ((config
linuxplus-quiz-return-window-configuration)
(buffer
linuxplus-quiz-return-buffer))
(when
(window-configuration-p config)
(set-window-configuration config))
(when
(buffer-live-p buffer)
(switch-to-buffer buffer))))
(defun linuxplus-quiz-finish ()
(interactive)
(let ((score 0)
(total
(length linuxplus-quiz-questions)))
(dotimes (i total)
(when
(linuxplus-quiz--answer-correct-p
(nth i linuxplus-quiz-questions)
(nth i linuxplus-quiz-user-answers))
(setq score (1+ score))))
(linuxplus-record-score
linuxplus-quiz-lesson-name
score)
(let ((inhibit-read-only t))
(erase-buffer)
(insert "Linux+ Quiz Results\n")
(insert "===================\n\n")
(insert
(format
"Lesson: %s\n"
linuxplus-quiz-lesson-name))
(insert
(format
"Score: %d/%d\n\n"
score total))
(insert "Detailed review:\n\n")
(dotimes (i total)
(let*
((question
(nth i linuxplus-quiz-questions))
(prompt
(linuxplus-quiz--question-prompt
question))
(user-answer
(nth i linuxplus-quiz-user-answers)))
(insert
(format
"%d. %s\n"
(1+ i)
prompt))
(insert
(format
" Your answer: %s\n"
(linuxplus-quiz--answer-string
user-answer)))
(insert
(format
" Correct answer: %s\n"
(linuxplus-quiz--correct-answer-for-display
question)))
(insert
(format
" Result: %s\n\n"
(if
(linuxplus-quiz--answer-correct-p
question
user-answer)
"correct"
"incorrect")))))
(insert
"Press q to return to the lesson.\n"))
(goto-char (point-min))
(linuxplus-quiz-mode)
(message
"Quiz complete. Score: %d/%d"
score total)))
(setq linuxplus-quiz-mode-map
(let ((map
(make-sparse-keymap)))
;; invalid printable input
(dotimes (i 95)
(define-key
map
(char-to-string (+ i 32))
#'linuxplus-quiz-invalid-key))
;; valid answers
(define-key map
(kbd "t")
#'linuxplus-quiz-answer-true)
(define-key map
(kbd "f")
#'linuxplus-quiz-answer-false)
(define-key map
(kbd "y")
#'linuxplus-quiz-answer-yes)
(define-key map
(kbd "n")
#'linuxplus-quiz-answer-no)
(define-key map
(kbd "SPC")
#'linuxplus-quiz-toggle-multiple)
;; navigation
(define-key map
(kbd "b")
#'linuxplus-quiz-prev)
(define-key map
(kbd "TAB")
#'linuxplus-quiz-next)
(define-key map
(kbd "x")
#'linuxplus-quiz-finish)
(define-key map
(kbd "q")
#'linuxplus-quiz-return-to-lesson)
map))
(define-derived-mode
linuxplus-quiz-mode
special-mode
"LinuxPlus-Quiz"
"Major mode for Linux+ quizzes."
(setq buffer-read-only t))
(defun linuxplus-start-quiz
(lesson questions buffer)
"Start Linux+ quiz in full window."
(let ((return-config
(current-window-configuration))
(return-buffer
(current-buffer))
(buf
(get-buffer-create buffer)))
(delete-other-windows)
(switch-to-buffer buf)
(linuxplus-quiz-mode)
(setq linuxplus-quiz-return-window-configuration
return-config)
(setq linuxplus-quiz-return-buffer
return-buffer)
(setq linuxplus-quiz-lesson-name
lesson)
(setq linuxplus-quiz-questions
questions)
(setq linuxplus-quiz-user-answers
(make-list
(length questions)
nil))
(setq linuxplus-quiz-current-question 0)
(setq linuxplus-quiz-current-choice 0)
(linuxplus-quiz-render)))
;;; 06-linuxplus-quiz-mode.el ends here