;;; keywiz.el --- Emacs key sequence quiz ;; Copyright (C) 2002, 2003 Jesper Harder ;; Author: Jesper Harder ;; Created: 15 Apr 2002 ;; Version: 1.4 ;; Location: ;; Keywords: games, keyboard ;; This program is free software; you can redistribute it and/or ;; modify it under the terms of the GNU General Public License as ;; published by the Free Software Foundation; either version 2 of ;; the License, or (at your option) any later version. ;; ;; This program is distributed in the hope that it will be ;; useful, but WITHOUT ANY WARRANTY; without even the implied ;; warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR ;; PURPOSE. See the GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public ;; License along with this program; if not, write to the Free ;; Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, ;; MA 02111-1307 USA ;; ;;; Commentary: ;; keywiz.el drills you about Emacs key-bindings. You're presented ;; with the name of a command and the docstring, and then prompted for ;; the correct key sequence. You'll earn one point for each correct ;; answer during the time limit. ;; ;; Invoke with `M-x keywiz'. A prefix argument will force keywiz to ;; rescan the key-binding -- this is useful if you want to include ;; bindings from a different mode. ;; ;; Are you a true Emacs key-binding wizard or just a poor vi looser? ;; Get your foot-pedals in position and see how many key-bindings you ;; can remember in two minutes. ;; ;; Forget about your Nethack high-score -- surely, knowing how to ;; wield the powers of the One True Editor at your fingertips will ;; earn you more bragging rights than ascending bare-footed in some ;; silly game with vi-keybindings :-) ;;; History: ;; Changes in version 1.4: ;; ;; * Copy the current local and global keymap to the keywiz buffer. ;; * Exclude `undefined'. ;; ;; Changes in version 1.3: ;; ;; Don't require cl at run-time. Doc and customize fixes. Exclude ;; `select-window'. ;; ;; Changes in version 1.2: ;; Patch from Luke Gorrie : ;; ;; * Cache key-bindings to make start up faster. ;; ;; * Pressing `r' will pause keywiz and enter a recursive edit in the ;; *scratch* buffer. ;; ;; Changes in version 1.1: ;; It now works in XEmacs -- patch from Hrvoje Niksic ;; ;;; Code: (require 'timer) (eval-when-compile (require 'cl)) (require 'gamegrid) (defgroup keywiz nil "Emacs key sequence quiz." :version "21.2" :group 'games :group 'keyboard :link '(emacs-commentary-link "keywiz.el")) (defface keywiz-wrong-face '((t (:foreground "Red"))) "Face for wrong answers." :group 'keywiz) (defface keywiz-right-face '((t (:foreground "dark green"))) "Face for right answers." :group 'keywiz) (defface keywiz-command-face '((t (:foreground "Blue" :weight bold :height 1.2 :inherit 'variable-pitch))) "Face for the Emacs commands." :group 'keywiz) (defface keywiz-heading-face '((t (:weight bold :height 1.5 :inherit 'variable-pitch))) "Face for headings." :group 'keywiz) (defcustom keywiz-brief-flag t "Non-nil means that only the first line of the doc-string is displayed." :type '(choice (const t) (const nil)) :group 'keywiz) (defvar keywiz-cached-commands nil "Command list from the previous run.") (defconst keywiz-right-phrases '("Excellent!" "Yes." "Indeed." "You're right." "t." "Spot-on." "Correct." "Yep.")) (defconst keywiz-wrong-phrases '("Nope." "Wrong." "Nah." "nil." "No." "Incorrect." "(beep)" "Huh?" "Nay.")) (defvar keywiz-temp-dir (if (fboundp 'temp-directory) (temp-directory) temporary-file-directory)) (defvar keywiz-score-file (if (fboundp 'gamegrid-add-score-insecure) "keywiz-scores" (expand-file-name "keywiz-scores" keywiz-temp-dir)) "File for holding high scores.") (defconst keywiz-time-limit 120) (defvar keywiz-not-key-regexp (regexp-opt '("mouse" "frame" "menu-bar" "mode-line" "compose-last-chars" "vertical-line" "vertical-scroll-bar" "header-line" "select-window"))) (defvar keywiz-global-map nil) (defvar keywiz-local-map nil) (when (featurep 'xemacs) (defun keywiz-events-to-keys (vector) (map 'vector #'identity (delq nil (mapcar (lambda (el) (cond ((key-press-event-p el) (let ((mods (event-modifiers el)) (key (event-key el))) (when (characterp key) (setq key (intern (make-string 1 key)))) (if mods (append mods (list key)) key))) ((or (symbolp el) (characterp el) (listp el)) el))) vector))))) (defun keywiz-key-press-event-p (x) "Return t if X is a keyboard event." (let ((event (append x nil))) (when (consp event) (setq event (car event))) (if (symbolp event) (not (string-match keywiz-not-key-regexp (prin1-to-string (car (get event 'event-symbol-elements))))) t))) (defun keywiz-insert-with-face (face &rest str) "Insert STR with face FACE." (put-text-property (point) (progn (mapcar 'insert str) (point)) 'face face)) ;; Emacs 20.7 doesn't have float-time (defalias 'keywiz-float-time (if (fboundp 'float-time) 'float-time (lambda () (let ((s (current-time))) (+ (* (car s) 65536.0) (cadr s)))))) (defun keywiz-random (x) "Return random element from list X." (nth (random (length x)) x)) (defun keywiz (rescan) "Start the key sequence quiz. If RESCAN (the prefix) is non-nil, force a rescan of the key bindings. Keywiz uses the key bindings for the buffer where it is invoked. Press `q' to quit before the two minute time limit is over. Press `r' to pause keywiz and enter a recursive edit in the *scratch* buffer." (interactive "P") (let ((score 0) (first t) commands start-time timer doc key input quit) (random t) (message "Finding key bindings...") ;; It's probably *much* faster to find the bindings by searching ;; the keymaps -- but this is easier: (if (and (not rescan) keywiz-cached-commands) (progn (setq commands keywiz-cached-commands) (message "Finding all key bindings...cached (use C-u to rescan)")) (do-all-symbols (sym) (when (and (commandp sym) (not (memq sym '(self-insert-command digit-argument undefined)))) (let ((keys (apply 'nconc (mapcar (lambda (key) (when (keywiz-key-press-event-p key) (list key))) (where-is-internal sym))))) ;; Politically incorrect, but clearer version of the above: ;; (let ((keys (delete-if-not 'keywiz-key-press-event-p ;; (where-is-internal sym)))) (and keys (push (list sym keys) commands))))) (setq keywiz-cached-commands commands) (message "Finding key bindings...done") (setq keywiz-global-map (current-global-map) keywiz-local-map (current-local-map))) (setq start-time (keywiz-float-time)) (switch-to-buffer (get-buffer-create "*keywiz*")) (use-global-map keywiz-global-map) (use-local-map keywiz-local-map) (make-local-variable 'mode-line-format) ;; Use unwind-protect to make sure the timer is killed. (unwind-protect (progn (setq timer (run-with-timer 0 1 (lambda () (setq mode-line-format (list "(keywiz) Score: " (number-to-string score) " Time left: " (number-to-string (round (- keywiz-time-limit (- (keywiz-float-time) start-time)))))) (force-mode-line-update)))) (erase-buffer) (keywiz-insert-with-face 'keywiz-heading-face "Welcome to keywiz\n") (insert "There are currently " (number-to-string (length commands)) (substitute-command-keys " commands. Your current score and the time left is displayed in the mode line. Press `q' to quit before the time limit is over. Press `r' to pause and enter a recursive edit, `\\[exit-recursive-edit]' (exit-recursive-edit) will return to keywiz. This allows you to try out a command.")) (while (and commands (not quit) (< (- (keywiz-float-time) start-time) keywiz-time-limit)) (setq commands (remove (setq key (keywiz-random commands)) commands)) (keywiz-insert-with-face 'keywiz-command-face "\n\n" (prin1-to-string (car key))) (when (setq doc (documentation (car key))) (insert "\n" (if keywiz-brief-flag (substring doc 0 (string-match "\n" doc)) doc))) (insert "\n") ;; We need to recenter the first one differently because the ;; intro is longer. (if (not first) (recenter 6) (recenter 8) (setq first nil)) (if (featurep 'xemacs) (setq input (keywiz-events-to-keys (read-key-sequence "?"))) (setq input (read-key-sequence-vector "?"))) (cond ((member input (cadr key)) (keywiz-insert-with-face 'keywiz-right-face (keywiz-random keywiz-right-phrases) " The answer is: " (mapconcat 'key-description (cadr key) ", ")) (incf score)) ((setq quit (and (member input '([?q] [q])) (y-or-n-p "Do you want to quit? ")))) ((member input '([?r] [r])) ;; 'r' means recursive muck-around with the timer stopped (keywiz-insert-with-face 'keywiz-wrong-face "Time-out time-out! Question skipped.") (cancel-timer timer) (let ((timeout-start (keywiz-float-time))) (cancel-timer timer) (save-excursion (save-window-excursion (switch-to-buffer-other-window (get-buffer-create "*scratch*")) (recursive-edit))) (timer-activate timer) (incf start-time (- (keywiz-float-time) timeout-start)))) (t (keywiz-insert-with-face 'keywiz-wrong-face (keywiz-random keywiz-wrong-phrases) " The correct answer is: " (mapconcat 'key-description (cadr key) ", ")))))) (cancel-timer timer)) (keywiz-insert-with-face 'keywiz-heading-face "\n\nTime's up\n") (insert "You made " (number-to-string score) " points.") (gamegrid-add-score keywiz-score-file score))) (provide 'keywiz) ;;; keywiz.el ends here