newline

Creating a custom Emacs input method for box drawing characters

Emacs

September 13, 2024

I wanted to manually create some file tree diagrams that look like those generated by tree, but without having to copy-paste or enter the Unicode for every box drawing character (, , etc.). In Emacs, I could do a C-x 8 RET and search for the name (e.g., “BOX DRAWINGS LIGHT HORIZONTAL”), but still, having to do that for every character is a hassle. I could assign some keys to each of those characters, and then press C-x 8 and the assigned keys to type the characters (like C-x 8 a > for ), but that’s still too many keypresses. There’s an even better way: a custom input method, which I can toggle with a single key combination.

In this post, I assume you’re familiar with Emacs and Elisp, I won’t explain the basics here. I’m working in GNU Emacs, version 29.4.

Emacs has a built-in package called quail, which lets you define character translations – very useful for when you want to type in a language other than English. When you enable an input method with C-\ ((toggle-input-method)), you can type sequences of keys such as 'e to enter é, p for π, or po for depending on your input method. You can extend this to type any character you want (at least those defined by Unicode, I’m not sure what the limitations are), using a custom sequence of keys. It’s sort of similar to digraphs in Vim, but with a nicer interface (in my opinion) for both defining and using these mappings, especially since you can easily toggle this functionality and switch between different input methods. Also, you’re not limited to two keys, you can map an arbitrarily long sequence of keys.

Let’s write some code. I’m going to call the input method “boxdrawing”, and the package “quail-boxdrawing”. We’ll be working in the file ~/.config/emacs/lisp/quail-boxdrawing.el; create it. You may need to add this directory to your load-path in your config, here’s the part of my init.el that does that (my user-emacs-directory is ~/.config/emacs):

;; Define my custom scripts path as a constant (`za/` is my prefix for custom stuff)
(defconst za/manually-installed-package-dir
    (concat user-emacs-directory "lisp/")
    "The directory for packages (.lisp) that I manually install.")

;; Make sure it exists
(make-directory za/manually-installed-package-dir t)

;; Add it to the list of directories looked up when `require`ing things
(add-to-list 'load-path za/manually-installed-package-dir)

;; And add all subdirectories to the load path, recursively
(let ((default-directory  za/manually-installed-package-dir))
  (normal-top-level-add-subdirs-to-load-path))

Inside the file, add the following line to load quail:

(require 'quail)

Then, let’s define the input method:

(quail-define-package
    "boxdrawing" ; package name
    "English" ; input
    "|-" ; modeline indicator
    t ; show what keys you can press, like which-key
    "English with boxdrawing maps" ; docstring
    nil ; no additional translation keys
    t ; remember previous translations of a key
    nil ; if there's more than one translation for keys, let the user choose
    nil ; don't translate user's keyboard to standard layout
    nil ; don't need a visual diagram of keyboard in helptext
    nil ; don't need decode map (table from translations back to keys)
    nil ; don't break key sequences at shortest sequence
    nil ; no overlay
    nil ; use standard function to insert translated characters
    nil ; no additional conversion keys
    t) ; keep bindings like C-f and C-b the same as standard

You probably won’t need most of these arguments for simple input methods; if you want more detailed info, do C-h f quail-define-package.

Next, let’s define the keys we want to translate when the boxdrawing input method is active:

(quail-define-rules
 ("|-" ?├)
 ("--" ?─)
 ("|" ?│)
 ("|_" ?└))

Each argument to quail-define-rules is a two-element list, where the first element is a string containing the keys to type, and the second element is the character that should show up in the buffer (the leading ? indicates it’s a single character).

How does quail-define-rules know which input method contains those rules? It uses the “current Quail package”, which it finds via the buffer-local variable quail-current-package. That variable is a list, with contents similar to what we entered in the call to quail-define-package: (car quail-current-package) prints the name of the package. When you call quail-define-package, it calls quail-select-package, which sets this buffer-local variable.

Then add this at the end of the file:

(provide 'quail-boxdrawing)

Save the file, and we’re ready to try it out. Press M-:, type (require 'quail-boxdrawing), then press C-u C-\ and select the boxdrawing method. Try typing |- in a buffer, and you should get the character. And we’re done! This is already very useful, and you can extend and customize it easily however you need.