branch: elpa/devil commit e52262afdd963f75fa31a6b21d46216efc947b94 Author: Susam Pal <su...@susam.net> Commit: Susam Pal <su...@susam.net>
Add Devil --- CHANGES.md | 22 +++ LICENSE.md | 23 +++ README.md | 457 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ devil.el | 385 +++++++++++++++++++++++++++++++++++++++++++++++ meta/Makefile | 9 ++ meta/README.md | 36 +++++ meta/example.el | 22 +++ meta/example.md | 27 ++++ meta/global.el | 4 + meta/god.el | 4 + meta/left.el | 6 + meta/local.el | 4 + meta/smiley.el | 46 ++++++ 13 files changed, 1045 insertions(+) diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000000..cbf18a0541 --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,22 @@ +Changelog +========= + +0.1.0 (2023-05-07) +------------------ + +### Added + +- Devil global and local minor modes. +- Default Devil key set to the comma (`,`). +- Special key `, ,` to type a literal comma. +- Special key `, SPC` to type a comma followed by a space. +- Special key `, RET` to type a comma followed by return. +- Translation rules that translate `,` and `, z` to `C-`. +- Translation rules that translate `m` and `, m m` to `M-`. +- Translation rule that translates `, ,` to `,`. +- Repeatable key sequences for `, p`, `, n`, `, f`, `, b`, `, m m f`, + `, m m b`, and `, m x o`. +- Key binding for `isearch-mode-map` to support Devil key sequences in + incremental search. +- Key binding for `universal-argument-map` to support repeating the + universal argument with `u`. diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000000..ee8f3b8fd1 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,23 @@ +The MIT License (MIT) +===================== + +Copyright (c) 2022-2023 Susam Pal + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000000..fb98c65696 --- /dev/null +++ b/README.md @@ -0,0 +1,457 @@ +Devil Mode +========== + +Devil mode trades your comma key in exchange for a modifier-free +editing experience! Yes, the comma key! The key you would normally +wield for punctuation in nearly every corner of text. Yes, this is +twisted! It would not be called the Devil otherwise, would it? If it +were any more rational, we might call it something divine, like, uh, +the God mode? But alas, there's nothing divine to be found here. +Welcome, instead, to the realm of the Devil! You will be granted the +occassional use of the comma key for punctuation, but only if you can +charm the Devil! But beware, for in this sinister domain, you must +relinquish your comma key and embrace an editing experience that +whisphers wicked secrets into your fingertips! + + +Contents +-------- + +* [Introduction](#introduction) +* [Notation](#notation) +* [Get Started](#get-started) +* [Use Devil](#use-devil) +* [Typing Commas](#typing-commas) +* [Devil Reader](#devil-reader) +* [Translation Rules](#translation-rules) +* [Translation Examples](#translation-examples) +* [Extra Key Bindings](#extra-key-bindings) +* [Local Mode](#local-mode) +* [Custom Appearance](#custom-appearance) +* [Custom Devil Key](#custom-devil-key) +* [Why?](#why) +* [Support](#support) +* [Channels](#channels) +* [More](#more) + + +Introduction +------------ + +Devil mode intercepts our keystrokes and translates them to Emacs key +sequences according to a configurable set of translation rules. For +example, with the default translation rules, when we type `, x , f`, +Devil translates it to `C-x C-f`. + +The choice of the comma key (`,`) to mean the control modifier key +(`C-`) may seem outrageous. After all, the comma is a very important +punctuation both in prose as well as in code. Can we really get away +with using `,` to mean the `C-` modifier? It turns out, this terrible +idea can be made to work without too much of a hassle. At least it +works for me! It might work for you too. If it does not, Devil can be +configured to use another key instead of `,` to mean the `C-` +modifier. See the section [Custom Devil Key](#custom-devil-key) for an +example. + +A skeptical reader may rightfully ask: If `,` is translated to `C-`, +how on earth are we going to insert a literal `,` into the text when +we need to? The section [Typing Commas](#typing-commas) answers this. +But before we get there, we have some fundamentals to cover. Take the +plunge and see what unfolds! Maybe you'll like this! Maybe you won't! +If you don't like this, you can always retreat to God mode, Evil mode, +the vanilla key bindings, or whatever piques your fancy! + + +Notation +-------- + +A quick note about the notation used in the document: The previous +example shows that `, x , f` is translated to `C-x C-f`. What this +really means is that the key sequence +<kbd>,</kbd><kbd>x</kbd><kbd>,</kbd><kbd>f</kbd> is translated to +<kbd>ctrl</kbd>+<kbd>x</kbd> <kbd>ctrl</kbd>+<kbd>f</kbd>. We do not +really type any space after the commas. The key <kbd>,</kbd> is +directly followed by the key <kbd>x</kbd>. However, the key sequence +notation used in this document contains spaces between each keystroke. +This is consistent with how key sequences are represented in Emacs in +general and how Emacs functions like `key-description`, +`describe-key`, etc. represent key sequences. When we really need to +type a space, it is represented as `SPC`. + + +Get Started +----------- + +To get started quickly with Devil, clone its Git repository to your +system and load it in your Emacs initialization file with the +following steps: + + 1. Clone Devil to your system: + + ```sh + git clone https://github.com/susam/devil.git + ``` + + 2. Add the following to your Emacs initialization file (i.e., to your + `~/.emacs` or `~/.emacs.d/init.el` or `~/.config/emacs/init.el`): + + ```elisp + (add-to-list 'load-path "/path/to/devil/") + (require 'devil) + (global-devil-mode) + (global-set-key (kbd "C-,") 'global-devil-mode) + ``` + + 3. Start the Emacs editor. Devil mode should now be enabled in all + buffers. The modeline of each buffer should show the `Devil` + lighter. + + 4. Type `, x , f` and watch Devil translate it to `C-x C-f` and + invoke the corresponding command. + + 5. Type `C-,` to disable Devil mode. Type `C-,` again to enable it. + + +Use Devil +--------- + +Assuming vanilla Emacs key bindings have not been changed and Devil +has not been customized, here are some examples that demonstrate how +Devil may be used: + + 1. Type `, x , f` and watch Devil translate it to `C-x C-f` and + invoke the find file functionality. + + 2. Type `, p` to move up one line. + + 3. To move up multiple lines type `, p p p` and so on. Some Devil key + sequences are repeatable keys. The repeatable Devil key sequences + can be repeated by typing the last key of the Devil key sequence + over and over again. + + 4. Another example of a repeatable Devil key sequence is `, f f f` + which moves the cursor word by multiple characters. + + 5. Type `, s` and watch Devil translate it to `C-s` and invoke + incremental search. + + 6. Type `, m s` and watch Devil translate it to `C-M-s` and invoke + regular-expression-based incremental search. Yes, `m` is + translated to `M-`. + + 7. Type `, m m x` and watch Devil translate it to `M-x` and invoke + the corresponding command. + + 8. Type `, u , f` and watch Devil translate it to `C-u C-f` and move + the cursor forward by 4 characters. + + 9. Type `, u u , f` and the cursor moves forward by 16 characters. + Devil uses its translation rules and an additional keymap to make + the input key sequence behave like `C-u C-u C-f` which moves the + cursor forward by 16 characters. + +10. Type `, SPC` to type a comma followed by space. This is a special + key sequence to make it convenient to type comma in the text. Note + that this sacrifices the use of `, SPC` to mean `C-SPC` which + could have been a convenient way to set a mark. + +11. Type `, z SPC` and watch Devil translate it to `C-SPC` and set a + mark. Yes, `, z` is translated to `C-` too. + +12. Similarly, type `, RET` to type a comma followed by the return + key. This is another special key. + +13. Type `, ,` to type a single comma. This special key is useful for + cases when you really need to type a single literal comma. + + +Typing Commas +------------- + +Devil makes the questionable choice of using the comma as its trigger +key. As illustrated in the previous section, typing `, x , f` produces +the same effect as typing `C-x C-f`. One might naturally wonder how +then are we supposed to type literal commas. + +Most often when we edit text, we don't really type a comma in +isolation. Often we immediately follow the comma with a space or a +newline. This assumption usually holds good while editing regular +text. However, this assumption may not hold in some situations, like +while working with code when we need to add a single comma at the end +of an existing line. + +In scenarios where the above assumption holds good, typing `, SPC` +inserts a comma and a space. Similarly, typing `, RET` inserts a comma +and a newline. + +In scenarios, when we do need to type a single comma, type `, ,` instead. + +Also, it is worth mentioning here that if all this fiddling with the +comma key feels clumsy, we could always customize the Devil key to +something else that feels better. We could also disable Devil mode +temporarily and renable it later with `C-,` as explained in section +[Get Started](#get-started). + + +Devil Reader +------------ + +The following points briefly describe how Devil reads Devil key +sequences, translates them to Emacs key sequences, and runs commands +bound to the key sequences: + + 1. As soon as the Devil key is typed (which is `,` by default), Devil + wakes up and starts reading Devil key sequences. Type `C-h v + devil-key RET` to see the current Devil key. + + 2. After each keystroke is read, Devil checks if the key sequence + accumulated is a special key. If it is, then the special command + bound to the special key is executed immediately. Note that this + step is performed before any translation rules are applied to the + input key sequence. This is how the Devil special key sequence `, + SPC` inserts a comma and a space. Type `C-h v + devil-special-keys RET` to see the list of special keys and + the commands bound to them. + + 3. If the key sequence accumulated so far is not a special key, then + Devil translates the Devil key sequence to a regular Emacs key + sequence. If the regular Emacs key sequence turns out to be a + complete key sequence and some command is found to be bound to it, + then that command is executed immediately. This is how the Devil + key sequence `, x , f` is translated to `C-x C-f` and the + corresponding binding is executed. If the translated key sequence + is a complete key sequence but no command is bound to it, then + Devil displays a message that the key sequence is undefined. Type + `C-h v devil-translations RET` to see the list of translation + rules. + + 4. After successfully translating a Devil key sequence to an Emacs + key sequence and executing the command bound to it, Devil checks + if the key sequence is a repeatable key sequence. If it is found + to be a repeatable key sequence, then Devil sets a transient map + so that the command can be repeated merely by typing the last + keystroke of the input key sequence. This is how `, p p p` moves + the cursor up by three lines. Type `C-h v devil-repeatable-keys + RET` to see the list of repeatable Devil key sequences. + +The variables `devil-special-keys`, `devil-translations`, and +`devil-repeatable-keys` may contain keys or values with the string +`%k` in them. This is a placeholder for `devil-key`. While applying +the special keys, translation rules, or repeat rules, each `%k` is +replaced with the actual value of `devil-key` before applying the +rules. + + +Translation Rules +----------------- + +The following points provide an account of the translation rules that +Devil follows in order to convert a Devil key sequence entered by the +user to an Emacs key sequence: + + 1. The input key vector read from the user is converted to a key + description (i.e., the string functions like `describe-key`, + `key-description`, produce). For example, if the user types + <kbd>,</kbd><kbd>x</kbd><kbd>,</kbd><kbd>f</kbd>, it is converted + to `, x , f`. + + 2. Now the resulting key description is translated with simple string + replacements. If any part of the string matches a key in + `devil-translations`, then it is replaced with the corresponding + value. For example, `, x , f` is translated to `C- x C- f`. Then + Devil normalizes the result to `C-x C-f` by removing superfluous + spaces after the modifier keys. + + 3. However, if the simple string based replacement leads to an + invalid Emacs key sequence, it skips the replacement that causes + the resulting Emacs key sequence to become invalid. For example `, + m ,` results in `C-M-C-` after the simple string replacement + because the default translation rules replace `,` with `C-` and + `m` with `M-`. However, `C-M-C-` is an invalid key sequence, so + the replacement of the second `,` to `C-` is skipped. Therefore, + the input `, m ,` is translated to `C-M-,` instead. + + +Translation Examples +-------------------- + +By default, Devil supports a small but peculiar set of translation +rules that can be used to avoid modifier keys while typing various +types of key sequences. See `C-h v devil-translations RET` for the +translation rules. Here are some examples that demonstrate the default +translation rules. The obvious ones are shown first first. The more +peculiar translations come later in the table. + +| Input | Translated | Remarks | +|-----------|------------|------------------------------------| +| `, s` | `C-s` | `,` is replaced with `C-` | +| `, m s` | `C-M-s` | `m` is replaced with `M-` | +| `, z s` | `C-SPC` | `, z` is replaced with `C-` too | +| `, z z` | `C-z` | ditto | +| `, m m x` | `M-x` | `, m m` is replaced with `M-` too | +| `, c , ,` | `C-c ,` | `, ,` is replaced with `,` | + +Note how we cannot use `, SPC` to set a mark because that key sequence +is already reserved as a special key sequence in `devil-special-keys`, +so Devil translates `, z` to `C-` too, so that we can still type +`C-SPC` using `, z s` and set a mark. + +Also, note how the translation of `, m m` to `M-` allows us to enter a +key sequence that begins with the `M-` modifier key. + + +Extra Key Bindings +------------------ + +Devil adds the following additional key bindings only when Devil is +enabled globally with `global-devil-mode`: + +- Adds the Devil key to `isearch-mode-map`, so that Devil key + sequences work in incremental search too. + +- Adds `u` to `universal-argument-more` to allow repeating the + universal argument command `C-u` simply by repeating `u`. + +As mentioned before these features are available only when Devil is +enabled globally with `global-devil-mode`. If Devil is enabled locally +with `devil-mode`, then these features not available. + + +Local Mode +---------- + +While the section [Get Started](#get-started) shows how we enable +Devil mode globally, this section shows how we can enable it locally. +Here is an example initialization code that enables Devil locally only +in text buffers. + +```elisp +(add-to-list 'load-path "/path/to/devil/") +(require 'devil) +(add-hook 'text-mode-hook 'devil-mode) +(global-set-key (kbd "C-,") 'devil-mode) +``` + +This is not recommended though because this does not provide a +seamless Devil experience. For example, with Devil enabled locally in +a text buffer like this, although we can type `, x , f` to launch the +find-file minibuffer, we cannot use Devil key sequences in the +minibuffer. Further the special keymaps described in the previous +section work only when Devil is enabled globally. + + +Custom Appearance +----------------- + +The following initialization code shows how we can customize Devil to +show a Devil face in the modeline and the echo area. + +```elisp +(add-to-list 'load-path "/path/to/devil/") +(require 'devil) +(setq devil-lighter " \U0001F608") +(setq devil-prompt "\U0001F608 %t") +(global-devil-mode) +(global-set-key (kbd "C-,") 'global-devil-mode) +``` + +This is how Emacs may look if emojis are rendered correctly: + +[![Screenshot of Emacs with Devil face][horns-screenshot]][horns-screenshot] + +[horns-screenshot]: https://i.imgur.com/6Ly7IOs.png + + +Custom Devil Key +---------------- + +The following initialization code shows how we can customize Devil to +use a different Devil key. + +```elisp +(add-to-list 'load-path "/path/to/devil/") +(setq devil-key "<left>") +(require 'devil) +(global-devil-mode) +(global-set-key (kbd "C-<left>") 'global-devil-mode) +``` + +The above example sets the Devil key to the left arrow key, perhaps +another dubious choice for the Devil key. With this configuration, we +can use `<left> x <left> f` and have Devil translate it to `C-x C-f`. + +To customize the special keys, translation rules, and repeatable keys, +see the variables `devil-special-keys`, `devil-translations`, and +`devil-repeatable-keys`, respectively. + + +Why? +---- + +Why go to the trouble of creating and using something like this? Why +not just remap <kbd>caps lock</kbd> to <code>ctrl</code> like every +other sane person does? Or if it is so important to avoid modifier +keys, why not use something like God mode? + +Well, this minor mode began as a tiny little experiment just for fun. +From the outset, it was clear that using something as crucial as the +comma for specifying modifier key is asking for trouble. However, I +still wanted to see how far we can go with it. It turned out that in a +matter of days, I was using it full-time for all of my Emacs usage. + +This experiment was partly motivated by Macbook keyboards which do not +have a right <kbd>ctrl</kbd> key. Being a touch-typist myself, I found +it inconvenient to type key combinations like `C-x`, `C-a`, `C-w`, +`C-s`, etc. where both the modifier key and the modified key need to +be pressed with the left hand fingers. I am not particularly fond of +remapping <kbd>caps lock</kbd> to behave like <kbd>ctrl</kbd> because +that still suffers from the problem that key combinations like `C-x`, +`C-a` require pressing both the modifier key and the modified key with +the left hand fingers. I know many people remap both their <kbd>caps +lock</kbd> and <kbd>enter</kbd> to behave like <kbd>ctrl</kbd> +modifier key. While I think that's a fine solution, I was not willing +to put up with the work required to make that work seamlessly across +all the various operating systems I work on. + +What began as a tiny whimsical experiment a few years ago turned out +to be quite effective, at least to me. I like that this solution is +implemented purely as Elisp and therefore does not have any external +dependency. I am sharing this solution here in the form of a minor +mode, just in case, there is someone out there who might find this +useful too. + + +Support +------- + +To report bugs, suggest improvements, or ask questions, +[create issues][ISSUES]. + +[ISSUES]: https://github.com/susam/devil/issues + + +Channels +-------- + +The author of this project hangs out at the following places online: + + - Website: [susam.net](https://susam.net) + - Mastodon: [@susam@mastodon.social](https://mastodon.social/@susam) + - Twitter: [@susam](https://twitter.com/susam) + - GitHub: [@susam](https://github.com/susam) + - Matrix: [#susam:matrix.org](https://app.element.io/#/room/#susam:matrix.org) + - IRC: [#susam:libera.chat](https://web.libera.chat/#susam) + +You are welcome to subscribe to, follow, or join one or more of the +above channels to receive updates from the author or ask questions +about this project. + + +More +---- + +See [Emacs4CL](https://github.com/susam/emacs4cl), a DIY quick-starter +kit to set up Emacs for Common Lisp programming. + +See [Emfy](https://github.com/susam/emfy), a DIY quick-starter kit to +set up Emacs for general purpose editing and programming. diff --git a/devil.el b/devil.el new file mode 100644 index 0000000000..2e9faca8a5 --- /dev/null +++ b/devil.el @@ -0,0 +1,385 @@ +;;; devil.el --- Minor mode for Devil-like command entering -*- lexical-binding: t; -*- + +;; Copyright (c) 2022-2023 Susam Pal + +;; Author: Susam Pal +;; Version: 0.1.0pre1 +;; Package-Requires: ((emacs "24.4")) +;; Keywords: convenience +;; URL: https://github.com/susam/devil + +;; This file is not part of GNU Emacs. + +;; Permission is hereby granted, free of charge, to any person +;; obtaining a copy of this software and associated documentation +;; files (the "Software"), to deal in the Software without +;; restriction, including without limitation the rights to use, copy, +;; modify, merge, publish, distribute, sublicense, and/or sell copies +;; of the Software, and to permit persons to whom the Software is +;; furnished to do so, subject to the following conditions: + +;; The above copyright notice and this permission notice shall be +;; included in all copies or substantial portions of the Software. + +;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +;; EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +;; MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +;; NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +;; BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +;; ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +;; CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +;; SOFTWARE. + +;;; Commentary: + +;; Devil intercepts your devil key (comma by default) to let you type +;; key sequences without using modifier keys. + +;;; Code: +(defvar devil-key "," + "The key sequence that begins Devil input. + +The key sequence must be specified in the format returned by `C-h +k' (`describe-key'). This variable should be set before enabling +Devil mode for it to take effect.") + +(defvar devil-lighter " Devil" + "String displayed on the mode line when Devil mode is enabled.") + +(defvar devil-mode-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd devil-key) #'devil) + map) + "Keymap to wake up Devil when `devil-key' is typed.") + +;;;###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 :group 'devil + (if global-devil-mode (devil-add-extra-keys) (devil-remove-extra-keys))) + +(defun devil--on () + "Turn Devil mode on." + (devil-mode 1)) + +(defvar devil-logging nil + "Non-nil if and only if Devil should print log messages.") + +(message "Devil loading ...") + +(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")))) + "Special Devil keys that are triggered 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 `devil-key' in the keys.") + +(defvar devil-translations + (list (cons "%k z" "C-") + (cons "%k %k" "%k") + (cons "%k m m" "M-") + (cons "%k" "C-") + (cons "m" "M-")) + "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 `devil-key' in the keys.") + +(defvar devil-repeatable-keys + (list "%k p" + "%k n" + "%k f" + "%k b" + "%k m m f" + "%k m m b" + "%k x o") + "Devil mode repeatable key sequences. + +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 `devil-key' in the keys.") + +(defun devil-run-key (key) + "Execute the given key sequence KEY. + +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 `devil-key' is inserted into the buffer." + (dolist (key (split-string key)) + (if (string= key "%k") (insert devil-key) (execute-kbd-macro (kbd key))))) + +(defvar devil--saved-keys + "Original key bindings saved by Devil.") + +(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 (kbd devil-key) 'devil) + (define-key universal-argument-map (kbd "u") 'universal-argument-more)) + +(defun devil-remove-extra-keys () + "Remove Devil key bindings from Isearch and universal argument." + (devil--log "Removing extra keybindings") + (define-key isearch-mode-map (kbd ",") + (cdr (assoc 'isearch-comma devil--saved-keys))) + (define-key universal-argument-map (kbd "u") + (cdr (assoc '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 (kbd devil-key))) + (cons 'universal-u (lookup-key universal-argument-map (kbd "u"))))) + +(defun devil () + "Wake up Devil to read and translate Devil key sequences." + (interactive) + (devil--log "Devil waking up") + (devil--read-key (vconcat (kbd devil-key)))) + +(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-key (devil--make-prompt key))))) + (unless (devil--run-command key) + (devil--read-key key))) + +(defvar 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.") + +(defun devil--make-prompt (key) + "Create Devil prompt based on the given KEY." + (let ((result devil-prompt) + (controls (list (cons "%k" (key-description key)) + (cons "%t" (devil-translate key)) + (cons "%%" "%")))) + (dolist (control controls result) + (setq result (replace-regexp-in-string (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." + (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." + (catch 'break + (dolist (entry devil-special-keys) + (when (string= (key-description key) (devil-format (car entry))) + (devil--log "Running 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* ((described-key (key-description key)) + (translated-key (devil-translate key)) + (parsed-key (condition-case nil (kbd translated-key) (error nil))) + (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) + nil) + ((keymapp binding) + (devil--log "Ignoring prefix key: %s => %s => %s" + described-key translated-key binding) + 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 (devil--repetable-key-p described-key) + (devil--set-transient-map (substring described-key -1) binding)) + t) + (t + (message "Devil: %s is undefined" translated-key) + t)))) + +(defun devil-translate (key) + "Translate a given Devil KEY to Emacs key sequence. + +The argument KEY is a vector that represents the key sequence +read so far." + (setq key (key-description key)) + (let ((result "") + (index 0)) + (while (< index (length key)) + (catch 'break + ;; Try translating the current position in Devil key to Emacs key. + (dolist (entry devil-translations key) + (let* ((from-key (devil-format (car entry))) + (to-key (devil-format (cdr entry))) + (in-key (substring key index)) + (try-key)) + (when (string-prefix-p from-key in-key) + (setq try-key (devil--clean-key (concat result to-key))) + (when (devil--valid-key-p try-key) + (setq result try-key) + (setq index (+ index (length from-key))) + (throw 'break t))))) + ;; If no translation succeeded, advance current position. + (let ((char (substring key index (1+ index)))) + (setq result (devil--clean-key (concat result char)))) + (setq index (1+ index)))) + result)) + +(defun devil--update-command-loop-info (key binding) + "Update variables that maintain command loop information. + +The given KEY and BINDING is used to update variables that +maintain command loop information. This allows the commands that +depend on them behave as if they were being invoked directly with +the original Emacs key sequence." + ;; + ;; Set `last-command-event' so that `digit-argument' can determine + ;; the correct digit for key sequences like , 5 (C-5). See M-x + ;; find-function RET digit-argument RET for details. + (setq last-command-event (aref key (- (length key) 1))) + ;; + ;; Set `this-command' to make several commands like , z SPC , z SPC + ;; (C-SPC C-SPC) and , p (C-p) work correctly. Emacs copies + ;; `this-command' to `last-command'. Both variables are used by + ;; `set-mark-command' to decide whether to activate/deactivate the + ;; current mark. The first variable is used by vertical motion + ;; commands to keep the cursor at the `temporary-goal-column'. There + ;; may be other commands too that depend on this variable. + (setq this-command binding) + ;; + ;; Set `real-this-command' to make , x z (C-x z) work correctly. + ;; Emacs copies it to `last-repeatable-command' which is then used + ;; by repeat. See the following for more details: + ;; + ;; - M-x find-function RET repeat RET + ;; - C-h v last-repeatable-command RET + ;; - grep kset_last_repeatable_command src/keyboard.c + (setq real-this-command binding)) + +(defun devil--log-command-loop-info () + "Log command loop information for debugging purpose." + (devil--log + (concat "Found " + (format "current-prefix-arg: %s; " current-prefix-arg) + (format "this-command: %s; " this-command) + (format "last-command: %s; " last-command) + (format "last-repeatable-command: %s" last-repeatable-command)))) + +(defun devil--repetable-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))))) + +(defun devil--set-transient-map (key binding) + "Set transient map to run BINDING with KEY." + (devil--log "Setting transient map: %s => %s" key binding) + (let ((map (make-sparse-keymap))) + (define-key map (kbd key) binding) + (set-transient-map map t))) + +(defun devil--clean-key (translated-key) + "Clean up TRANSLATED-KEY to properly formatted Emacs key sequence." + (replace-regexp-in-string "\\([ACHMsS]\\)- " "\\1-" translated-key)) + +(defun devil--valid-key-p (translated-key) + "Return nil iff TRANSLATED-KEY is an invalid Emacs key sequence." + (not (string-match-p (concat "A-[^ ]*A-\\|" "C-[^ ]*C-\\|" "H-[^ ]*H-\\|" + "M-[^ ]*M-\\|" "s-[^ ]*s-\\|" "S-[^ ]*S-") + translated-key))) + +(defun devil-format (string) + "Replace %k in STRING with `devil-key'." + (replace-regexp-in-string "%k" devil-key string)) + +(defun devil--log (format-string &rest args) + "Write log message with the given FORMAT-STRING and ARGS." + (when devil-logging + (apply 'message (concat "Devil: " format-string) args))) + +(defmacro devil--assert (form) + "Evaluate FORM and cause error if the result is nil." + `(unless ,form + (error "Assertion failed: %s" ',form))) + +(defun devil--tests () + "Test Devil functions assuming Devil has not been customized." + (interactive) + (devil--assert (string= (devil-translate (vconcat ",")) "C-")) + (devil--assert (string= (devil-translate (vconcat ",x")) "C-x")) + (devil--assert (string= (devil-translate (vconcat ",x,")) "C-x C-")) + (devil--assert (string= (devil-translate (vconcat ",x,f")) "C-x C-f")) + (devil--assert (string= (devil-translate (vconcat ",,")) "C-,")) + (devil--assert (string= (devil-translate (vconcat ",,,,")) "C-, C-,")) + (devil--assert (string= (devil-translate (vconcat ",mx")) "C-M-x")) + (devil--assert (string= (devil-translate (vconcat ",,mx")) "M-x")) + (devil--assert (string= (devil-translate (vconcat ",mmm")) "M-m")) + (devil--log "Tests completed")) + +(provide 'devil) + +;;; devil.el ends here diff --git a/meta/Makefile b/meta/Makefile new file mode 100644 index 0000000000..ca933a0b53 --- /dev/null +++ b/meta/Makefile @@ -0,0 +1,9 @@ +checks: + cd ~/git/melpa/ && make clean && rm -rf packages/devil* working/devil/ + cd ~/git/melpa/ && make recipes/devil + -cd ~/git/melpa/ && STABLE=t make recipes/devil + cd ~/git/melpa/ && make sandbox INSTALL=devil + cd ~/git/melpa/ && ls -l packages/ packages-stable/ sandbox/elpa/ + +smiley: + emacs -q -l smiley.el example.el example.md diff --git a/meta/README.md b/meta/README.md new file mode 100644 index 0000000000..80b9886d39 --- /dev/null +++ b/meta/README.md @@ -0,0 +1,36 @@ +Developer Notes +=============== + +Release Checklist +----------------- + +Perform the following tasks for every release: + + - Update version in devil.el. + - Update copyright notice in devil.el. + - Update copyright notice in LICENSE.md. + - Update CHANGES.md. + - Run checks: + + ```sh + make checks + ``` + + - Commit changes: + + ```sh + git status + git add -p + git commit + git push + ``` + + - Tag the release: + + ``` + VERSION= + + git commit -em "Set version to $VERSION" + git tag $VERSION -m "Devil $VERSION" + git push origin main $VERSION + ``` diff --git a/meta/example.el b/meta/example.el new file mode 100644 index 0000000000..7ba5f4c9ac --- /dev/null +++ b/meta/example.el @@ -0,0 +1,22 @@ +;;; Emacs Lisp Examples. + +(defun hello-world () + "Show 'hello, world' message." + (interactive) + (message "hello, world")) + +(defun show-current-time () + "Show current time." + (interactive) + (message (current-time-string))) + +(defun fibonacci (n) + "Compute nth Fibonacci number." + (cond ((= n 0) 0) + ((= n 1) 1) + (t (+ (fibonacci (- n 1)) + (fibonacci (- n 2)))))) + +(global-set-key (kbd "C-c h") 'hello-world) +(global-set-key (kbd "C-c t") 'show-current-time) +(global-set-key (kbd "C-c d") 'delete-trailing-whitespace) diff --git a/meta/example.md b/meta/example.md new file mode 100644 index 0000000000..374b092f11 --- /dev/null +++ b/meta/example.md @@ -0,0 +1,27 @@ +Emacs Lisp +========== + +Emacs Lisp is a dialect of the Lisp programming language. Most +of the GNU Emacs text editor is written in this programming +language. The Emacs Lisp manual describes the usefulness of +Emacs Lisp as follows: + +> You can write new code in Emacs Lisp and install it as an +> extension to the editor. However, Emacs Lisp is more than a +> mere extension language; it is a full computer programming +> language in its own right. You can use it as you would any +> other programming language. + +Here is a simple Emacs Lisp example that prints the string +"hello, world" in the echo area: + +``` +(message "hello, world") +``` + +Emacs Lisp supports both imperative and functional programming +paradigms. The extension `.el` is used by convention to name +files that contain Emacs Lisp programs. + +The GNU Emacs Lisp manual is available in various formats at +<https://www.gnu.org/software/emacs/manual/elisp.html>. diff --git a/meta/global.el b/meta/global.el new file mode 100644 index 0000000000..e93114c099 --- /dev/null +++ b/meta/global.el @@ -0,0 +1,4 @@ +(add-to-list 'load-path "~/git/devil/") +(require 'devil) +(global-devil-mode) +(global-set-key (kbd "C-,") 'global-devil-mode) diff --git a/meta/god.el b/meta/god.el new file mode 100644 index 0000000000..75d2e21271 --- /dev/null +++ b/meta/god.el @@ -0,0 +1,4 @@ +(add-to-list 'load-path "~/git/god-mode/") +(require 'god-mode) +(god-mode) +(global-set-key (kbd "<escape>") 'god-mode-all) diff --git a/meta/left.el b/meta/left.el new file mode 100644 index 0000000000..ef519930f2 --- /dev/null +++ b/meta/left.el @@ -0,0 +1,6 @@ +(add-to-list 'load-path "~/git/devil/") +(setq devil-logging t) +(setq devil-key "<left>") +(require 'devil) +(global-devil-mode) +(global-set-key (kbd "C-<left>") 'global-devil-mode) diff --git a/meta/local.el b/meta/local.el new file mode 100644 index 0000000000..aeaf9ca915 --- /dev/null +++ b/meta/local.el @@ -0,0 +1,4 @@ +(add-to-list 'load-path "~/git/devil/") +(require 'devil) +(add-hook 'text-mode-hook 'devil-mode) +(global-set-key (kbd "C-,") 'devil-mode) diff --git a/meta/smiley.el b/meta/smiley.el new file mode 100644 index 0000000000..c3c83b92ef --- /dev/null +++ b/meta/smiley.el @@ -0,0 +1,46 @@ +;; Customize user interface. +(when (display-graphic-p) + (tool-bar-mode 0) + (scroll-bar-mode 0)) +(setq inhibit-startup-screen t) +(column-number-mode) + +;; Do not display file icon or name on title bar. +(add-to-list 'default-frame-alist '(ns-appearance . dark)) +(setq ns-use-proxy-icon nil) +(setq frame-title-format nil) + +;; Dark theme colours. +(load-theme 'wombat) +(set-face-attribute 'menu nil :background "#444" :foreground "#eee") +(set-face-attribute 'default nil :background "#111" :foreground "#eee") +(set-face-attribute 'region nil :background "#354" :foreground "#eee") +(set-face-attribute 'isearch nil :background "#ff0" :foreground "#000") +(set-face-attribute 'lazy-highlight nil :background "#990" :foreground "#000") +(set-face-attribute 'mode-line nil :background "#444" :foreground "#ccc") +(set-face-attribute 'mode-line-inactive nil :background "#222" :foreground "#999") +(set-face-background 'cursor "#c96") +(set-face-foreground 'font-lock-comment-face "#fc0") + +;; Dark theme attributes. +(set-face-attribute 'menu nil :inverse-video nil) +(set-face-attribute 'mode-line nil :box '(:style released-button)) +(set-face-attribute 'mode-line-inactive nil :box '(:style pressed-button)) + +;; Packages. +(require 'package) +(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t) +(package-initialize) +(unless package-archive-contents + (package-refresh-contents)) +(dolist (package '(markdown-mode paredit rainbow-delimiters)) + (unless (package-installed-p package) + (package-install package))) + +;; Devil +(add-to-list 'load-path "~/git/devil/") +(require 'devil) +(setq devil-lighter " \U0001F608") +(setq devil-prompt "\U0001F608 %t") +(global-devil-mode) +(global-set-key (kbd "C-,") 'global-devil-mode)