An open API service indexing awesome lists of open source software.

https://github.com/integral-dw/superstar-kit

A simple template for org-superstar like minor modes for other modes
https://github.com/integral-dw/superstar-kit

emacs emacs-lisp emacs-packages minor-mode outline template

Last synced: over 1 year ago
JSON representation

A simple template for org-superstar like minor modes for other modes

Awesome Lists containing this project

README

          

#+TITLE:superstar-kit: A Quickstart Template for *-bullet Modes
#+STARTUP: showeverything

* Contents
* *[[#about][About]]:* Start here.
+ *[[#this-package-is-not-a-package][This "Package" is NOT a Package]]:* Mission statement.
* *[[#a-guided-tour][A Guided Tour]]:* How to use this template explained using an imaginary
example.
+ *[[#defining-a-minor-mode][Defining a Minor Mode]]:* Getting started.
+ *[[#setting-up-font-lock][Setting up Font Lock]]:* Wrestling with boilerplate.
+ *[[#defining-font-lock-keywords][Defining Font Lock Keywords]]:* Adding features.
+ *[[#prettifiers-and-compose-region][Prettifiers and compose-region]]:* Implementing features explicitly.
- *[[#the-quintessential-prettifier---prettify-main-hbullet][The Quintessential Prettifier: --prettify-main-hbullet]]*
- *[[#more-prettifiers][More Prettifiers]]*
+ *[[#custom-variables-interfacing-to-the-end-user][Custom Variables: Interfacing to the End User]]:* Adding Options.
- *[[#advanced-custom-functionality][Advanced Custom Functionality]]:* Creating decent menus for the Custom interface.
+ *[[#hiding-and-the-invisibility-spec][Hiding and the Invisibility Spec]]:* Removing clutter from view.
+ *[[#disabling-a-mode-cleaning-up][Disabling a Mode: Cleaning up]]:* Exiting a minor mode (gracefully).
* *[[#extending-the-minor-mode][Extending the Minor Mode]]:* Extending the bare-bones minor mode (an
example).
+ *[[#beginning-with-a-vision][Beginning with a Vision]]:* Drafting new features.
+ *[[#defining-a-new-keyword][Defining a New Keyword]]:* Locating new syntax elements.
+ *[[#prettifiers-accessors-variables][Prettifiers, Accessors, Variables]]:* Implementing a new feature.
+ *[[#faces][Faces]]:* Tweaking the look.
+ *[[#cleaning-up][Cleaning up]]:* Disabling the new feature on exit.
* *[[#quick-reference][Quick Reference]]:* Where each symbol is first used in the guided tour.
* *[[#news][NEWS]]:* Updates of note.
* *[[#archive][Archive]]:* Yesterday's NEWS.

* About
:PROPERTIES:
:CUSTOM_ID: about
:END:
With the success of [[https://github.com/integral-dw/org-superstar-mode][Org Superstar]] it is pretty clear that there is a demand
for eye candy in Outline-related modes. A demand I won't be able to satisfy
on my own. Superstar simply does not have the tools to generalize to other
modes, and trying to would only harm it. For this reason, I decided to
provide something for the Emacs community that [[https://github.com/sabof][sabof]] (albeit unintentionally)
provided me with when I decided to create Org Superstar: a starting point. I
tried my best to strip down Superstar to its bare essentials, which I will try
to explain both via the already existing documentation as well as brief
descriptions in this README. By the end of it, you should have all the
facilities necessary to start working on your own eye-candy mode. However,
this "package" (or rather, your copy of it) won't exist anymore by the time
you are done, because\dots

** This "Package" is NOT a Package
:PROPERTIES:
:CUSTOM_ID: this-package-is-not-a-package
:END:
What do I mean by this? Well, =superstar-kit.el= /looks/ like a package. It
/behaves/ like a package, mostly, if loaded. Tools like ~package-lint~,
~checkdoc~, and ~flycheck~ have no complaints. So why isn't it on MELPA?
*Because it would make no sense.* The most crucial elements that make a mode
like this work are /placeholders/. The blanks still need to be filled in with
specifics of a mode. The placeholders are documented in a handy [[file:CHECKLIST.org][checklist]]
(=CHECKLIST.org=), and many changes can be made semi-automatically. But not
all. This is also by design, a design that in many ways is governed strongly
by what ~superstar-kit~ is *not*.

* It is NOT a dependency :: This is important. You don't ~require~ this file.
You take it, you edit it, you change the name. It's really just a template
to get you started quickly.
* It is NOT an automated build tool / set of macros / code generator ::
Org Superstar, with all its features, is over 630 LOC (lines of code) and
growing. It has legacy code. It has compatibility code. Bells and
whistles. It has lots of special cases to integrate well with Org.
Superstar Kit at the time of writing has 243 LOC. It also has none of
that. By the time you are done adding the complexities and features for
/your/ use case the time and effort invested into /that/ will outweigh whatever
a more advanced starter kit could have conceivably saved you. No point in
dwelling over starting off low-tech.
* It is NOT "feature complete" ::
Superstar Kit merely "implements" fancy headline bullets on its own.
That's basically it. No item bullets, no syntax checks, nothing. Instead,
it focuses on that one example, from which other features can be easily
inferred, to the point it may feel like a kill-yank-job. It does however
do a couple of things with fancy headline bullets. Enough to show what
this approach is roughly capable of.

* A Guided Tour
:PROPERTIES:
:CUSTOM_ID: a-guided-tour
:END:
In this section I will explain /how/ a mode derived from this starter kit
actually works, so you can really make it your own. I will only brush on
topics that are better explained elsewhere (such as how to define minor modes
and font lock details), and instead focus on the particularities of the core
mechanics inherited from Org Superstar and how to exploit them. We will start
with the main protagonist of the file, the minor mode itself, and will descend
into greater detail as we begin to require it. In other words, we will
utilize a crude approximation of /[[https://mitpress.mit.edu/sites/default/files/sicp/full-text/book/book-Z-H-14.html#%_idx_1306][wishful thinking]]/ to take the code apart.

*Side Note:* I added some links to info nodes referring to the documentation of
certain Emacs internals, but they won't show up on GitHub, so opening the
README in Emacs has benefits. Also, for those who just want to know where
they should start reading for a specific defined symbol, see the [[Quick Reference][quick reference]].

** Defining a Minor Mode
:PROPERTIES:
:CUSTOM_ID: defining-a-minor-mode
:END:
Our end goal is making a minor mode for some Outline-like mode. For that we
need to define one using ~define-minor-mode~.
#+begin_src elisp
;;; Mode commands
;;;###autoload
(define-minor-mode superstar-kit-mode
"Use UTF8 bullets for headlines and plain lists."
nil nil nil
:group 'superstar-kit
:require 'M-PKG
;; ...
)
#+end_src
Similar to a function definition, we begin with a function symbol to use both
interactively and in lisp. No argument list is required, so the next entry
is the docstring. The next three arguments (all nil) are of no particular
importance to us, as the mode we want to make is purely cosmetic and
consequently immensely unobtrusive. Finally, there is the ~&BODY~ of the
minor-mode, in which we will implement the necessary logic for our mode. We
see two special keywords here: ~:group~ and ~:require~, with placeholder symbols.
The former associates the mode with a customization group (which allows the
user to manipulate things via the custom interface) and the latter
automatically requires the mode we are writing this minor mode for.
Currently, the file is full of placeholders, so before anything else we must
first replace them for our application of interest. Suppose there is a bare
bones Outline-type of mode for simple note taking called ~grok-mode~, named
after Hubert Grokbold. Hubert likes ~org-superstar~ and wants to make a
similar minor-mode called ~grok-bullets~ for his mode. He consults the
=CHECKLIST= file and does everything up to the point where he is sent to the
=README=. Casting the paradox of him encountering his own hypothetical story
aside, he would have already progressed quite far towards making his own
mode. All instances of ~superstar-kit~ are replaced with ~grok-bullets~, among
other things. His newly created minor mode now reads:
#+begin_src elisp
;;;###autoload
(define-minor-mode grok-bullets-mode
"Use UTF8 bullets for headlines and plain lists."
nil nil nil
:group 'grok-bullets
:require 'grok
;; ...
)
#+end_src
It now auto-requires ~grok~ and also comes with its own custom group, which is
also already defined. Finally, the ~;;;###autoload~ cookie helps Emacs to
defer having to load the package until it is actually needed. Now, what
about the custom group itself? It's already almost fully predefined as well.
#+begin_src elisp
(defgroup grok-bullets nil
"Use UTF8 bullets for headlines and plain lists."
;; FIXME: Change this to the appropriate group of MODE
:group 'emacs)
#+end_src
The ~:group~ keyword here tells Emacs to put the entire group into a reasonable
super-group. Hubert takes a quick glance at the checklist again and finds
he's supposed to change the group to a Grok-related group. Luckily, ~grok~
defines a custom group of the same name, so replacing ~:group 'emacs~ with
~:group 'grok~ is all it took. Now a user can find the options of Grok Bullets
expectedly in the same category as those of Grok mode.

Next would be to set up the actual logic of the minor mode. Instead of
directly having to work with the function argument of a minor mode, all we
have to do in the ~&BODY~ is to check the value of the /variable/
~grok-bullets-mode~. This local variable is automatically generated. If
non-nil, the body should execute whatever necessary to enable the mode.
Conversely, a value of nil tells the mode to clean up after itself and exit.

** Setting up Font Lock
:PROPERTIES:
:CUSTOM_ID: setting-up-font-lock
:END:
Font Lock is the minor mode responsible for syntax highlighting in Emacs. It
will handle most of the low-level manipulations in our buffer and will locate
our syntax elements (headlines) we want to prettify. Naively, all we (or in
our case, Hubert) would hence need to do is pass a list of things for Font
Lock to do (conditionally), and tell Font Lock to stop highlighting these
things when the mode stops. This of course implies that our major mode *uses
Font Lock* in the first place.
#+begin_src elisp
(define-minor-mode grok-bullets-mode
"Use UTF8 bullets for headlines and plain lists."
nil nil nil
:group 'grok-bullets
:require 'grok
(cond
;; Set up Grok Bullets.
(grok-bullets-mode
;; ...
(font-lock-add-keywords nil grok-bullets--font-lock-keywords
'append)
;; ...
)
;; Clean up and exit.
(t
;; ...
(font-lock-remove-keywords nil grok-bullets--font-lock-keywords)
;; ...
))
#+end_src
This tells Font Lock to add or remove instructions in the current buffer
stored in ~grok-bullets--font-lock-keywords~. This would be fine if we didn't
want to be able to change and customize the keywords at runtime. However,
since we generally want to do that we need a function to update the variable
based on the current configuration (~grok-bullets--update-font-lock-keywords~).
We also want to tell Font Lock to update the buffer once it receives new
instructions (~grok-bullets--fontify-buffer~, which we won't need to look at).
Hence setting up the mode is a little more involved.
#+begin_src elisp
;; Set up Grok Bullets.
(grok-bullets-mode
(font-lock-remove-keywords nil grok-bullets--font-lock-keywords)
(grok-bullets--update-font-lock-keywords)
(font-lock-add-keywords nil grok-bullets--font-lock-keywords
'append)
(grok-bullets--fontify-buffer)
;; ...
)
#+end_src
The mode now cleans up whatever previous information we may have fed to Font
Lock, update the keywords and redraws the buffer.

** Defining Font Lock Keywords
:PROPERTIES:
:CUSTOM_ID: defining-font-lock-keywords
:END:
[[info:elisp#Search-based Fontification][Font Lock keywords]] are simple lists which come in a variety of forms, fully
documented in a corresponding info node. We will only use a small subset of
what keywords are capable of and restrict ourselves to the format
#+begin_src emacs-lisp
(REGEX . SUBEXP-HIGHLIGHTER)
#+end_src
meaning a cons of a [[info:Elisp#Regular Expressions][regular expression]] =REGEX= and a list =SUBEXP-HIGHLIGHTER=.
Each element of the latter is of the form
#+begin_src emacs-lisp
(SUBEXP FACESPEC [OVERRIDE [LAXMATCH]])
#+end_src
Where =SUBEXP= is an integer essentially corresponding to the number of a
numbered [[info:Elisp#Regexp Backslash][group]]^{}^{a)}, =FACESPEC= is an /expression/ whose value specifies the [[info:Elisp#Faces][face]] to
use (a symbol) and =OVERRIDE= and =LAXMATCH= are optional flags. To reiterate:
=FACESPEC= is an /expression/ which will be evaluated every time =REGEX= is
matched. *This is the core mechanism used by modes derived from this
template*. =OVERRIDE= governs whether aspects of existing fontification can be
overridden. A value of ~prepend~ works intuitively by merging properties of
the face with existing fontification, taking precedence. Let us now look at
the code.
#+begin_src elisp
(defvar-local grok-bullets--font-lock-keywords nil)

(defun grok-bullets--update-font-lock-keywords ()
"Set ‘grok-bullets--font-lock-keywords’ to reflect current settings.
You should not call this function to avoid confusing this mode’s
cleanup routines."
(setq grok-bullets--font-lock-keywords
;; FIXME: Replace REGEXP to match your headlines.
`(("^\\(?2:\\**?\\)\\(?1:\\*\\) "
(1 (grok-bullets--prettify-main-hbullet) prepend)
,@(unless grok-bullets-remove-leading-chars
'((2 (grok-bullets--prettify-leading-hbullets)
t)))
,@(when grok-bullets-remove-leading-chars
'((2 (grok-bullets--make-invisible 2))))))))
#+end_src
~grok-bullets--font-lock-keywords~ is simply initialized as an empty list, and
properly generated by ~grok-bullets--update-font-lock-keywords~ on the fly.
Now, in the case of Grok, our imaginary mode, asterisks are no longer what
defines a headline, but tildes. Hubert hence quickly fixes up the regular
expression and ticks another check box.
#+begin_src emacs-lisp
(defun grok-bullets--update-font-lock-keywords ()
"Set ‘grok-bullets--font-lock-keywords’ to reflect current settings.
You should not call this function to avoid confusing this mode’s
cleanup routines."
(setq grok-bullets--font-lock-keywords
`(("^\\(?2:~*?\\)\\(?1:~\\) "
(1 (grok-bullets--prettify-main-hbullet) prepend)
;; ...
))))
#+end_src
The logic used for constructing this particular keyword is quite simple, but
can be easily extended. By default, the custom variable
~grok-bullets-remove-leading-chars~ allows every headline character but the
first to be removed (visually), which is not a significant loss of
information since the depth of the headline can be encoded in the choice of
face used combined with the bullet character. Hence, two different functions
handle the possible ways in which leading characters are handled.
~grok-bullets--make-invisible~ is a versatile function that can be recycled to
optionally hide away verbose syntax that rarely if ever needs manual editing.
~grok-bullets--prettify-leading-hbullets~, much like
~grok-bullets--prettify-main-hbullet~ serves a singular purpose of providing
the eye candy.

a) *Remark:* The value 0 is special in the sense that it corresponds to the
entire match of =REGEX=.

** Prettifiers and ~compose-region~
:PROPERTIES:
:CUSTOM_ID: prettifiers-and-compose-region
:END:
A /prettifier/, in my nomenclature, is a function that visually modifies a
region from within Font Lock /beyond/ the [[info:Elisp#Faces][face]] properties. Consequently,
prettifiers are the abstractions doing the actual heavy lifting through Font
Lock. The name alludes to ~prettify-symbols-mode~, which this approach shares
a fair amount of conceptual DNA with. The effect of displaying some
character (here: =~=) as some other character (a /bullet/) is achieved using a
function called ~compose-region~ which handles character composition (serving
as a thin wrapper for an internal C function). For our purposes, it is a
function of three arguments ~(compose-region START END CHAR-OR-STRING)~,
displaying the region from =START= to =END= either as a single character or all
characters in a string superimposed. The latter can be used to make
characters which are "thinner" than a monospaced character, which hence may
look out of place, effectively monospaced by superimposing it with a space
instead of using the literal character. The downside to using ~compose-region~
this way is that superimposing characters can't be relied upon when Emacs is
used from a terminal. This is why special care has to be taken when dealing
with terminal displays, as we will see later.

*** The Quintessential Prettifier: ~--prettify-main-hbullet~
:PROPERTIES:
:CUSTOM_ID: the-quintessential-prettifier---prettify-main-hbullet
:END:
This is the most basic (and likely most iconic) prettifier.
#+begin_src emacs-lisp
(defun grok-bullets--prettify-main-hbullet ()
"Prettify the trailing tilde in a headline."
(let ((level (grok-bullets--heading-level)))
(compose-region (match-beginning 1) (match-end 1)
(grok-bullets--hbullet level)))
'grok-bullets-header-bullet)
#+end_src
Basically all of the actual complexity is tucked neatly away.
~grok-bullets--heading-level~ and ~grok-bullets--hbullet~ compute which bullet
to use, the function implicitly assumes the target character is defined by
the last regex match (sub-expression *1*) and returns a customizable face
~grok-bullets-header-bullet~. The function ~grok-bullets--heading-level~ is
comparably trivial, since the level of an outline is essentially assumed to
be the number of heading characters. Any other prettifier imaginable looks
similar to this. Take (parts of) the matched region, extract information
from it, compute the visual replacement, pass it to ~compose-region~, return a
face. Everything past this point either calls Emacs internals directly and
is of no concern to us, or interfaces to options exposed to the user. Hence
what remains is storing and accessing data.

*** More Prettifiers
:PROPERTIES:
:CUSTOM_ID: more-prettifiers
:END:
To fully complete this section it is necessary to also look at the other
default prettifier provided by this package. This one is a little more
involved, as leading characters have to be composed one by one,
necessitating a loop.
#+begin_src emacs-lisp
(defun grok-bullets--prettify-leading-hbullets ()
"Prettify the leading bullets of a header line.
Each leading tilde is rendered as ‘grok-bullets-leading-bullet’
and inherits face properties from ‘grok-bullets-leading’.

If viewed from a terminal, ‘grok-bullets-leading-fallback’ is
used instead of the regular leading bullet to avoid errors."
(let ((star-beg (match-beginning 2))
(lead-end (match-end 2)))
(while (< star-beg lead-end)
(compose-region star-beg (setq star-beg (1+ star-beg))
(grok-bullets--lbullet)))
'grok-bullets-leading))
#+end_src
We also see that the documentation already fully explains how this function
interacts with user-level variables. For each kind of data accessed there
is a corresponding accessor, in this case ~grok-bullets--lbullet~, and for
every kind of prettifier there is a face, in this case ~grok-bullets-leading~.

** Custom Variables: Interfacing to the End User
:PROPERTIES:
:CUSTOM_ID: custom-variables-interfacing-to-the-end-user
:END:
While prettifiers handle putting pretty symbols on the screen, we still
require data to hold them (and functions to access them). I also like to
define a nice custom interface, which also comes with the benefit of
declaring [[info:Elisp#Customization Types][valid types]]. If you are interested in supporting customization, I
recommend the corresponding [[info:Elisp#Customization][manual section]]. The data structure to hold
bullet chars for each heading level is a simple list. Each element
corresponds to the bullet to use for the corresponding level (starting from
zero).
#+begin_src elisp
(defcustom grok-bullets-headline-bullets-list
'(?◉ ?○ ?🞛 ?▷)
;; long docstring
:group 'grok-bullets
:type ;; long customization type declaration
)
#+end_src
It can either hold characters or a simple list with a string handed to
~compose-region~ as the first element and a fallback character for terminals as
the second. Writing a function that accesses such a list and distinguishes
the two cases is pretty straightforward.
#+begin_src elisp
(defun grok-bullets--nth-headline-bullet (n)
"Return the Nth specified headline bullet or its corresponding fallback.
N counts from zero. Headline bullets are specified in
‘grok-bullets-headline-bullets-list’."
(let ((bullet-entry
(elt grok-bullets-headline-bullets-list n)))
(cond
((characterp bullet-entry)
bullet-entry)
((display-graphic-p)
(elt bullet-entry 0))
(t
(elt bullet-entry 1)))))
#+end_src
However, this function on its own would be useless to a prettifier, as trying
to obtain bullets for levels greater than those specified would eventually
raise an error. To give the user some agency over how to extrapolate from
the given number of bullets, another custom variable is defined.
#+begin_src elisp
(defcustom grok-bullets-cycle-headline-bullets t
"Non-nil means cycle through available headline bullets.

The following values are meaningful:

An integer value of N cycles through the first N entries of the
list instead of the whole list.

If otherwise non-nil, cycle through the entirety of the list.

If nil, repeat the final list entry for all successive levels.

You should call ‘grok-bullets-restart’ after changing this
variable for your changes to take effect."
;; more custom interface boilerplate
)
#+end_src
This gives the user plenty of options to fine tune the mode's behavior to
their liking. All that is left to do is actually implement the accessor
function that obtains the correct bullet for the prettifier.
#+begin_src elisp
(defun grok-bullets--hbullets-length ()
"Return the length of ‘grok-bullets-headline-bullets-list’."
(length grok-bullets-headline-bullets-list))

(defun grok-bullets--hbullet (level)
"Return the desired headline bullet replacement for LEVEL N.

For more information on how to customize headline bullets, see
‘grok-bullets-headline-bullets-list’.

See also ‘grok-bullets-cycle-headline-bullets’."
(let ((max-bullets grok-bullets-cycle-headline-bullets)
(n (1- level)))
(cond ((integerp max-bullets)
(grok-bullets--nth-headline-bullet (% n max-bullets)))
(max-bullets
(grok-bullets--nth-headline-bullet
(% n (grok-bullets--hbullets-length))))
(t
(grok-bullets--nth-headline-bullet
(min n (1- (grok-bullets--hbullets-length))))))))
#+end_src
Since leading bullets do not change with the level (functioning more as
[[https://en.wikipedia.org/wiki/Leader_(typography)][leaders]]), their custom variables and accessors are rather straightforward.
#+begin_src elisp
(defcustom grok-bullets-leading-bullet ?.
;; docstring and custom boilerplate
)

(defcustom grok-bullets-leading-fallback
(cond ((characterp grok-bullets-leading-bullet)
grok-bullets-leading-bullet)
(t ?.))
;; again
)

;; some other code

(defun grok-bullets--lbullet ()
"Return the correct leading bullet for the current display."
(if (display-graphic-p)
grok-bullets-leading-bullet
grok-bullets-leading-fallback))
#+end_src
A particularly noteworthy trick here is how the fallback option defaults to
the regular bullet if there is no need for a fallback (that is, if the main
bullet is a character and works on terminals).

*** Advanced Custom Functionality
:PROPERTIES:
:CUSTOM_ID: advanced-custom-functionality
:END:
The custom interface allows us to do more than just specify a type for a
given variable. We can even define specialized setter functions and raise
errors depending on user input. We can for example mirror the load-up
behavior of ~grok-bullets-leading-bullet~ (also setting the fallback when it is
a character) in the custom interface by defining a function of the below form
and passing it to the variable's ~defcustom~ using the ~:set~ keyword.
#+begin_src elisp
(defun grok-bullets--set-lbullet (symbol value)
"Set SYMBOL ‘grok-bullets-leading-bullet’ to VALUE.
If set to a character, also set ‘grok-bullets-leading-fallback’."
(set-default symbol value)
(when (characterp value)
(set-default 'grok-bullets-leading-fallback value)))
#+end_src
Validating a customized value works similarly using the ~:validate~ [[info:Elisp#Type Keywords][keyword]] in
a given customization type. Here, we ensure that the number of bullets to
cycle through does not exceed the actual number of bullet items. The way we
have to communicate errors to custom is a little unusual, as it involves
handing the error information to the responsible widget and returning it.
Widgets on their own can fill an entire manual (in fact, [[info:Widget][they do]]), but all we
need to know here is that they are the buttons, text fields and check boxes
we interact with in the custom interface, and that we can manipulate them
with various functions through lisp. A validation function receives the
widget as its argument. We can "unpack" the user-set value with ~widget-value~
and override it with a valid input using ~widget-value-set~, should the user
input be incorrect. Finally, we can pass an error message to the widget
using ~(widget-put WIDGET :error ERROR-MESSAGE-STRING)~. We should only
manipulate the widget if the user input is erroneous, and return nil if it
isn't. With this knowledge we can write perfectly fine validation functions
such as the one the template already defines.
#+begin_src elisp
(defun grok-bullets--validate-hcycle (text-field)
"Raise an error if TEXT-FIELD’s value is an invalid hbullet number.
This function is used for ‘grok-bullets-cycle-headline-bullets’.
If the integer exceeds the length of
‘grok-bullets-headline-bullets-list’, set it to the length and
raise an error."
(let ((ncycle (widget-value text-field))
(maxcycle (grok-bullets--hbullets-length)))
(unless (<= 1 ncycle maxcycle)
(widget-put
text-field
:error (format "Value must be between 1 and %i"
maxcycle))
(widget-value-set text-field maxcycle)
text-field)))
#+end_src

** Hiding and the Invisibility Spec
:PROPERTIES:
:CUSTOM_ID: hiding-and-the-invisibility-spec
:END:
With prettifiers and their internals and interfaces out of the way, there is
only one more aspect to the Font Lock code that has not been looked at in
greater detail.
#+begin_src elisp
(defun grok-bullets--update-font-lock-keywords ()
;; docstring
(setq grok-bullets--font-lock-keywords
`(("^\\(?2:~*?\\)\\(?1:~\\) "
;; ... (we already covered this part)
,@(when grok-bullets-remove-leading-chars
'((2 (grok-bullets--make-invisible 2))))))))
#+end_src
Making text in a buffer [[info:Elisp#Invisible Text][invisible]] is another lower-level feature of Emacs.
It does exactly what it sounds like, and requires nothing beyond adding a
simple [[info:Elisp#Text Properties][text property]] to the region in question. What essentially happens in
the background is that Emacs stores a small bit of metadata (the symbol
~grok-bullets-hide~) in the buffer region. That symbol needs to be added to
the so-called "invisibility spec" to function correctly, necessitating one
more line of boilerplate in our mode setup.
#+begin_src elisp
(define-minor-mode grok-bullets-mode
;; etc.
(cond
;; Set up Grok Bullets.
(grok-bullets-mode
;; ... (as before)
(add-to-invisibility-spec '(grok-bullets-hide)))
;; ...
))

#+end_src
Implementing support for making the leading characters invisible then turns
out to be rather straightforward.
#+begin_src elisp
(defcustom grok-bullets-remove-leading-chars nil
;; docstring
:group 'grok-bullets
:type 'boolean)

;; some code

(defun grok-bullets--make-invisible (subexp)
"Make part of the text matched by the last search invisible.
SUBEXP, a number, specifies which parenthesized expression in the
last regexp. If there is no SUBEXPth pair, do nothing."
(let ((start (match-beginning subexp))
(end (match-end subexp)))
(when start
(add-text-properties
start end '(invisible grok-bullets-hide)))))
#+end_src
This completes all features available to the basic mode. All that remains is
some cleanup should the mode be disabled or restarted.

** Disabling a Mode: Cleaning up
:PROPERTIES:
:CUSTOM_ID: disabling-a-mode-cleaning-up
:END:
Now that the worst part of defining the mode is over, all that is left are
cleanup functions. First, the mode itself needs to handle the case of
(~grok-bullets-mode~) being nil.
#+begin_src elisp
(define-minor-mode grok-bullets-mode
"Use UTF8 bullets for headlines and plain lists."
nil nil nil
:group 'grok-bullets
:require 'grok
(cond
;; ...
;; Clean up and exit.
(t
(remove-from-invisibility-spec '(grok-bullets-hide))
(font-lock-remove-keywords nil grok-bullets--font-lock-keywords)
(grok-bullets--unprettify-hbullets)
(grok-bullets--fontify-buffer))))
#+end_src
Apart from cleaning up the invisibility spec and Font Lock keywords all that
is left is undoing the work of the prettifiers with a corresponding
/unprettifier/.
#+begin_src elisp
(defun grok-bullets--unprettify-hbullets ()
"Revert visual tweaks made to header bullets in current buffer."
(save-excursion
(goto-char (point-min))
;; FIXME: Replace REGEXP to match your headlines.
(while (re-search-forward "^\\*+ " nil t)
(decompose-region (match-beginning 0) (match-end 0)))))
#+end_src
Unlike the prettifiers, which operate only on one match in the file, an
unprettifier traverses the entire file. Undoing composing is done by the
aptly-named ~decompose-region~. This is also the last part we have edit
manually for the mode to work. We could use the same regex we used for the
Font Lock keyword, but since we don't need groups we get away just using
~(re-search-forward "^~+ " nil t)~.

* Extending the Minor Mode
:PROPERTIES:
:CUSTOM_ID: extending-the-minor-mode
:END:
After consulting the =CHECKLIST= file your minor mode should already work
decently and compile without warning. However, the mode is rather bare bones,
which is why I want to give a minor example for how to implement a new
feature. For this reason, we will now take a look at our hypothetical Hubert
Grokbold implementing a new feature for his ~grok-bullets~ mode.
** Beginning with a Vision
:PROPERTIES:
:CUSTOM_ID: beginning-with-a-vision
:END:
Suppose Grok mode supports a fancy type of text block, called grok blocks.
Each line of a grok block begins with an integer enclosed in square brackets,
followed by a =>=, like this:
#+begin_src fundamental
[0]> Quote of the day: "Stay hydrated, this is a threat."
[1]> Buy eggs, milk, cereal, flour, toothpaste,
[1]> 4 chicken thighs, 500g breast, celery.
[2]> Remember to look up the tampon brand in the bathroom.
[3]> Dentist appointment next week => calendar!
[1]> Also, remember to take the trash out.
#+end_src
Possibly, the integers represent the importance of the note. Hubert wants to
prettify grok blocks. He imagines the following:
* Instead of =[1]=, he would like a symbol depending on the integer.
* Instead of =>=, he would like some other character.
* A face for both.
* He wants to highlight important lines and de-emphasize unimportant ones.

** Defining a New Keyword
:PROPERTIES:
:CUSTOM_ID: defining-a-new-keyword
:END:
How does one accomplish that? It becomes clear that three components need to
be distinguished, =[1]=, =>=, and the rest of the line.
#+begin_src elisp
(defun grok-bullets--update-font-lock-keywords ()
"Set ‘grok-bullets--font-lock-keywords’ to reflect current settings.
You should not call this function to avoid confusing this mode’s
cleanup routines."
(setq grok-bullets--font-lock-keywords
`(("^\\(?2:~*?\\)\\(?1:~\\) "
(1 (grok-bullets--prettify-main-hbullet) prepend)
,@(unless grok-bullets-remove-leading-chars
'((2 (grok-bullets--prettify-leading-hbullets)
t)))
,@(when grok-bullets-remove-leading-chars
'((2 (grok-bullets--make-invisible 2)))))
("^\\(?1:\\[[0-9]+\\]\\)\\(?2:>\\)\\(?3: .*\\)$"
(1 (grok-bullets--prettify-gb-priority))
(2 (grok-bullets--prettify-gb-delim))
(3 (grok-bullets--gb-face))))))
#+end_src
** Prettifiers, Accessors, Variables
:PROPERTIES:
:CUSTOM_ID: prettifiers-accessors-variables
:END:
Hubert requires two prettifiers and one function that simply obtains the
face for the remaining line. Since everything is already nicely packaged
away into neat groups, working on them is comparably easy.
#+begin_src elisp
(defun grok-bullets--prettify-gb-priority ()
"Prettify the priority of a Grok block line."
(let ((priority (grok-bullets--priority)))
(compose-region (match-beginning 1) (match-end 1)
(grok-bullets--gb-icon priority)))
'grok-bullets-priority-icon)
#+end_src
What remains to do for this prettifier are defining the function to compute
the priority, an accessor function obtaining the correct icon and a face.
Hubert looks at how bullets are stored in his mode and copies the approach.
However, it makes no sense to be able to cycle through icons for higher
priorities, so the last one just repeats.
#+begin_src elisp
(defcustom grok-bullets-priority-icons
'((" " ?\s) (" ○" ?○) (" ❔" ??) (" ❗" ?!))
"List of icons used in Grok blocks.
It can contain any number of icons, the Nth entry usually
corresponding to the icon used for priority N.

Every entry in this list can either be a character or a list.
Characters are used as simple, verbatim replacements of the
headline character for every display (be it graphical or
terminal). If the list element is a list, it should be of the
general form
\(COMPOSE-STRING CHARACTER)

where COMPOSE-STRING should be a string according to the rules of
the third argument of ‘compose-region’. It will be used to
compose the specific priority icon. CHARACTER is the fallback
character used in terminal displays, where composing characters
cannot be relied upon.

You should re-enable Grok Bullets after changing this variable
for your changes to take effect."
:group 'grok-bullets
:type '(repeat (choice
(character :value ?!
:format "Icon: %v\n"
:tag "Simple icon")
(list :tag "Advanced string and fallback"
(string :value "!"
:format "String of characters to compose: %v")
(character :value ?!
:format "Fallback character for terminal: %v\n")))))
#+end_src
Next would be the function accessing the priority information, which simply
has to strip the surrounding brackets and turn the string to an integer, and
the function to access the custom variable.
#+begin_src elisp
(defun grok-bullets--priority ()
"Return the priority of the Grok block line."
(let ((token (match-string 1)))
(string-to-number
(substring token 1 (1- (length token))))))

(defun grok-bullets--gb-icon (priority)
"Obtain Grok block icon for the given PRIORITY.

If PRIORITY is greater than the number of icons specified in
‘grok-bullets-priority-icons’, return the highest priority
icon."
(let* ((priority (min priority
(1- (length grok-bullets-priority-icons))))
(entry (elt grok-bullets-priority-icons priority)))
(cond
((characterp entry)
entry)
((display-graphic-p)
(elt entry 0))
(t
(elt entry 1)))))
#+end_src
Prettifying the delimiter is trivial in comparison.
#+begin_src elisp
(defcustom grok-bullets-gb-delimiter ?»
"Character to delimit Grok block lines.
This variable is a character replacing the default greater-than
in terminal displays instead of ‘grok-bullets-leading-bullet’.

You should re-enable Grok Bullets after changing this
variable for your changes to take effect."
:group 'grok-bullets
:type '(character :tag "Character to display"
:format "\n%t: %v\n"
:value ?>))

;; ...

(defun grok-bullets--prettify-gb-delim ()
"Prettify the delimiter of a Grok block line."
(compose-region (match-beginning 2) (match-end 2)
grok-bullets-gb-delimiter)
'grok-bullets-priority-icon)
#+end_src
** Faces
:PROPERTIES:
:CUSTOM_ID: faces
:END:
Defining simple faces is comparably straightforward, although it is best to
still read up on it, both the [[info:Elisp#Faces][info node]] as well as the documentation of
~defface~ could prove useful here. Hubert believes that the best default is a
subtle default, so he just inherits the default face.
#+begin_src elisp
(defface grok-bullets-priority-icon
'((default . (:inherit default)))
"Face used to display prettified Grok block icons."
:group 'grok-bullets)
#+end_src
For the final necessary element (a function providing priority-dependent
faces) Hubert wants to try something more extravagant. Instead of creating a
fixed number of faces and potentially providing the user with some flags to
modify the mode's behavior he decides to mirror the way bullets are stored.
This is possible because faces don't /have/ to be symbols. Instead, property
lists can be used. These /anonymous faces/ can be stored in a list. The face
function is then consequently straightforward.
#+begin_src elisp
(defcustom grok-bullets-priority-faces
'((:foreground "gray70" :slant italic)
default
(:weight bold)
(:weight bold :foreground "red3"))
"Faces to use for Grok block lines of a given priority.

Should a Grok block line have a higher priority than the highest
specified by this variable, the highest available is used."
:group 'grok-bullets
:type '(repeat
(choice :tag "Face spec"
(face :value default)
(plist :key-type (symbol :tag "Property")
:tag "Face properties"))))
;; ...

(defun grok-bullets--gb-face ()
"Return the appropriate face to use for the given priority."
(let* ((priority (grok-bullets--priority))
(facespec (elt grok-bullets-priority-faces
priority)))
(or facespec
(last grok-bullets-priority-faces))))
#+end_src

** Cleaning up
:PROPERTIES:
:CUSTOM_ID: cleaning-up
:END:
For each new set of /prettifiers/ there needs to be a corresponding
/unprettifier/ in case the user wants to disable your mode. Consequently,
Hubert needs to implement an unprettifier for Grok blocks to have the mode
exit cleanly (as it should).
#+begin_src elisp
(defun grok-bullets--unprettify-gb ()
"Revert visual tweaks made to grok blocks in current buffer."
(save-excursion
(goto-char (point-min))
(while (re-search-forward "^\\[[0-9]+\\]> " nil t)
(decompose-region (match-beginning 0) (match-end 0)))))

;; ...

(define-minor-mode grok-bullets-mode
;; ... (nothing new)
(cond
;; Set up Grok Bullets.
(grok-bullets-mode
;; ...
)
;; Clean up and exit.
(t
(remove-from-invisibility-spec '(grok-bullets-hide))
(font-lock-remove-keywords nil grok-bullets--font-lock-keywords)
(grok-bullets--unprettify-hbullets)
(grok-bullets--unprettify-gb)
(grok-bullets--fontify-buffer))))
#+end_src
With this, the mode is finally complete again and ready for shipping (after
some thorough testing, of course).
* Quick Reference
:PROPERTIES:
:CUSTOM_ID: quick-reference
:END:
For the impatient, here is a list of all symbols with their original names, in
order of appearance in the [[A Guided Tour][guided tour]] above. Implementation of functions is
often addressed later in dedicated sections, with the first mention usually
showing where it is utilized instead.

* Defining a Minor Mode ::
+ ~superstar-kit-mode~ (minor mode)
+ ~superstar-kit~ (group)
* Setting up Font Lock ::
+ ~superstar-kit--update-font-lock-keywords~ (private function)
+ ~superstar-kit--font-lock-keywords~ (private buffer local variable)
+ ~superstar-kit--fontify-buffer~ (private function)
* Defining Font Lock Keywords ::
+ ~superstar-kit-remove-leading-chars~ (custom variable)
+ ~superstar-kit--prettify-main-hbullet~ (private function)
+ ~superstar-kit--prettify-leading-hbullets~ (private function)
+ ~superstar-kit--make-invisible~ (private function)
+ The Quintessential Prettifier: ~--prettify-main-hbullet~ ::
- ~superstar-kit--heading-level~ (private function)
- ~superstar-kit-header-bullet~ (face)
+ More Prettifiers ::
- ~superstar-kit-leading~ (face)
- ~superstar-kit-leading-bullet~ (custom variable)
- ~superstar-kit-leading-fallback~ (custom variable)
- ~superstar-kit--lbullet~ (private function)
* Custom Variables: Interfacing to the End User ::
+ ~superstar-kit-headline-bullets-list~ (custom variable)
+ ~superstar-kit-cycle-headline-bullets~ (custom variable)
+ ~superstar-kit--nth-headline-bullet~ (private function)
+ ~superstar-kit--hbullets-length~ (private function)
+ ~superstar-kit--hbullet~ (private function)
+ Advanced Custom Functionality ::
- ~superstar-kit--set-lbullet~ (private function)
- ~superstar-kit--validate-hcycle~ (private function)
* Hiding and the Invisibility Spec ::
+ ~grok-bullets-hide~ (symbol)
* Disabling a Mode: Cleaning up ::
+ ~superstar-kit--unprettify-hbullets~ (private function)
+ ~superstar-kit-restart~ (interactive function)

* NEWS
:PROPERTIES:
:CUSTOM_ID: news
:END:

* Archive
:PROPERTIES:
:CUSTOM_ID: archive
:END:

# LocalWords: Grokbold fontification prettifiers prettifier accessors cdr
# LocalWords: accessor unprettifier