emacs-elpa-diffs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[nongnu] elpa/devil 46eb0a5fd6 2/2: Add devil-describe-key to describe D


From: ELPA Syncer
Subject: [nongnu] elpa/devil 46eb0a5fd6 2/2: Add devil-describe-key to describe Devil key
Date: Sun, 4 Jun 2023 21:59:52 -0400 (EDT)

branch: elpa/devil
commit 46eb0a5fd613dd7c9d174ca0bea80fab9dc56864
Author: Susam Pal <susam@susam.net>
Commit: Susam Pal <susam@susam.net>

    Add devil-describe-key to describe Devil key
---
 CHANGES.org    |   6 +
 MANUAL.org     |  76 +++++---
 Makefile       |   2 +-
 devil-tests.el | 103 ++++++-----
 devil.el       | 560 +++++++++++++++++++++++++++++++++------------------------
 5 files changed, 442 insertions(+), 305 deletions(-)

diff --git a/CHANGES.org b/CHANGES.org
index 9b403ce25f..7918132b4a 100644
--- a/CHANGES.org
+++ b/CHANGES.org
@@ -15,6 +15,10 @@
   the source version conveniently.  It helps during troubleshooting
   the package when installed from MELPA which sets the package version
   to a version derived from the current date and time.
+- Command =devil-describe-key= to describe Devil key sequences.
+- Command =devil-toggle-logging= to toggle logging.
+- Special key =, h , k= to execute =devil-describe-key=.
+- Special key =, h , l= to execute =devil-toggle-logging=.
 
 *** Changed
 
@@ -26,6 +30,8 @@
   x <tab>= is converted to =C-x <tab>=, since no command is bound to
   this key sequence, it is further translated to =C-x TAB= and the
   command =indent-rigidly= bound to it is executed.
+- Format control sequence to show the Devil key sequence read by Devil
+  has changed from =%k= to =%r=.
 
 *** Fixed
 
diff --git a/MANUAL.org b/MANUAL.org
index d37d4a4209..8700d5ffed 100644
--- a/MANUAL.org
+++ b/MANUAL.org
@@ -224,6 +224,15 @@ Devil may be used:
 13. Type =, ,= to type a single comma.  This special key is useful for
     cases when you really need to type a single literal comma.
 
+14. Type =, h , k= to invoke =devil-describe-key=.  This is a special
+    key that invokes the Devil variant of =describe-key= included in
+    vanilla Emacs.  When the key input prompt appears, type the Devil
+    key sequence =, x , f= and Devil will display the documentation of
+    the function invoked by this Devil key sequence.  Note: The key
+    sequence =, h k= translates to =C-h k= and invokes the vanilla
+    =describe=key=.  It is the Devil key sequence =, h , k= that
+    invokes =devil-describe-key=.
+
 * Typing Commas
 :PROPERTIES:
 :CUSTOM_ID: typing-commas
@@ -416,6 +425,25 @@ that are not covered by these initial rules, revisit the 
above table
 to pick up new translation rules and adopt them in your day-to-day
 usage of Devil.
 
+* Describe Devil Key
+:PROPERTIES:
+:CUSTOM_ID: devil-describe-key
+:END:
+Devil offers a command named =devil-describe-key= that can be used to
+describe a Devil key sequence.  It works similarly to the
+=describe-key= command of vanilla Emacs that can be invoked with =C-h
+k=.  The =devil-describe-key= command can be invoked with the special
+key sequence =, h , k=.  Type =, h , k= and a prompt appears to read a
+key sequence.  Type any Devil key sequence, say, =, x , f= and Devil
+immediately shows the documentation for the function invoked by this
+key sequence.
+
+Note that =, x , f= (=devil-describe-key=) can also be used to look up
+documentation for vanilla Emacs key sequences like =C-x C-f=.
+
+Also note that the Devil key sequence is =, h k= is still free to
+invoke =C-h k= (=describe-key= of vanilla Emacs).
+
 * Bonus Key Bindings
 :PROPERTIES:
 :CUSTOM_ID: bonus-key-bindings
@@ -482,7 +510,7 @@ show a Devil smiley (😈) in the modeline and in the Devil 
prompt.
 
 ** Reclaim , SPC to Set Mark
 :PROPERTIES:
-:CUSTOM_ID: reclaim-comma-spc-to-set-mark
+:CUSTOM_ID: reclaim-comma-space-to-set-mark
 :END:
 The default configuration for special keys reserves =, SPC= to insert
 a literal comma followed by space.  This default makes it easy to type
@@ -491,20 +519,19 @@ translate to =C-SPC=.  Therefore =, SPC= cannot be used 
to set mark.
 Instead, the default translation rules offer =, z SPC= as a way to set
 mark.
 
-If you would rather set mark using =, SPC= and you are happy with the
-special key =, ,= alone to insert a literal comma, then use the
+If you would rather set mark using =, SPC= and you are happy with
+typing the special key =, ,= to insert a literal comma, then use the
 following configuration:
 
 #+begin_src elisp
   (require 'devil)
   (global-devil-mode)
   (global-set-key (kbd "C-,") 'global-devil-mode)
-  (setq devil-special-keys '(("%k %k" . (lambda () (interactive) 
(devil-run-key "%k")))))
+  (assoc-delete-all "%k SPC" devil-special-keys)
 #+end_src
 
-This reduces the number of special keys so that only =, ,= is treated
-as special.  All the other special key definitions (=, SPC= was one of
-them) are removed.  As a result, =, SPC= is now translated to =C-SPC=.
+This removes the special key =, SPC= from =devil-special-keys= so that
+it is now free to be translated to =C-SPC= and invoke =set-mark-command=.
 
 ** Custom Devil Key
 :PROPERTIES:
