emacs/var/elfeed/db/data/bf/bfd12bf58d7abcd8b914e7ad765c96bc911292b6
2022-01-03 12:49:32 -06:00

403 lines
16 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<p>In Emacs a “theme” is a set of configurations that can be enabled or
disabled as a block. Each of those controls a construct of the
rendering engine known as a “face”. Faces store the properties that are
associated with each element on display. These properties encompass
background and/or foreground colours as well as typographic attributes,
such as the font weight or slant.</p>
<h2>Finding faces</h2>
<p>Themes are programs written in Emacs Lisp (Elisp), whose intended role
is to control faces. We can learn about all the faces that are loaded
in the current session with <code>M-x list-faces-display</code>. The command will
produce a buffer with the <em>symbol</em> (i.e. unique name) of the face and a
preview of how it looks.</p>
<p>You can always consult the help page of a given symbol with <code>C-h o</code>
(<code>M-x describe-symbol</code>). Place the point over a faces symbol, type
<code>C-h o</code> to have the thing at point as the default option. Select that
(such as with <code>M-n</code>) to get a description of what it is supposed to do.</p>
<p>If we do this over the <code>cursor</code> face, we get the following:</p>
<blockquote>
<p>Basic face for the cursor color under X. Currently, only the
:background attribute is meaningful; all other attributes are
ignored. The cursor foreground color is taken from the background
color of the underlying text.</p>
<p>Note: Other faces cannot inherit from the cursor face.</p>
</blockquote>
<p>As with all <code>*Help*</code> buffers, the ones for individual faces contain a
link to the library that defines them. We are informed, for instance,
that the <code>cursor</code> is defined in <code>faces.el</code>. So we can always visit the
source code from there whenever we need to understand more about the
item of our inquiry.</p>
<p>Note that <code>list-faces-display</code> will only cover the libraries that are
currently loaded, but not necessarily the faces that your active theme
defines. If you have installed some package that you have not used yet,
then any faces it may be defining will not be immediately available in
the <code>*Faces*</code> buffer. To actually include those in the list, you need
to either use their package or explicitly load the relevant file with
<code>M-x load-library</code>. You can always regenerate the <code>*Faces*</code> buffer by
typing <code>g</code> while inside of it.</p>
<h2>Configuring an individual face</h2>
<p>Before we proceed to write a fully fledged theme, let us first examine
how to control faces one by one. The function dedicated to that task is
<code>set-face-attribute</code>. Read its documentation string with <code>C-h f</code>
followed by its symbol. <strong>This is important</strong> because it provides
valuable information about the properties that a face may be associated
with. You will need it when configuring your own theme.</p>
<p><em>Assuming you read the documentation</em> of <code>set-face-attribute</code>, let us
consider this example:</p>
<pre><code class="language-elisp">(set-face-attribute 'cursor nil :background "red")
</code></pre>
<p>We have learnt that the <code>cursor</code> only recognises a <code>:background</code>
property and will ignore any other. What we do here is instruct it to
use the generic red colour.</p>
<p>To confirm that this works, place the point to the right of the closing
parenthesis and type <code>C-x C-e</code> (which calls <code>eval-last-sexp</code>). Your
cursor show now be coloured red. If you were to put this in your
initialisation file, or any other library that gets loaded when you open
Emacs, your cursor would always get the colour you specified (unless
something else overrides it later on, but you get the point).</p>
<p>A good use-case for this is to define your font families for the three
main constructs of <code>default</code>, <code>variable-pitch</code>, and <code>fixed-pitch</code>.</p>
<p>This is the gist of what is included in the manual of the Modus themes
on the topic of <a href="https://protesilaos.com/emacs/modus-themes/#h:defcf4fc-8fa8-4c29-b12e-7119582cc929">font configurations for Org (and
others)</a></p>
<pre><code class="language-elisp">;; my custom build of Iosevka
;; https://gitlab.com/protesilaos/iosevka-comfy
(set-face-attribute 'default nil :font "Iosevka Comfy-15")
(set-face-attribute 'variable-pitch nil :family "Source Sans Pro" :height 1.0)
(set-face-attribute 'fixed-pitch nil :family "Iosevka Comfy" :height 1.0)
</code></pre>
<p>Depending on what you want to do, you can use Elisp to further control
things. Here we can be a bit more succinct by using <code>dolist</code> (remember
that <code>C-h f</code>, <code>C-h v</code>, or just <code>C-h o</code> are among your most valuable
tools in your Emacs journey).</p>
<pre><code class="language-elisp">(dolist (face '(default fixed-pitch))
(set-face-attribute face nil :family "Iosevka Comfy"))
</code></pre>
<h2>Using colours</h2>
<p>We can find the names of all generic colours with <code>list-colors-display</code>.
Notice how earlier we specified the <code>:background</code> of the <code>cursor</code> face
to a <code>"red"</code> value. Alternatively, one could use a hexadecimal RGB
code, such as <code>"#ff0000"</code> for pure red. I prefer the latter because it
is more precise and flexible.</p>
<p>How you specify colours is ultimately up to you. Picking the right
values is not an easy task. It is a field of endeavour that stands at
the intersection or art and science, as I explained in my essay about
the <a href="https://protesilaos.com/codelog/2020-03-17-design-modus-themes-emacs/">design of the Modus themes</a>.</p>
<h2>Deconstructing an Emacs face</h2>
<p>While <code>set-face-attribute</code> is perfectly fine for a few items, it becomes
inefficient at scale. This is why Emacs provides the
<code>custom-theme-set-faces</code> function. Before we start using that, <em>we must
first understand what the specifications of a face are</em>.</p>
<p>Consider this excerpt from <code>M-x find-library RET faces</code> (here “RET”
means to type the command, then confirm your choice with the
Return/Enter key, and follow it up with the “faces” library).</p>
<pre><code class="language-elisp">(defface tab-bar
'((((class color) (min-colors 88))
:inherit variable-pitch
:background "grey85"
:foreground "black")
(((class mono))
:background "grey")
(t
:inverse-video t))
"Tab bar face."
:version "27.1"
:group 'basic-faces)
</code></pre>
<p>We can read all about these specs with <code>C-h o defface</code>. Again, read the
docs to save yourself from trouble and frustration. While you start
making a habit of that, let me simplify this <code>defface</code> for you (extra
space for didactic purposes):</p>
<pre><code class="language-elisp">(defface tab-bar
'(
(
((class color) (min-colors 88))
:inherit variable-pitch
:background "grey85"
:foreground "black")
(
((class mono))
:background "grey")
(t
:inverse-video t)
)
"Tab bar face.")
</code></pre>
<p>Here we have the general structure of an expression that evaluates
multiple conditions. It looks like <code>cond</code>:</p>
<pre><code class="language-elisp">(cond
((FIRST TEST)
FIRST RESULT)
((SECOND TEST)
SECOND RESULT)
(t ; if none of the above
FALLBACK RESULT))
</code></pre>
<p>With these in mind, we can read each test more easily. Focus on this:</p>
<pre><code class="language-elisp">(
((class color) (min-colors 88))
:inherit variable-pitch
:background "grey85"
:foreground "black")
</code></pre>
<p>It checks whether the display terminal can support a minimum of 88
colours. If you are using Emacs with a graphical toolkit, this is most
likely the case. If the condition is satisfied, this face will use
<code>grey85</code> for its background and <code>black</code> for its foreground. Whereas in
more limited display terminals, it uses something simpler:</p>
<pre><code class="language-elisp">(
((class mono))
:background "grey")
</code></pre>
<p>Same principle for the fallback condition, which merely inverts the
colours with the assumption that those are some variant of black and
white for the foreground/background:</p>
<pre><code class="language-elisp">(t
:inverse-video t)
</code></pre>
<p>While you could define all your faces to adapt to every possible display
terminal out there, I find that what one typically needs is to optimise
for <code>((class color) (min-colors 89))</code>.</p>
<p>With these in mind, we can start writing our first theme.</p>
<h2>Skeleton of a custom theme</h2>
<p>As noted in the previous section, Emacs offers <code>custom-theme-set-faces</code>
for the express purpose of streamlining the process of controlling faces
in bulk. As always, read the documentation of that function to learn
more about the finer points.</p>
<p>Here we will be working with a minimal, yet perfectly usable base.
Every theme must be placed in a file whose name follows the pattern of
<code>SYMBOL-theme.el</code>. We declare the symbol of our theme with the
following:</p>
<pre><code class="language-elisp">(deftheme prot-base
"The basis for a custom theme.")
</code></pre>
<p>The above means that the file name must be <code>prot-base-theme.el</code> (we have
some more code at the end of the file, but we take things one step at a
time).</p>
<p>Now we want to configure a set of faces that are optimised for the
display spec of <code>((class color) (min-colors 89))</code>. Instead of writing
this expression each time, we will dynamically bind it to a variable,
using <code>let</code>.</p>
<pre><code class="language-elisp">(let ((class '((class color) (min-colors 89)))
...other variables)
...body)
</code></pre>
<p>Since we are defining local variables, it is a good idea to also write
our colours here, so that we economise on typing, but also to avoid
discrepencies. Each colour is defined as <code>(name value)</code>.</p>
<pre><code class="language-elisp">(let ((class '((class color) (min-colors 89)))
(main-bg "#ffffff") (main-fg "#000000")
(red "#a00000") (green "#005000") (blue "#000077"))
...body)
</code></pre>
<p>Everything is in place to start defining face attributes. The body of
our dynamically bound variables contains <code>custom-theme-set-faces</code>,
followed by the name of the <code>deftheme</code> we declared and then each faces
symbol, display spec and attributes:</p>
<pre><code class="language-elisp">(deftheme prot-base
"The basis for a custom theme.")
(let ((class '((class color) (min-colors 89)))
(main-bg "#ffffff") (main-fg "#000000")
(red "#a00000") (green "#005000") (blue "#000077"))
(custom-theme-set-faces
'prot-base
`(default ((,class :background ,main-bg :foreground ,main-fg)))
`(cursor ((,class :background ,red)))
`(font-lock-builtin-face ((,class :foreground ,blue))))
`(font-lock-string-face ((,class :foreground ,green))))
(provide-theme 'prot-base)
(provide 'prot-base-theme)
</code></pre>
<p>This is a valid theme. To actually use it, you must write it to a file,
which in this case is <code>prot-base-theme.el</code>. This file must be in a
directory read by Emacs. Say you put it in <code>~/.emacs.d/themes/</code>. To
inform Emacs about it, evaluate this:</p>
<pre><code class="language-elisp">(add-to-list 'load-path "~/.emacs.d/themes/")
</code></pre>
<p>With the theme written at <code>~/.emacs.d/themes/prot-base-theme.el</code>, you
can now <code>M-x load-theme RET prot-base</code>. And there you have it!</p>
<p>Note though that you may also need to <code>M-x disable-theme</code> and specify
the one currently in use to make sure you do not get mixed results
(unless you want to overlay one theme on top of another, but I will let
you run such experiments).</p>
<p>Remember to rely on <code>list-faces-display</code> to find all the symbols you
wish to cover. Furthermore, you can always identify the properties of
the character at point with <code>M-x describe-char</code> (or type it directly
with <code>C-u C-x =</code>). If it uses a face, you will see it mentioned in the
resulting <code>*Help*</code> buffer.</p>
<p>To understand the syntax for backquotes and commas, type <code>M-:</code> and then
insert <code>(info "(elisp) Backquote")</code>. This will take you to the relevant
node in the Emacs Lisp Reference Manual.</p>
<h2>More tools for theme developers</h2>
<p>These are excerpts from my dotemacs. They are meant to further assist
you in the task of developing a custom theme. Check the doc string of
each variable and adapt things to your liking.</p>
<h3>Rainbow mode for colour previews</h3>
<p>While experience may help estimate with decent accuracy a hexadecimal
RGB colour, it is always better to have a live preview available. Once
the following package is loaded, you can get it with <code>M-x rainbow-mode</code>.</p>
<pre><code class="language-elisp">(use-package rainbow-mode
:ensure
:diminish
:commands rainbow-mode
:config
(setq rainbow-ansi-colors nil)
(setq rainbow-x-colors nil))
</code></pre>
<h3>Use a linter front-end to improve your code</h3>
<p>You can either rely on the built-in <code>flymake</code> or the third party
<code>flycheck</code>. Both work great with Elisp files. You activate them with
<code>flymake-mode</code> or <code>flycheck-mode</code> respectively.</p>
<pre><code class="language-elisp">(use-package flymake
:commands flymake-mode
:config
(setq flymake-fringe-indicator-position 'left-fringe)
(setq flymake-suppress-zero-counters t)
(setq flymake-start-on-flymake-mode t)
(setq flymake-no-changes-timeout nil)
(setq flymake-start-on-save-buffer t)
(setq flymake-proc-compilation-prevents-syntax-check t)
(setq flymake-wrap-around nil))
(use-package flycheck
:ensure
:commands flycheck-mode
:config
(setq flycheck-check-syntax-automatically
'(save mode-enabled))
:hook (flycheck-error-list-mode-hook . visual-line-mode))
</code></pre>
<p>If you go with Flycheck, you may also want a modeline indicator, unless
you use a custom modeline that already defines one:</p>
<pre><code class="language-elisp">(use-package flycheck-indicator
:ensure
:after flycheck
:config
(setq flycheck-indicator-icon-error (string-to-char "!"))
(setq flycheck-indicator-icon-info (string-to-char "·"))
(setq flycheck-indicator-icon-warning (string-to-char "*"))
(setq flycheck-indicator-status-icons
'((not-checked "%")
(no-checker "-")
(running "&amp;")
(errored "!")
(finished "=")
(interrupted "#")
(suspicious "?")))
:hook (flycheck-mode-hook . flycheck-indicator-mode))
</code></pre>
<p>And here is how to ensure that you are following best practices for
packaging Elisp libraries (you only need one of the two, depending on
the front-end you choose):</p>
<pre><code class="language-elisp">(use-package flycheck-package
:ensure
:after flycheck
:config
(flycheck-package-setup))
(use-package package-lint-flymake
:ensure
:after flymake
:config
(package-lint-flymake-setup))
</code></pre>
<h2>Remember that Emacs themes are Elisp programs</h2>
<p>It should be clear by now that a theme can rely on advanced programming
techniques to do its work. Here we used <code>let</code>. While you can always go
with something simple, you retain the option to define more elaborate
criteria that, say, come into effect once a certain variable is enabled.</p>
<p>My Modus themes, which were <a href="https://protesilaos.com/codelog/2020-08-27-emacs-modus-themes-core/">recently added to upstream
Emacs</a>,
contain lots of Elisp logic, making them highly customisable. Study
their <a href="https://gitlab.com/protesilaos/modus-themes">source code</a> if you
want. It can help you learn more about defining and then evaluating
customisation options.</p>
<p>Use the information in this document to write your own theme or to just
gain insight into how the theme of your choice is designed.</p>
<p>Good luck!</p>