[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