@@ -536,19 +563,18 @@ use yet another different Devil key.
   (global-devil-mode)
   (global-set-key (kbd "C-<left>") 'global-devil-mode)
   (devil-set-key (kbd "<left>"))
-  (setq devil-special-keys '(("%k %k" . left-char)))
+  (dolist (key '("%k SPC" "%k RET" "%k <return>"))
+    (assoc-delete-all key devil-special-keys))
 #+end_src
 
 The above example sets the Devil key to the left arrow key.  With this
 configuration, we can use =<left> x <left> f= and have Devil translate
-it to =C-x C-f=.
+it to =C-x C-f=.  We can type the special key =<left> <left>= to
+produce the same effect as the original =<left>=.
 
-Additionally, the above example defines the =devil-special-keys=
-variable to have a single entry that allows typing =<left> <left>= to
-produce the same effect as the original =<left>=.  It removes the
-other entries, so that =<left> SPC= is no longer reserved as a special
-key.  Thus =<left> SPC= can now be used to set a mark like one would
-normally expect.
+The above example removes some special keys that are no longer useful.
+In particular, =<left> SPC= is no longer reserved as a special key, so
+we can use it now to set a mark.
 
 ** Multiple Devil Keys
 :PROPERTIES:
@@ -567,16 +593,20 @@ requirements:
   (require 'devil)
   (global-devil-mode)
   (define-key devil-mode-map (kbd ".") #'devil)
-  (setq devil-special-keys '((", ," . (lambda () (insert ",")))
-                             (". ." . (lambda () (insert ".")))))
-  (setq devil-translations '(("," . "C-")
+  (add-to-list 'devil-special-keys `(". ." . ,(devil-key-executor ".")))
+  (setq devil-translations '((", z" . "C-")
+                             (". z" . "M-")
+                             (", ," . ",")
+                             (". ." . ".")
+                             ("," . "C-")
                              ("." . "M-")))
 #+end_src
 
 With this configuration, we can type =, x , f= for =C-x C-f= like
 before.  But now we can also type =. x= for =M-x=.  Similarly, we can
-type =, . s= for =C-M-s= and so on.  Further, =, ,= inserts a literal
-comma and =. .= inserts a literal dot.
+type =, . s= for =C-M-s= and so on.  Also =, ,= inserts a literal
+comma and =. .= inserts a literal dot.  Further we can type =, z ,= to
+get =C-,= and =. z .= to get =M-.=.
 
 Note that by default Devil configures only one activation key (=,=)
 because the more activation keys we add, the more intrusive Devil
@@ -744,9 +774,9 @@ and preferences.
 02. I am happy with typing =, ,= every time, I need to type a comma.
     Can I free up =, SPC= to invoke =set-mark-command=?
 
-    Yes, this can be done by updating =devil-special-keys= to define
-    only =, ,= as a special key and remove the rest.  See the section
-    [[*Reclaim , SPC to Set Mark]] to find out how to do this.
+    Yes, this can be done by removing the special key =, SPC= from
+    =devil-special-keys=.  See the section [[*Reclaim , SPC to Set Mark]]
+    to find out how to do this.
 
 03. Can I make the Devil key sticky, i.e., can I type =, x f= instead
     of =, x , f= to invoke =C-x C-f=?
diff --git a/Makefile b/Makefile
index 3c24c490d9..a53562ca32 100644
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,7 @@
 checks: tests test-sentence-ends
 
 test-sentence-ends:
-       errors=$$(grep -n '[^0-9]\. [^ lswx."(]' MANUAL.org); echo "$$errors"; 
[ -z "$$errors" ]
+       errors=$$(grep -n '[^0-9]\. [^ lswxz.,"(]' MANUAL.org); echo 
"$$errors"; [ -z "$$errors" ]
        errors=$$(grep -n '\. [^ a]' README.org CHANGES.org LICENSE.org); echo 
"$$errors"; [ -z "$$errors" ]
        errors=$$(grep -n '\. [^ ]' *.el); echo "$$errors"; [ -z "$$errors" ]
        errors=$$(grep -n '[?!] [^ ]' *.org *.el); echo "$$errors"; [ -z 
"$$errors" ]
diff --git a/devil-tests.el b/devil-tests.el
index 59f9238e29..bd7c1c41d7 100644
--- a/devil-tests.el
+++ b/devil-tests.el
@@ -10,6 +10,25 @@
 (require 'ert)
 (require 'devil)
 
+(let ((devil-key (kbd "<left>")))
+  (devil-format "%k"))
+(ert-deftest devil-format ()
+  "Test if `devil-format' works as expected."
+  (let ((devil-key ","))
+    (should (string= (devil-format "%k") ","))
+    (should (string= (devil-format "Devil: %k") "Devil: ,"))
+    (should (string= (devil-format "%k %%") ", %"))
+    (should (string= (devil-format "%r => %t" (kbd ",")) ", => C-"))
+    (should (string= (devil-format "%r => %t" (kbd ", x")) ", x => C-x")))
+  (let ((devil-key (kbd "<left>")))
+    (should (string= (devil-format "%k") "<left>"))
+    (should (string= (devil-format "Devil: %k") "Devil: <left>"))
+    (should (string= (devil-format "%k %%") "<left> %"))
+    (should (string= (devil-format "%r => %t" (kbd "<left> x"))
+                     "<left> x => C-x"))
+    (should (string= (devil-format "%r => %t" (kbd "<left> x <left>"))
+                     "<left> x <left> => C-x C-"))))
+
 (ert-deftest devil-string-replace ()
   "Test if `devil-string-replace' works as expected."
   (should (string= (devil-string-replace "" "" "") ""))
@@ -62,62 +81,62 @@
   (should (not (devil--invalid-key-p "C-x C-f")))
   (should (not (devil--invalid-key-p "C-M-x"))))
 
-(ert-deftest devil-translate ()
+(ert-deftest devil--translate ()
   "Test if `devil-translate' works as expected."
   ;; Trivial translations.
-  (should (string= (devil-translate (vconcat "a")) "a"))
-  (should (string= (devil-translate (vconcat "A")) "A"))
+  (should (string= (devil--translate (vconcat "a")) "a"))
+  (should (string= (devil--translate (vconcat "A")) "A"))
   ;; Translations involving the C- modifier.
-  (should (string= (devil-translate (vconcat ",")) "C-"))
-  (should (string= (devil-translate (vconcat ",x")) "C-x"))
-  (should (string= (devil-translate (vconcat ",x,")) "C-x C-"))
-  (should (string= (devil-translate (vconcat ",x,f")) "C-x C-f"))
+  (should (string= (devil--translate (vconcat ",")) "C-"))
+  (should (string= (devil--translate (vconcat ",x")) "C-x"))
+  (should (string= (devil--translate (vconcat ",x,")) "C-x C-"))
+  (should (string= (devil--translate (vconcat ",x,f")) "C-x C-f"))
   ;; Escape hatch to type commas.
-  (should (string= (devil-translate (vconcat ",,")) ","))
-  (should (string= (devil-translate (vconcat ",,,,")) ", ,"))
+  (should (string= (devil--translate (vconcat ",,")) ","))
+  (should (string= (devil--translate (vconcat ",,,,")) ", ,"))
   ;; Translations involving M- modifier.
-  (should (string= (devil-translate (vconcat ",mx")) "C-M-x"))
-  (should (string= (devil-translate (vconcat ",mmx")) "M-x"))
-  (should (string= (devil-translate (vconcat ",mmm")) "M-m"))
+  (should (string= (devil--translate (vconcat ",mx")) "C-M-x"))
+  (should (string= (devil--translate (vconcat ",mmx")) "M-x"))
+  (should (string= (devil--translate (vconcat ",mmm")) "M-m"))
   ;; Translations involing C- and uppercase letter.
-  (should (string= (devil-translate (vconcat ",a")) "C-a"))
-  (should (string= (devil-translate (vconcat ",A")) "C-S-a"))
-  (should (string= (devil-translate (vconcat ",mA")) "C-M-S-a"))
-  (should (string= (devil-translate (vconcat ",mmA")) "M-A"))
-  (should (string= (devil-translate (vconcat ",A,mA,a")) "C-S-a C-M-S-a C-a"))
-  (should (string= (devil-translate (vconcat ",AmA,mmA,a")) "C-S-a M-A M-A 
C-a"))
+  (should (string= (devil--translate (vconcat ",a")) "C-a"))
+  (should (string= (devil--translate (vconcat ",A")) "C-S-a"))
+  (should (string= (devil--translate (vconcat ",mA")) "C-M-S-a"))
+  (should (string= (devil--translate (vconcat ",mmA")) "M-A"))
+  (should (string= (devil--translate (vconcat ",A,mA,a")) "C-S-a C-M-S-a C-a"))
+  (should (string= (devil--translate (vconcat ",AmA,mmA,a")) "C-S-a M-A M-A 
C-a"))
   ;; Translations involving C- and RET.
-  (should (string= (devil-translate (vconcat ",\r")) "C-RET"))
-  (should (string= (devil-translate (vconcat ",m\r")) "C-M-RET"))
-  (should (string= (devil-translate (vconcat ",mm\r")) "M-RET"))
-  (should (string= (devil-translate (vconcat ",\r,R,mm\r")) "C-RET C-S-r 
M-RET"))
+  (should (string= (devil--translate (vconcat ",\r")) "C-RET"))
+  (should (string= (devil--translate (vconcat ",m\r")) "C-M-RET"))
+  (should (string= (devil--translate (vconcat ",mm\r")) "M-RET"))
+  (should (string= (devil--translate (vconcat ",\r,R,mm\r")) "C-RET C-S-r 
M-RET"))
   ;; Translations provided in the manual as examples.
-  (should (string= (devil-translate (vconcat ",s")) "C-s"))
-  (should (string= (devil-translate (vconcat ",ms")) "C-M-s"))
-  (should (string= (devil-translate (vconcat ",mmx")) "M-x"))
-  (should (string= (devil-translate (vconcat ",c,,")) "C-c ,"))
-  (should (string= (devil-translate (vconcat ",cmm")) "C-c m"))
-  (should (string= (devil-translate (vconcat ",z ")) "C-SPC"))
-  (should (string= (devil-translate (vconcat ",zz")) "C-z"))
-  (should (string= (devil-translate (vconcat ",z,")) "C-,"))
-  (should (string= (devil-translate (vconcat ",cmzm")) "C-c M-m"))
-  (should (string= (devil-translate (vconcat ",mzm")) "C-M-m")))
+  (should (string= (devil--translate (vconcat ",s")) "C-s"))
+  (should (string= (devil--translate (vconcat ",ms")) "C-M-s"))
+  (should (string= (devil--translate (vconcat ",mmx")) "M-x"))
+  (should (string= (devil--translate (vconcat ",c,,")) "C-c ,"))
+  (should (string= (devil--translate (vconcat ",cmm")) "C-c m"))
+  (should (string= (devil--translate (vconcat ",z ")) "C-SPC"))
+  (should (string= (devil--translate (vconcat ",zz")) "C-z"))
+  (should (string= (devil--translate (vconcat ",z,")) "C-,"))
+  (should (string= (devil--translate (vconcat ",cmzm")) "C-c M-m"))
+  (should (string= (devil--translate (vconcat ",mzm")) "C-M-m")))
 
-(ert-deftest devil-fallback-key ()
-  "Test if `devil-fallback-key' works as expected."
+(ert-deftest devil--fallback-key ()
+  "Test if `devil--fallback-key' works as expected."
   (let ((local-function-key-map (make-sparse-keymap)))
     ;; Define bindings for fallback.
     (define-key local-function-key-map (kbd "<tab>") (kbd "TAB"))
     (define-key local-function-key-map (kbd "M-<return>") (kbd "M-RET"))
     ;; Test translation
-    (should (string= (devil-fallback-key "") nil))
-    (should (string= (devil-fallback-key "a") nil))
-    (should (string= (devil-fallback-key "<return>") nil))
-    (should (string= (devil-fallback-key "C-<tab>") nil))
-    (should (string= (devil-fallback-key "C-<return>") nil))
-    (should (string= (devil-fallback-key "<tab>") "TAB"))
-    (should (string= (devil-fallback-key "M-<return>") "M-RET"))
-    (should (string= (devil-fallback-key "C-<tab> M-<return>") "C-<tab> 
M-RET"))))
+    (should (string= (devil--fallback-key "") nil))
+    (should (string= (devil--fallback-key "a") nil))
+    (should (string= (devil--fallback-key "<return>") nil))
+    (should (string= (devil--fallback-key "C-<tab>") nil))
+    (should (string= (devil--fallback-key "C-<return>") nil))
+    (should (string= (devil--fallback-key "<tab>") "TAB"))
+    (should (string= (devil--fallback-key "M-<return>") "M-RET"))
+    (should (string= (devil--fallback-key "C-<tab> M-<return>") "C-<tab> 
M-RET"))))
 
 (provide 'devil-tests)
 ;;; devil-tests.el ends here
diff --git a/devil.el b/devil.el
index 993eb4b84b..b98788f301 100644
--- a/devil.el
+++ b/devil.el
@@ -39,12 +39,16 @@
 ;; translations.
 
 ;;; Code:
+
+
+;;; Customization ====================================================
+
 (defgroup devil '()
   "Minor mode for translating key sequences."
   :prefix "devil-"
   :group 'editing)
 
-(defconst devil-version "0.5.0-beta1"
+(defconst devil-version "0.5.0-beta2"
   "Devil version string.")
 
 (defvar devil-mode-map (make-sparse-keymap)
@@ -59,6 +63,12 @@ activate Devil.")
   "Non-nil iff Devil should print log messages."
   :type 'boolean)
 
+(defun devil-toggle-logging ()
+  "Toggle the value of `devil-logging'."
+  (interactive)
+  (setq devil-logging (not devil-logging))
+  (message "Devil: Logging %s" (if devil-logging "enabled" "disabled")))
+
 (defun devil--log (format-string &rest args)
   "Write log message with the given FORMAT-STRING and ARGS."
   (when devil-logging
@@ -75,10 +85,6 @@ sequence given in VALUE activates Devil."
   (define-key devil-mode-map value #'devil)
   (devil--log "Keymap updated to %s" devil-mode-map))
 
-(defcustom devil-lighter " Devil"
-  "String displayed on the mode line when Devil mode is enabled."
-  :type 'string)
-
 (defcustom devil-key ","
   "The key sequence that begins Devil input.
 
@@ -90,41 +96,66 @@ updated value of this variable."
   :type 'key-sequence
   :set #'devil--custom-devil-key)
 
-(defun devil-set-key (key-sequence)
-  "Set `devil-key' to the given KEY-SEQUENCE and update `devil-mode-map'.
-
-This function clears existing key bindings in `devil-mode-map'
-and sets a single key binding in this keymap so that Devil can be
-activated using the given KEY-SEQUENCE."
-  (devil--custom-devil-key 'devil-key key-sequence))
-
-;;;###autoload
-(define-minor-mode devil-mode
-  "Local minor mode to support Devil key sequences."
-  :lighter devil-lighter
-  (devil--log "Mode is %s in %s" devil-mode (buffer-name)))
-
-;;;###autoload
-(define-globalized-minor-mode
-  global-devil-mode devil-mode devil--on
-  (if global-devil-mode (devil-add-extra-keys) (devil-remove-extra-keys)))
-
-(defun devil--on ()
-  "Turn Devil mode on."
-  (devil-mode 1))
-
-(defvar devil-special-keys
-  (list (cons "%k %k" (lambda () (interactive) (devil-run-key "%k")))
-        (cons "%k SPC" (lambda () (interactive) (devil-run-key "%k SPC")))
-        (cons "%k RET" (lambda () (interactive) (devil-run-key "%k RET")))
-        (cons "%k <return>" (lambda () (interactive) (devil-run-key "%k 
RET"))))
+(defun devil-set-key (key)
+  "Set `devil-key' to the given KEY and update `devil-mode-map'.
+
+KEY is a string or vector that represents a sequence of
+keystrokes, e.g., `\",\"', `(kbd \"<left>\")', etc.  This
+function clears existing key bindings in `devil-mode-map' and
+sets a single key binding in this keymap so that Devil can be
+activated using the given KEY."
+  (devil--custom-devil-key 'devil-key key))
+
+(defun devil-key-executor (key)
+  "Create a command to call `devil-execute-key' with KEY when invoked.
+
+KEY is a string in the format returned by commands such as `C-h
+k' (`describe-key').  Format control sequences supported by
+`devil-format' may be used in KEY.
+
+This is a convenience function that returns an interactive lambda
+that may be used as a binding value for a special key defined in
+`devil-special-keys'.  When the lambda returned by this function
+is later invoked, it disables `devil-mode-map' temporarily and
+executes KEY as a keyboard macro."
+  (lambda ()
+    (interactive)
+    (devil-execute-key key)))
+
+(defun devil-execute-key (key)
+  "Execute KEY with `devil-mode-map' temporarily disabled.
+
+KEY is a string in the format returned by commands such as `C-h
+k' (`describe-key').  Format control sequences supported by
+`devil-format' may be used in KEY."
+  (let ((keymap (cdr devil-mode-map))
+        (key (devil-format key)))
+    (setcdr devil-mode-map nil)
+    (devil--remove-extra-keys)
+    (devil--log "Disabling keymaps")
+    (unwind-protect
+        (progn
+          (devil--log "Executing kbd macro: %s => %s" key (key-binding key))
+          (execute-kbd-macro (kbd key)))
+      (devil--log "Enabling keymaps")
+      (setcdr devil-mode-map keymap)
+      (devil--add-extra-keys))))
+
+(defcustom devil-special-keys
+  (list (cons "%k %k" (devil-key-executor "%k"))
+        (cons "%k SPC" (devil-key-executor "%k SPC"))
+        (cons "%k RET" (devil-key-executor "%k RET"))
+        (cons "%k <return>" (devil-key-executor "%k <return>"))
+        (cons "%k h %k k" #'devil-describe-key)
+        (cons "%k h %k l" #'devil-toggle-logging))
   "Special Devil keys that are executed as soon as they are typed.
 
 The value of this variable is an alist where each key represents
-a Devil key sequence.  If a Devil key sequence matches any key in
-this alist, the function or lambda in the corresponding value is
-invoked.  The format control specifier `%k' may be used to
-represent `key-description' of `devil-key' in the keys.")
+a Devil key sequence.  If `key-description' of Devil key sequence
+matches any key in this alist, the function or lambda in the
+corresponding value is invoked.  Format control sequences
+supported by `devil-format' may be used in the keys."
+  :type '(alist :key-type string :value-type function))
 
 (defcustom devil-translations
   (list (cons "%k z" "C-")
@@ -137,14 +168,15 @@ represent `key-description' of `devil-key' in the keys.")
   "Translation rules to convert Devil input to Emacs key sequence.
 
 The value of this variable is an alist where each item represents
-a translation rule that is applied on the Devil key sequence read
-from the user to obtain the Emacs key sequence to be executed.
-The translation rules are applied in the sequence they occur in
-the alist.  For each rule, if the key occurs anywhere in the
-Devil key sequence, it is replaced with the corresponding value
-in the translation rule.  The format control specifier `%k' may
-be used to represent `key-description' of `devil-key' in the
-keys."
+a translation rule that is applied on the `key-description' of
+the Devil key sequence read from the user in order to obtain the
+Emacs key sequence to be executed.  The translation rules are
+applied in the sequence they occur in the alist.  For each rule,
+if the key occurs anywhere in the Devil key sequence, it is
+replaced with the corresponding value in the translation rule.
+However, if a replacement leads to an invalid key sequence, then
+that replacement is skipped.  Format control sequences supported
+by `devil-format' may be used in the keys and values."
   :type '(alist :key-type string :value-type string))
 
 (defcustom devil-repeatable-keys
@@ -165,13 +197,12 @@ keys."
 
 The value of this variable is a list where each item represents a
 key sequence that may be repeated merely by typing the last
-character in the key sequence.  The format control specified `%k'
-may be used to represent `key-description' of `devil-key' in the
-keys.  Only key sequences that translate to a complete Emacs key
-sequence according to `devil-translations' and execute an Emacs
-command are made repeatable.  Key sequences that belong to
-`devil-special-keys' are never made repeatable.  Note that this
-variable is ignored if `devil-all-keys-repeatable' is set to t."
+character in the key sequence.  Format control sequences
+supported by `devil-format' may be used in the items.  Only key
+sequences that translate to a complete Emacs key sequence
+according to `devil-translations' and execute an Emacs command
+are made repeatable.  Note that this variable is ignored if
+`devil-all-keys-repeatable' is set to t."
   :type '(repeat string))
 
 (defcustom devil-all-keys-repeatable nil
@@ -179,191 +210,207 @@ variable is ignored if `devil-all-keys-repeatable' is 
set to t."
 
 When this variable is set to t all key sequences that translate
 to a complete and defined Emacs key sequence become a repeatable
-key sequence, i.e., it can be repeated merely by typing the last
-character in the key sequence.  Note that key sequences that
-belong to `devil-special-keys' are never made repeatable.  Also,
+key sequence, i.e., every such key sequence can be repeated
+merely by typing the last character in the key sequence.  Also,
 note that when this variable is set to t, the variable
 `devil-repeatable-keys' is ignored.  However when this variable
 is set to nil, the variable `devil-repeatable-keys' is used to
 determine whether a key sequence is repeatable or not."
   :type 'boolean)
 
-(defun devil-run-key (key)
-  "Execute the given key sequence KEY.
+(defcustom devil-lighter " Devil"
+  "String displayed on the mode line when Devil mode is enabled."
+  :type 'string)
+
+(defcustom devil-prompt "Devil: %t"
+  "A format control string that determines the `devil' prompt.
+
+Format control sequences supported by `devil-format' may be used
+in the format control string."
+  :type 'string)
 
-KEY must be in the format returned by `C-h k` (`describe-key').
-If the format control specifier `%k' occurs in KEY, for each such
-occurrence `key-description' of `devil-key' is inserted into the
-buffer."
-  (dolist (key (split-string key))
-    (if (string= key "%k")
-        (insert (key-description devil-key))
-      (execute-kbd-macro (kbd key)))))
+(defcustom devil-describe-prompt "Describe Devil key: %t"
+  "A format control string that determines the `devil-describe-key' prompt.
+
+Format control sequences supported by `devil-format' may be used
+in the format control string."
+  :type 'string)
+
+
+;;; Minor Mode Definition ============================================
+
+;;;###autoload
+(define-minor-mode devil-mode
+  "Local minor mode to support Devil key sequences."
+  :lighter devil-lighter
+  (devil--log "Mode is %s in %s" devil-mode (buffer-name)))
+
+;;;###autoload
+(define-globalized-minor-mode
+  global-devil-mode devil-mode devil--on
+  (if global-devil-mode (devil--add-extra-keys) (devil--remove-extra-keys)))
+
+(defun devil--on ()
+  "Turn Devil mode on."
+  (devil-mode 1))
+
+
+;;; Bonus Key Bindings ===============================================
 
 (defvar devil--saved-keys nil
   "Original key bindings saved by Devil.")
 
-(defun devil-add-extra-keys ()
+(defun devil--add-extra-keys ()
   "Add key bindings to keymaps for Isearch and universal argument."
   (devil--log "Adding extra key bindings")
   (setq devil--saved-keys (devil--original-keys-to-be-saved))
   (define-key isearch-mode-map devil-key #'devil)
   (define-key universal-argument-map (kbd "u") #'universal-argument-more))
 
-(defun devil-remove-extra-keys ()
+(defun devil--remove-extra-keys ()
   "Remove Devil key bindings from Isearch and universal argument."
   (devil--log "Removing extra key bindings")
   (define-key isearch-mode-map (kbd ",")
-    (cdr (assoc 'isearch-comma devil--saved-keys)))
+    (devil--aget 'isearch-comma devil--saved-keys))
   (define-key universal-argument-map (kbd "u")
-    (cdr (assoc 'universal-u devil--saved-keys))))
+    (devil--aget 'universal-u devil--saved-keys)))
 
 (defun devil--original-keys-to-be-saved ()
   "Return an alist of keys that will be modified by Devil."
   (list (cons 'isearch-comma (lookup-key isearch-mode-map devil-key))
         (cons 'universal-u (lookup-key universal-argument-map (kbd "u")))))
 
+
+;;; Activation Commands ==============================================
+
 (defun devil ()
-  "Wake up Devil to read and translate Devil key sequences."
+  "Read and execute a Devil key sequence."
   (interactive)
-  (devil--log "Devil awake")
-  (devil--read-key (this-command-keys)))
-
-(defun devil--read-key (key)
-  "Read Devil key sequences.
-
-Key sequences are read until it is determined to be a valid Devil
-mode special key sequence, a valid complete key sequence after
-translation to Emacs key sequence, or an undefined key sequence
-after translation to Emacs key sequence.
-
-The argument KEY is a vector that represents the key sequence
-read so far.  This function reads a new key from the user, appends
-it to KEY, and then checks if the result is a valid key sequence
-or an undefined key sequence.  If the result is a valid key
-sequence for a special key command or an Emacs command, then the
-command is executed.  Otherwise, this function calls itself
-recursively to read yet another key from the user."
-  (setq key (vconcat key (vector (read-event (devil--make-prompt key)))))
-  (unless (devil--run-command key)
-    (devil--read-key key)))
-
-(defcustom devil-prompt "Devil: %t"
-  "A format control string that determines the Devil prompt.
-
-The following format control sequences are supported:
-
-%k - Devil key sequence read by Devil so far.
-%t - Emacs key sequence translated from Devil key sequence read so far.
-%% - The percent sign."
-  :type 'string)
-
-(defun devil--make-prompt (key)
-  "Create Devil prompt based on the given KEY."
-  ;; If you are interested in adding Compat as a dependency, you can
-  ;; make use of `format-spec' without raining the minimum version.
-  (let ((result devil-prompt)
-        (controls (list (cons "%k" (key-description key))
-                        (cons "%t" (devil-translate key))
-                        (cons "%%" "%"))))
-    (dolist (control controls result)
-      (setq result (devil-string-replace (car control)
-                                         (cdr control) result)))))
-
-(defun devil--run-command (key)
-  "Try running the command bound to the key sequence in KEY.
-
-KEY is a vector that represents a sequence of keystrokes.  If KEY
-is found to be a special key in `devil-special-keys', the
-corresponding special command is executed immediately and t is
-returned.
-
-Otherwise, it is translated to an Emacs key sequence using
-`devil-translations'.  If the resulting Emacs key sequence is
-found to be a complete key sequence, the command it is bound to
-is executed interactively and t is returned.  If it is found to
-be an undefined key sequence, then t is returned.  If the
-resulting Emacs key sequence is found to be an incomplete key
-sequence, then nil is returned.  The return value t indicates to
-the caller that no more Devil key sequences should be read from
-the user."
-  (devil--log "Trying to execute key: %s" (key-description key))
-  (or (devil--run-special-command key)
-      (devil--run-regular-command key)))
-
-(defun devil--run-special-command (key)
-  "Run Devil mode special command defined for the Devil key sequence KEY.
-
-If the given key sequence KEY is found to be a special key in
-`devil-special-keys', the corresponding special command is
-executed, and t is returned.  Otherwise nil is returned."
+  (devil--log "Activated with %s" (key-description (this-command-keys)))
+  (let* ((result (devil--read-key devil-prompt (this-command-keys)))
+         (key (devil--aget 'key result))
+         (translated-key (devil--aget 'translated-key result))
+         (binding (devil--aget 'binding result)))
+    (devil--log "Read key: %s => %s => %s => %s"
+                key (key-description key) translated-key binding)
+    (if (eq binding 'devil--undefined)
+        (message "Devil: %s is undefined" translated-key)
+      (devil--execute-command key binding))))
+
+(defun devil-describe-key ()
+  "Describe a Devil key sequence."
+  (interactive)
+  (devil--log "Activated with %s" (key-description (this-command-keys)))
+  (let* ((result (devil--read-key devil-describe-prompt (vector)))
+         (key (devil--aget 'key result))
+         (translated-key (devil--aget 'translated-key result))
+         (binding (devil--aget 'binding result)))
+    (devil--log "Read key: %s => %s => %s => %s"
+                key (key-description key) translated-key binding)
+    (if translated-key
+        (describe-key (list (cons (kbd translated-key) key)))
+      ;; Create a transient keymap to describe special key sequence.
+      (let* ((virtual-keymap (make-sparse-keymap))
+             (exit-function (set-transient-map virtual-keymap)))
+        (define-key virtual-keymap key binding)
+        (describe-key key)
+        (funcall exit-function)))))
+
+
+;;; Command Lookup ===================================================
+
+(defun devil--read-key (prompt key)
+  "Read Devil key sequence.
+
+Key events are read until it is determined to be a valid special
+key sequence, a valid complete key sequence after translation to
+Emacs key sequence, or an undefined key sequence after
+translation to Emacs key sequence.
+
+PROMPT is a format control string that defines the prompt to be
+displayed while reading the key sequence.  Format control
+sequences supported by `devil-format' may be used in PROMPT.
+
+KEY is a vector that represents the key sequence read so far.
+This function reads a new key from the user, appends it to KEY,
+and then checks if the result is a valid key sequence or an
+undefined key sequence.  If the result is a valid key sequence
+for a special key command or an Emacs command, then the command
+is executed.  Otherwise, this function calls itself recursively
+to read yet another key from the user."
+  (setq key (vconcat key (vector (read-event (devil-format prompt key)))))
+  (or (devil--find-special-command key)
+      (devil--find-regular-command key)
+      (devil--read-key prompt key)))
+
+(defun devil--find-special-command (key)
+  "Find special command defined for KEY.
+
+If the `key-description' of the given key sequence vector KEY is
+found to be a special key in `devil-special-keys', the
+corresponding special command is executed, and a non-nil result
+is returned.  Otherwise nil is returned."
   (catch 'break
     (dolist (entry devil-special-keys)
       (when (string= (key-description key) (devil-format (car entry)))
-        (devil--log "Running special command: %s => %s"
+        (devil--log "Found special command: %s => %s"
                     (key-description key) (cdr entry))
-        (funcall (cdr entry))
-        (throw 'break t)))))
-
-(defun devil--run-regular-command (key)
-  "Translate KEY and run command bound to it.
-
-After translating KEY to an Emacs key sequence, if the resulting
-key sequence turns out to be an incomplete key, then nil is
-returned.  If it turns out to be a complete key sequence, the
-corresponding Emacs command is executed, and t is returned.  If it
-turns out to be an undefined key sequence, t is returned.  The
-return value t indicates to the caller that no more Devil key
-sequences should be read from the user."
-  (let* ((description (key-description key))
-         (translation (devil-translate key))
-         (fallback (devil-fallback-key translation))
-         (result (devil--run-translation key description translation (not 
fallback))))
-    (if result
-        result
-      (when fallback
-        (devil--run-translation key description fallback t)))))
-
-(defun devil--run-translation (key described-key translated-key default)
-  "Try to run the given TRANSLATED-KEY.
-
-KEY is a vector that represents the original sequence of
-keystrokes from which DESCRIBED-KEY and TRANSLATED-KEY were
-derived.  If TRANSLATED-KEY is an incomplete key sequence, nil is
-returned.  If it is a complete key sequence, the Emacs command
-bound to it is executed, and t is returned.  If it is an
-undefined key sequence, DEFAULT is returned."
+        (throw 'break (devil--make-result key nil (cdr entry)))))))
+
+(defun devil--find-regular-command (key)
+  "Translate KEY and find command bound to it.
+
+After translating the given key sequence vector KEY to an Emacs
+key sequence, if the resulting key sequence turns out to be an
+incomplete key, then nil is returned.  If it turns out to be a
+complete key sequence, a non-nil result is returned."
+  (let* ((translated-key (devil--translate key))
+         (binding (devil--find-command translated-key)))
+    (when (eq binding 'devil--undefined)
+      (let ((fallback-key (devil--fallback-key translated-key)))
+        (when fallback-key
+          (setq translated-key fallback-key)
+          (setq binding (devil--find-command fallback-key)))))
+    (when binding
+      (devil--make-result key translated-key binding))))
+
+(defun devil--find-command (translated-key)
+  "Find command bound to TRANSLATED-KEY."
   (let* ((parsed-key (ignore-errors (kbd translated-key)))
          (binding (when parsed-key (key-binding parsed-key))))
-    (cond ((string-match "[ACHMSs]-$" translated-key)
-           (devil--log "Ignoring incomplete key: %s => %s"
-                       described-key translated-key)
+    (cond ((devil--incomplete-key-p translated-key)
+           (devil--log "Ignoring incomplete key: %s" translated-key)
            nil)
           ((keymapp binding)
-           (devil--log "Ignoring prefix key: %s => %s => %s"
-                       described-key translated-key binding)
+           (devil--log "Ignoring prefix key: %s" translated-key)
            nil)
           ((commandp binding)
-           (devil--update-command-loop-info key binding)
-           (devil--log-command-loop-info)
-           (devil--log "Executing key: %s => %s => %s"
-                       described-key translated-key binding)
-           (call-interactively binding)
-           (when (or devil-all-keys-repeatable
-                     (devil--repeatable-key-p described-key))
-             (devil--set-transient-map (substring described-key -1) binding))
-           t)
+           (devil--log "Found command: %s => %s" translated-key binding)
+           binding)
           (t
-           (devil--log "Undefined key: %s => %s" described-key translated-key)
-           (when default
-             (message "Devil: %s is undefined" translated-key))
-           default))))
+           (devil--log "Undefined key: %s => %s" translated-key binding)
+           'devil--undefined))))
+
+(defun devil--make-result (key translated-key binding)
+  "Create alist for the given KEY, TRANSLATED-KEY, and BINDING."
+  (list (cons 'key key)
+        (cons 'translated-key translated-key)
+        (cons 'binding binding)))
+
+
+;;; Key Translation ==================================================
+
+(defun devil--translate (key)
+  "Translate a given Devil key sequence vector to Emacs key sequence.
 
-(defun devil-translate (key)
-  "Translate a given Devil KEY to Emacs key sequence.
+KEY is a key sequence vector that represents a Devil key
+sequence.  The returned value is an Emacs key sequence string in
+the format returned by commands such as `C-h k' (`describe-key').
 
-The argument KEY is a vector that represents the key sequence
-read so far."
+If FALLBACK is non-nil, the translated key is further translated
+using `local-function-key-map'.  In this case, if this further
+translation does not yield a new translation, then nil is
+returned."
   (setq key (key-description key))
   (let ((result "")
         (index 0))
@@ -393,11 +440,11 @@ read so far."
         (setq index (1+ index))))
     (devil--normalize-ctrl-uppercase-chord result)))
 
-(defun devil-fallback-key (translated-key)
+(defun devil--fallback-key (translated-key)
   "Translate TRANSLATED-KEY to an Emacs key sequence for terminal Emacs.
 
 The argument TRANSLATED-KEY is a string that represents an Emacs
-key sequence returned by `devil-translate'.  Each keystroke in
+key sequence returned by `devil--translate'.  Each keystroke in
 the key sequence is looked up in `local-function-key-map'.  If a
 match is found, it is replaced with its corresponding binding."
   (unless (devil--incomplete-key-p translated-key)
@@ -411,6 +458,49 @@ match is found, it is replaced with its corresponding 
binding."
       (when (not (member result (list "" translated-key)))
         result))))
 
+(defun devil--clean-key (translated-key)
+  "Clean up TRANSLATED-KEY to properly formatted Emacs key sequence."
+  (devil-regexp-replace "\\([ACHMSs]\\)- " "\\1-" translated-key))
+
+(defun devil--normalize-ctrl-uppercase-chord (translated-key)
+  "Normalize chords containing ctrl and uppercase letter in TRANSLATED-KEY."
+  (devil-regexp-replace "C-\\(?:[ACHMs]-\\)*[A-Z]\\(?: \\|$\\)"
+                        'devil--shifted-key translated-key))
+
+(defun devil--shifted-key (translated-key)
+  "Replace the last character in TRANSLATED-KEY with its shifted form."
+  (let* ((hyphen-index (if (string-suffix-p " " translated-key) -2 -1))
+         (prefix (substring translated-key 0 hyphen-index))
+         (suffix (substring translated-key hyphen-index)))
+    (concat prefix "S-" (downcase suffix))))
+
+(defun devil--incomplete-key-p (translated-key)
+  "Return t iff TRANSLATED-KEY is an incomplete Emacs key sequence."
+  (string-match "[ACHMSs]-$" translated-key))
+
+(defun devil--invalid-key-p (translated-key)
+  "Return t iff TRANSLATED-KEY is an invalid Emacs key sequence."
+  (catch 'break
+    (dolist (chunk (split-string translated-key " "))
+      (when (or (string= chunk "")
+                (not (string-match-p "^\\(?:[ACHMSs]-\\)*[^-]*$" chunk))
+                (string-match-p "\\([ACHMSs]-\\)[^ ]*\\1" chunk))
+        (throw 'break t)))))
+
+
+;;; Command Execution ================================================
+
+(defun devil--execute-command (key binding)
+  "Execute the given BINDING bound to the given KEY."
+  (let ((described-key (key-description key)))
+    (devil--update-command-loop-info key binding)
+    (devil--log-command-loop-info)
+    (devil--log "Executing command: %s => %s" described-key binding)
+    (call-interactively binding)
+    (when (devil--repeatable-key-p described-key)
+      (devil--set-transient-map (vector (aref key (1- (length key))))
+                                binding))))
+
 (defun devil--update-command-loop-info (key binding)
   "Update variables that maintain command loop information.
 
@@ -444,60 +534,48 @@ the original Emacs key sequence."
 
 (defun devil--log-command-loop-info ()
   "Log command loop information for debugging purpose."
-  (devil--log
-   (format "Found current-prefix-arg: %s; \
-this-command: %s; last-command: %s; last-repeatable-command: %s"
-          current-prefix-arg
-          this-command
-          last-command
-          last-repeatable-command)))
+  (devil--log "Found current-prefix-arg: %s; \
+this-command: %s; last-command: %s; last-repeatable-command: %s; \
+last-command-event: %s; char-before: %s"
+              current-prefix-arg
+              this-command
+              last-command
+              last-repeatable-command
+              last-command-event
+              (char-before)))
 
 (defun devil--repeatable-key-p (described-key)
   "Return t iff DESCRIBED-KEY belongs to `devil-repeatable-keys'."
-  (catch 'break
-    (dolist (repeatable-key devil-repeatable-keys)
-      (when (string= described-key (devil-format repeatable-key))
-        (throw 'break t)))))
+  (or devil-all-keys-repeatable
+      (catch 'break
+        (dolist (repeatable-key devil-repeatable-keys)
+          (when (string= described-key (devil-format repeatable-key))
+            (throw 'break t))))))
 
 (defun devil--set-transient-map (key binding)
   "Set transient map to run BINDING with KEY."
-  (devil--log "Setting transient map: %s => %s" key binding)
+  (devil--log "Setting transient map: %s => %s" (key-description key) binding)
   (let ((map (make-sparse-keymap)))
-    (define-key map (kbd key) binding)
+    (define-key map key binding)
     (set-transient-map map t)))
 
-(defun devil--clean-key (translated-key)
-  "Clean up TRANSLATED-KEY to properly formatted Emacs key sequence."
-  (devil-regexp-replace "\\([ACHMSs]\\)- " "\\1-" translated-key))
+
+;;; Utility Functions ================================================
 
-(defun devil--normalize-ctrl-uppercase-chord (translated-key)
-  "Normalize chords containing ctrl and uppercase letter in TRANSLATED-KEY."
-  (devil-regexp-replace "C-\\(?:[ACHMs]-\\)*[A-Z]\\(?: \\|$\\)"
-                        'devil--shifted-key translated-key))
+(defun devil-format (format-string &optional key)
+  "Format a Devil FORMAT-STRING.
 
-(defun devil--shifted-key (translated-key)
-  "Replace the last character in TRANSLATED-KEY with its shifted form."
-  (let* ((hyphen-index (if (string-suffix-p " " translated-key) -2 -1))
-         (prefix (substring translated-key 0 hyphen-index))
-         (suffix (substring translated-key hyphen-index)))
-    (concat prefix "S-" (downcase suffix))))
+KEY must be a key sequence vector.  The following format control
+sequences are supported in FORMAT-STRING:
 
-(defun devil--incomplete-key-p (translated-key)
-  "Return t iff TRANSLATED-KEY is an incomplete Emacs key sequence."
-  (string-match "[ACHMSs]-$" translated-key))
-
-(defun devil--invalid-key-p (translated-key)
-  "Return t iff TRANSLATED-KEY is an invalid Emacs key sequence."
-  (catch 'break
-    (dolist (chunk (split-string translated-key " "))
-      (when (or (string= chunk "")
-                (not (string-match-p "^\\(?:[ACHMSs]-\\)*[^-]*$" chunk))
-                (string-match-p "\\([ACHMSs]-\\)[^ ]*\\1" chunk))
-        (throw 'break t)))))
-
-(defun devil-format (string)
-  "Replace %k in STRING with `key-description' of `devil-key'."
-  (devil-string-replace "%k" (key-description devil-key) string))
+%k - Devil key.
+%r - Devil key sequence read by Devil so far.
+%t - Emacs key sequence translated from the Devil key sequence.
+%% - The percent sign."
+  (format-spec format-string (list (cons ?k (key-description devil-key))
+                                   (cons ?r (key-description key))
+                                   (cons ?t (devil--translate key))
+                                   (cons ?% "%"))))
 
 (defun devil-string-replace (from-string to-string in-string)
   "Replace FROM-STRING with TO-STRING in IN-STRING."
@@ -510,5 +588,9 @@ this-command: %s; last-command: %s; 
last-repeatable-command: %s"
   (let ((case-fold-search nil))
     (replace-regexp-in-string regexp replacement in-string t)))
 
+(defun devil--aget (key alist)
+  "Find KEY in ALIST and return corresponding value."
+  (cdr (assoc key alist)))
+
 (provide 'devil)
 ;;; devil.el ends here



reply via email to

[Prev in Thread] Current Thread [Next in Thread]