{"id":18027810,"url":"https://github.com/integral-dw/superstar-kit","last_synced_at":"2025-03-27T02:31:01.481Z","repository":{"id":122073385,"uuid":"336583400","full_name":"integral-dw/superstar-kit","owner":"integral-dw","description":"A simple template for org-superstar like minor modes for other modes","archived":false,"fork":false,"pushed_at":"2021-02-12T11:37:33.000Z","size":37,"stargazers_count":13,"open_issues_count":0,"forks_count":2,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-03-22T22:04:34.798Z","etag":null,"topics":["emacs","emacs-lisp","emacs-packages","minor-mode","outline","template"],"latest_commit_sha":null,"homepage":"","language":"Emacs Lisp","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/integral-dw.png","metadata":{"files":{"readme":"README.org","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-02-06T16:33:23.000Z","updated_at":"2023-02-26T22:07:33.000Z","dependencies_parsed_at":null,"dependency_job_id":"5a84ce7e-977d-4f46-b504-4ffbaaf75944","html_url":"https://github.com/integral-dw/superstar-kit","commit_stats":null,"previous_names":[],"tags_count":0,"template":true,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/integral-dw%2Fsuperstar-kit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/integral-dw%2Fsuperstar-kit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/integral-dw%2Fsuperstar-kit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/integral-dw%2Fsuperstar-kit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/integral-dw","download_url":"https://codeload.github.com/integral-dw/superstar-kit/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245769261,"owners_count":20669150,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["emacs","emacs-lisp","emacs-packages","minor-mode","outline","template"],"created_at":"2024-10-30T08:13:17.063Z","updated_at":"2025-03-27T02:31:01.473Z","avatar_url":"https://github.com/integral-dw.png","language":"Emacs Lisp","funding_links":[],"categories":[],"sub_categories":[],"readme":"#+TITLE:superstar-kit: A Quickstart Template for *-bullet Modes\n#+STARTUP: showeverything\n\n* Contents\n  * *[[#about][About]]:* Start here.\n    + *[[#this-package-is-not-a-package][This \"Package\" is NOT a Package]]:* Mission statement.\n  * *[[#a-guided-tour][A Guided Tour]]:* How to use this template explained using an imaginary\n    example.\n    + *[[#defining-a-minor-mode][Defining a Minor Mode]]:* Getting started.\n    + *[[#setting-up-font-lock][Setting up Font Lock]]:* Wrestling with boilerplate.\n    + *[[#defining-font-lock-keywords][Defining Font Lock Keywords]]:* Adding features.\n    + *[[#prettifiers-and-compose-region][Prettifiers and compose-region]]:* Implementing features explicitly.\n      - *[[#the-quintessential-prettifier---prettify-main-hbullet][The Quintessential Prettifier: --prettify-main-hbullet]]*\n      - *[[#more-prettifiers][More Prettifiers]]*\n    + *[[#custom-variables-interfacing-to-the-end-user][Custom Variables: Interfacing to the End User]]:* Adding Options.\n      - *[[#advanced-custom-functionality][Advanced Custom Functionality]]:* Creating decent menus for the Custom interface.\n    + *[[#hiding-and-the-invisibility-spec][Hiding and the Invisibility Spec]]:* Removing clutter from view.\n    + *[[#disabling-a-mode-cleaning-up][Disabling a Mode: Cleaning up]]:* Exiting a minor mode (gracefully).\n  * *[[#extending-the-minor-mode][Extending the Minor Mode]]:* Extending the bare-bones minor mode (an\n    example).\n    + *[[#beginning-with-a-vision][Beginning with a Vision]]:* Drafting new features.\n    + *[[#defining-a-new-keyword][Defining a New Keyword]]:* Locating new syntax elements.\n    + *[[#prettifiers-accessors-variables][Prettifiers, Accessors, Variables]]:* Implementing a new feature.\n    + *[[#faces][Faces]]:* Tweaking the look.\n    + *[[#cleaning-up][Cleaning up]]:* Disabling the new feature on exit.\n  * *[[#quick-reference][Quick Reference]]:* Where each symbol is first used in the guided tour.\n  * *[[#news][NEWS]]:* Updates of note.\n  * *[[#archive][Archive]]:* Yesterday's NEWS.\n\n* About\n  :PROPERTIES:\n  :CUSTOM_ID: about\n  :END:\n  With the success of [[https://github.com/integral-dw/org-superstar-mode][Org Superstar]] it is pretty clear that there is a demand\n  for eye candy in Outline-related modes.  A demand I won't be able to satisfy\n  on my own.  Superstar simply does not have the tools to generalize to other\n  modes, and trying to would only harm it.  For this reason, I decided to\n  provide something for the Emacs community that [[https://github.com/sabof][sabof]] (albeit unintentionally)\n  provided me with when I decided to create Org Superstar: a starting point.  I\n  tried my best to strip down Superstar to its bare essentials, which I will try\n  to explain both via the already existing documentation as well as brief\n  descriptions in this README.  By the end of it, you should have all the\n  facilities necessary to start working on your own eye-candy mode.  However,\n  this \"package\" (or rather, your copy of it) won't exist anymore by the time\n  you are done, because\\dots\n\n** This \"Package\" is NOT a Package\n   :PROPERTIES:\n   :CUSTOM_ID: this-package-is-not-a-package\n   :END:\n   What do I mean by this? Well, =superstar-kit.el= /looks/ like a package.  It\n   /behaves/ like a package, mostly, if loaded.  Tools like ~package-lint~,\n   ~checkdoc~, and ~flycheck~ have no complaints.  So why isn't it on MELPA?\n   *Because it would make no sense.* The most crucial elements that make a mode\n   like this work are /placeholders/.  The blanks still need to be filled in with\n   specifics of a mode.  The placeholders are documented in a handy [[file:CHECKLIST.org][checklist]]\n   (=CHECKLIST.org=), and many changes can be made semi-automatically. But not\n   all.  This is also by design, a design that in many ways is governed strongly\n   by what ~superstar-kit~ is *not*.\n\n   * It is NOT a dependency :: This is important. You don't ~require~ this file.\n     You take it, you edit it, you change the name.  It's really just a template\n     to get you started quickly.\n   * It is NOT an automated build tool / set of macros / code generator ::\n     Org Superstar, with all its features, is over 630 LOC (lines of code) and\n     growing.  It has legacy code.  It has compatibility code.  Bells and\n     whistles.  It has lots of special cases to integrate well with Org.\n     Superstar Kit at the time of writing has 243 LOC.  It also has none of\n     that.  By the time you are done adding the complexities and features for\n     /your/ use case the time and effort invested into /that/ will outweigh whatever\n     a more advanced starter kit could have conceivably saved you.  No point in\n     dwelling over starting off low-tech.\n   * It is NOT \"feature complete\" ::\n     Superstar Kit merely \"implements\" fancy headline bullets on its own.\n     That's basically it.  No item bullets, no syntax checks, nothing.  Instead,\n     it focuses on that one example, from which other features can be easily\n     inferred, to the point it may feel like a kill-yank-job.  It does however\n     do a couple of things with fancy headline bullets.  Enough to show what\n     this approach is roughly capable of.\n\n* A Guided Tour\n  :PROPERTIES:\n  :CUSTOM_ID: a-guided-tour\n  :END:\n  In this section I will explain /how/ a mode derived from this starter kit\n  actually works, so you can really make it your own.  I will only brush on\n  topics that are better explained elsewhere (such as how to define minor modes\n  and font lock details), and instead focus on the particularities of the core\n  mechanics inherited from Org Superstar and how to exploit them.  We will start\n  with the main protagonist of the file, the minor mode itself, and will descend\n  into greater detail as we begin to require it.  In other words, we will\n  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.\n\n  *Side Note:* I added some links to info nodes referring to the documentation of\n  certain Emacs internals, but they won't show up on GitHub, so opening the\n  README in Emacs has benefits.  Also, for those who just want to know where\n  they should start reading for a specific defined symbol, see the [[Quick Reference][quick reference]].\n\n** Defining a Minor Mode\n   :PROPERTIES:\n   :CUSTOM_ID: defining-a-minor-mode\n   :END:\n   Our end goal is making a minor mode for some Outline-like mode.  For that we\n   need to define one using ~define-minor-mode~.\n   #+begin_src elisp\n     ;;; Mode commands\n     ;;;###autoload\n     (define-minor-mode superstar-kit-mode\n       \"Use UTF8 bullets for headlines and plain lists.\"\n       nil nil nil\n       :group 'superstar-kit\n       :require 'M-PKG\n       ;; ...\n       )\n   #+end_src\n   Similar to a function definition, we begin with a function symbol to use both\n   interactively and in lisp.  No argument list is required, so the next entry\n   is the docstring.  The next three arguments (all nil) are of no particular\n   importance to us, as the mode we want to make is purely cosmetic and\n   consequently immensely unobtrusive.  Finally, there is the ~\u0026BODY~ of the\n   minor-mode, in which we will implement the necessary logic for our mode.  We\n   see two special keywords here: ~:group~ and ~:require~, with placeholder symbols.\n   The former associates the mode with a customization group (which allows the\n   user to manipulate things via the custom interface) and the latter\n   automatically requires the mode we are writing this minor mode for.\n   Currently, the file is full of placeholders, so before anything else we must\n   first replace them for our application of interest.  Suppose there is a bare\n   bones Outline-type of mode for simple note taking called ~grok-mode~, named\n   after Hubert Grokbold.  Hubert likes ~org-superstar~ and wants to make a\n   similar minor-mode called ~grok-bullets~ for his mode.  He consults the\n   =CHECKLIST= file and does everything up to the point where he is sent to the\n   =README=.  Casting the paradox of him encountering his own hypothetical story\n   aside, he would have already progressed quite far towards making his own\n   mode.  All instances of ~superstar-kit~ are replaced with ~grok-bullets~, among\n   other things.  His newly created minor mode now reads:\n   #+begin_src elisp\n     ;;;###autoload\n     (define-minor-mode grok-bullets-mode\n       \"Use UTF8 bullets for headlines and plain lists.\"\n       nil nil nil\n       :group 'grok-bullets\n       :require 'grok\n       ;; ...\n       )\n   #+end_src\n   It now auto-requires ~grok~ and also comes with its own custom group, which is\n   also already defined.  Finally, the ~;;;###autoload~ cookie helps Emacs to\n   defer having to load the package until it is actually needed.  Now, what\n   about the custom group itself? It's already almost fully predefined as well.\n   #+begin_src elisp\n     (defgroup grok-bullets nil\n       \"Use UTF8 bullets for headlines and plain lists.\"\n       ;; FIXME: Change this to the appropriate group of MODE\n       :group 'emacs)\n   #+end_src\n   The ~:group~ keyword here tells Emacs to put the entire group into a reasonable\n   super-group.  Hubert takes a quick glance at the checklist again and finds\n   he's supposed to change the group to a Grok-related group.  Luckily, ~grok~\n   defines a custom group of the same name, so replacing ~:group 'emacs~ with\n   ~:group 'grok~ is all it took.  Now a user can find the options of Grok Bullets\n   expectedly in the same category as those of Grok mode.\n\n   Next would be to set up the actual logic of the minor mode.  Instead of\n   directly having to work with the function argument of a minor mode, all we\n   have to do in the ~\u0026BODY~ is to check the value of the /variable/\n   ~grok-bullets-mode~.  This local variable is automatically generated.  If\n   non-nil, the body should execute whatever necessary to enable the mode.\n   Conversely, a value of nil tells the mode to clean up after itself and exit.\n\n** Setting up Font Lock\n   :PROPERTIES:\n   :CUSTOM_ID: setting-up-font-lock\n   :END:\n   Font Lock is the minor mode responsible for syntax highlighting in Emacs.  It\n   will handle most of the low-level manipulations in our buffer and will locate\n   our syntax elements (headlines) we want to prettify.  Naively, all we (or in\n   our case, Hubert) would hence need to do is pass a list of things for Font\n   Lock to do (conditionally), and tell Font Lock to stop highlighting these\n   things when the mode stops.  This of course implies that our major mode *uses\n   Font Lock* in the first place.\n   #+begin_src elisp\n     (define-minor-mode grok-bullets-mode\n       \"Use UTF8 bullets for headlines and plain lists.\"\n       nil nil nil\n       :group 'grok-bullets\n       :require 'grok\n       (cond\n        ;; Set up Grok Bullets.\n        (grok-bullets-mode\n         ;; ...\n         (font-lock-add-keywords nil grok-bullets--font-lock-keywords\n                                 'append)\n         ;; ...\n         )\n        ;; Clean up and exit.\n        (t\n         ;; ...\n         (font-lock-remove-keywords nil grok-bullets--font-lock-keywords)\n         ;; ...\n         ))\n   #+end_src\n   This tells Font Lock to add or remove instructions in the current buffer\n   stored in ~grok-bullets--font-lock-keywords~.  This would be fine if we didn't\n   want to be able to change and customize the keywords at runtime.  However,\n   since we generally want to do that we need a function to update the variable\n   based on the current configuration (~grok-bullets--update-font-lock-keywords~).\n   We also want to tell Font Lock to update the buffer once it receives new\n   instructions (~grok-bullets--fontify-buffer~, which we won't need to look at).\n   Hence setting up the mode is a little more involved.\n   #+begin_src elisp\n     ;; Set up Grok Bullets.\n     (grok-bullets-mode\n      (font-lock-remove-keywords nil grok-bullets--font-lock-keywords)\n      (grok-bullets--update-font-lock-keywords)\n      (font-lock-add-keywords nil grok-bullets--font-lock-keywords\n                              'append)\n      (grok-bullets--fontify-buffer)\n      ;; ...\n      )\n   #+end_src\n   The mode now cleans up whatever previous information we may have fed to Font\n   Lock, update the keywords and redraws the buffer.\n\n** Defining Font Lock Keywords\n   :PROPERTIES:\n   :CUSTOM_ID: defining-font-lock-keywords\n   :END:\n   [[info:elisp#Search-based Fontification][Font Lock keywords]] are simple lists which come in a variety of forms, fully\n   documented in a corresponding info node.  We will only use a small subset of\n   what keywords are capable of and restrict ourselves to the format\n   #+begin_src emacs-lisp\n     (REGEX . SUBEXP-HIGHLIGHTER)\n   #+end_src\n   meaning a cons of a [[info:Elisp#Regular Expressions][regular expression]] =REGEX= and a list =SUBEXP-HIGHLIGHTER=.\n   Each element of the latter is of the form\n   #+begin_src emacs-lisp\n     (SUBEXP FACESPEC [OVERRIDE [LAXMATCH]])\n   #+end_src\n   Where =SUBEXP= is an integer essentially corresponding to the number of a\n   numbered [[info:Elisp#Regexp Backslash][group]]^{}^{a)}, =FACESPEC= is an /expression/ whose value specifies the [[info:Elisp#Faces][face]] to\n   use (a symbol) and =OVERRIDE= and =LAXMATCH= are optional flags.  To reiterate:\n   =FACESPEC= is an /expression/ which will be evaluated every time =REGEX= is\n   matched.  *This is the core mechanism used by modes derived from this\n   template*.  =OVERRIDE= governs whether aspects of existing fontification can be\n   overridden.  A value of ~prepend~ works intuitively by merging properties of\n   the face with existing fontification, taking precedence.  Let us now look at\n   the code.\n   #+begin_src elisp\n     (defvar-local grok-bullets--font-lock-keywords nil)\n\n     (defun grok-bullets--update-font-lock-keywords ()\n       \"Set ‘grok-bullets--font-lock-keywords’ to reflect current settings.\n     You should not call this function to avoid confusing this mode’s\n     cleanup routines.\"\n       (setq grok-bullets--font-lock-keywords\n             ;; FIXME: Replace REGEXP to match your headlines.\n             `((\"^\\\\(?2:\\\\**?\\\\)\\\\(?1:\\\\*\\\\) \"\n                (1 (grok-bullets--prettify-main-hbullet) prepend)\n                ,@(unless grok-bullets-remove-leading-chars\n                    '((2 (grok-bullets--prettify-leading-hbullets)\n                         t)))\n                ,@(when grok-bullets-remove-leading-chars\n                    '((2 (grok-bullets--make-invisible 2))))))))\n   #+end_src\n   ~grok-bullets--font-lock-keywords~ is simply initialized as an empty list, and\n   properly generated by ~grok-bullets--update-font-lock-keywords~ on the fly.\n   Now, in the case of Grok, our imaginary mode, asterisks are no longer what\n   defines a headline, but tildes.  Hubert hence quickly fixes up the regular\n   expression and ticks another check box.\n   #+begin_src emacs-lisp\n     (defun grok-bullets--update-font-lock-keywords ()\n       \"Set ‘grok-bullets--font-lock-keywords’ to reflect current settings.\n     You should not call this function to avoid confusing this mode’s\n     cleanup routines.\"\n       (setq grok-bullets--font-lock-keywords\n             `((\"^\\\\(?2:~*?\\\\)\\\\(?1:~\\\\) \"\n                (1 (grok-bullets--prettify-main-hbullet) prepend)\n                ;; ...\n                ))))\n   #+end_src\n   The logic used for constructing this particular keyword is quite simple, but\n   can be easily extended.  By default, the custom variable\n   ~grok-bullets-remove-leading-chars~ allows every headline character but the\n   first to be removed (visually), which is not a significant loss of\n   information since the depth of the headline can be encoded in the choice of\n   face used combined with the bullet character.  Hence, two different functions\n   handle the possible ways in which leading characters are handled.\n   ~grok-bullets--make-invisible~ is a versatile function that can be recycled to\n   optionally hide away verbose syntax that rarely if ever needs manual editing.\n   ~grok-bullets--prettify-leading-hbullets~, much like\n   ~grok-bullets--prettify-main-hbullet~ serves a singular purpose of providing\n   the eye candy.\n\n   a) *Remark:* The value 0 is special in the sense that it corresponds to the\n   entire match of =REGEX=.\n\n** Prettifiers and ~compose-region~\n   :PROPERTIES:\n   :CUSTOM_ID: prettifiers-and-compose-region\n   :END:\n   A /prettifier/, in my nomenclature, is a function that visually modifies a\n   region from within Font Lock /beyond/ the [[info:Elisp#Faces][face]] properties.  Consequently,\n   prettifiers are the abstractions doing the actual heavy lifting through Font\n   Lock.  The name alludes to ~prettify-symbols-mode~, which this approach shares\n   a fair amount of conceptual DNA with.  The effect of displaying some\n   character (here: =~=) as some other character (a /bullet/) is achieved using a\n   function called ~compose-region~ which handles character composition (serving\n   as a thin wrapper for an internal C function).  For our purposes, it is a\n   function of three arguments ~(compose-region START END CHAR-OR-STRING)~,\n   displaying the region from =START= to =END= either as a single character or all\n   characters in a string superimposed.  The latter can be used to make\n   characters which are \"thinner\" than a monospaced character, which hence may\n   look out of place, effectively monospaced by superimposing it with a space\n   instead of using the literal character.  The downside to using ~compose-region~\n   this way is that superimposing characters can't be relied upon when Emacs is\n   used from a terminal.  This is why special care has to be taken when dealing\n   with terminal displays, as we will see later.\n\n*** The Quintessential Prettifier: ~--prettify-main-hbullet~\n    :PROPERTIES:\n    :CUSTOM_ID: the-quintessential-prettifier---prettify-main-hbullet\n    :END:\n    This is the most basic (and likely most iconic) prettifier.\n    #+begin_src emacs-lisp\n      (defun grok-bullets--prettify-main-hbullet ()\n        \"Prettify the trailing tilde in a headline.\"\n        (let ((level (grok-bullets--heading-level)))\n          (compose-region (match-beginning 1) (match-end 1)\n                          (grok-bullets--hbullet level)))\n        'grok-bullets-header-bullet)\n    #+end_src\n    Basically all of the actual complexity is tucked neatly away.\n    ~grok-bullets--heading-level~ and ~grok-bullets--hbullet~ compute which bullet\n    to use, the function implicitly assumes the target character is defined by\n    the last regex match (sub-expression *1*) and returns a customizable face\n    ~grok-bullets-header-bullet~.  The function ~grok-bullets--heading-level~ is\n    comparably trivial, since the level of an outline is essentially assumed to\n    be the number of heading characters.  Any other prettifier imaginable looks\n    similar to this. Take (parts of) the matched region, extract information\n    from it, compute the visual replacement, pass it to ~compose-region~, return a\n    face.  Everything past this point either calls Emacs internals directly and\n    is of no concern to us, or interfaces to options exposed to the user.  Hence\n    what remains is storing and accessing data.\n\n*** More Prettifiers\n    :PROPERTIES:\n    :CUSTOM_ID: more-prettifiers\n    :END:\n    To fully complete this section it is necessary to also look at the other\n    default prettifier provided by this package.  This one is a little more\n    involved, as leading characters have to be composed one by one,\n    necessitating a loop.\n    #+begin_src emacs-lisp\n      (defun grok-bullets--prettify-leading-hbullets ()\n        \"Prettify the leading bullets of a header line.\n      Each leading tilde is rendered as ‘grok-bullets-leading-bullet’\n      and inherits face properties from ‘grok-bullets-leading’.\n\n      If viewed from a terminal, ‘grok-bullets-leading-fallback’ is\n      used instead of the regular leading bullet to avoid errors.\"\n        (let ((star-beg (match-beginning 2))\n              (lead-end (match-end 2)))\n          (while (\u003c star-beg lead-end)\n            (compose-region star-beg (setq star-beg (1+ star-beg))\n                            (grok-bullets--lbullet)))\n          'grok-bullets-leading))\n    #+end_src\n    We also see that the documentation already fully explains how this function\n    interacts with user-level variables.  For each kind of data accessed there\n    is a corresponding accessor, in this case ~grok-bullets--lbullet~, and for\n    every kind of prettifier there is a face, in this case ~grok-bullets-leading~.\n\n** Custom Variables: Interfacing to the End User\n   :PROPERTIES:\n   :CUSTOM_ID: custom-variables-interfacing-to-the-end-user\n   :END:\n   While prettifiers handle putting pretty symbols on the screen, we still\n   require data to hold them (and functions to access them).  I also like to\n   define a nice custom interface, which also comes with the benefit of\n   declaring [[info:Elisp#Customization Types][valid types]].  If you are interested in supporting customization, I\n   recommend the corresponding [[info:Elisp#Customization][manual section]].  The data structure to hold\n   bullet chars for each heading level is a simple list.  Each element\n   corresponds to the bullet to use for the corresponding level (starting from\n   zero).\n   #+begin_src elisp\n     (defcustom grok-bullets-headline-bullets-list\n       '(?◉ ?○ ?🞛 ?▷)\n       ;; long docstring\n       :group 'grok-bullets\n       :type ;; long customization type declaration\n       )\n   #+end_src\n   It can either hold characters or a simple list with a string handed to\n   ~compose-region~ as the first element and a fallback character for terminals as\n   the second.  Writing a function that accesses such a list and distinguishes\n   the two cases is pretty straightforward.\n   #+begin_src elisp\n     (defun grok-bullets--nth-headline-bullet (n)\n       \"Return the Nth specified headline bullet or its corresponding fallback.\n     N counts from zero.  Headline bullets are specified in\n     ‘grok-bullets-headline-bullets-list’.\"\n       (let ((bullet-entry\n              (elt grok-bullets-headline-bullets-list n)))\n         (cond\n          ((characterp bullet-entry)\n           bullet-entry)\n          ((display-graphic-p)\n           (elt bullet-entry 0))\n          (t\n           (elt bullet-entry 1)))))\n   #+end_src\n   However, this function on its own would be useless to a prettifier, as trying\n   to obtain bullets for levels greater than those specified would eventually\n   raise an error.  To give the user some agency over how to extrapolate from\n   the given number of bullets, another custom variable is defined.\n   #+begin_src elisp\n     (defcustom grok-bullets-cycle-headline-bullets t\n       \"Non-nil means cycle through available headline bullets.\n\n     The following values are meaningful:\n\n     An integer value of N cycles through the first N entries of the\n     list instead of the whole list.\n\n     If otherwise non-nil, cycle through the entirety of the list.\n\n     If nil, repeat the final list entry for all successive levels.\n\n     You should call ‘grok-bullets-restart’ after changing this\n     variable for your changes to take effect.\"\n       ;; more custom interface boilerplate\n       )\n   #+end_src\n   This gives the user plenty of options to fine tune the mode's behavior to\n   their liking.  All that is left to do is actually implement the accessor\n   function that obtains the correct bullet for the prettifier.\n   #+begin_src elisp\n     (defun grok-bullets--hbullets-length ()\n       \"Return the length of ‘grok-bullets-headline-bullets-list’.\"\n       (length grok-bullets-headline-bullets-list))\n\n     (defun grok-bullets--hbullet (level)\n       \"Return the desired headline bullet replacement for LEVEL N.\n\n     For more information on how to customize headline bullets, see\n     ‘grok-bullets-headline-bullets-list’.\n\n     See also ‘grok-bullets-cycle-headline-bullets’.\"\n       (let ((max-bullets grok-bullets-cycle-headline-bullets)\n             (n  (1- level)))\n         (cond ((integerp max-bullets)\n                (grok-bullets--nth-headline-bullet (% n max-bullets)))\n               (max-bullets\n                (grok-bullets--nth-headline-bullet\n                 (% n (grok-bullets--hbullets-length))))\n               (t\n                (grok-bullets--nth-headline-bullet\n                 (min n (1- (grok-bullets--hbullets-length))))))))\n   #+end_src\n   Since leading bullets do not change with the level (functioning more as\n   [[https://en.wikipedia.org/wiki/Leader_(typography)][leaders]]), their custom variables and accessors are rather straightforward.\n   #+begin_src elisp\n     (defcustom grok-bullets-leading-bullet ?.\n       ;; docstring and custom boilerplate\n       )\n\n     (defcustom grok-bullets-leading-fallback\n       (cond ((characterp grok-bullets-leading-bullet)\n              grok-bullets-leading-bullet)\n             (t ?.))\n       ;; again\n       )\n\n     ;; some other code\n\n     (defun grok-bullets--lbullet ()\n       \"Return the correct leading bullet for the current display.\"\n       (if (display-graphic-p)\n           grok-bullets-leading-bullet\n         grok-bullets-leading-fallback))\n   #+end_src\n   A particularly noteworthy trick here is how the fallback option defaults to\n   the regular bullet if there is no need for a fallback (that is, if the main\n   bullet is a character and works on terminals).\n\n*** Advanced Custom Functionality\n    :PROPERTIES:\n    :CUSTOM_ID: advanced-custom-functionality\n    :END:\n   The custom interface allows us to do more than just specify a type for a\n   given variable.  We can even define specialized setter functions and raise\n   errors depending on user input.  We can for example mirror the load-up\n   behavior of ~grok-bullets-leading-bullet~ (also setting the fallback when it is\n   a character) in the custom interface by defining a function of the below form\n   and passing it to the variable's ~defcustom~ using the ~:set~ keyword.\n   #+begin_src elisp\n     (defun grok-bullets--set-lbullet (symbol value)\n       \"Set SYMBOL ‘grok-bullets-leading-bullet’ to VALUE.\n     If set to a character, also set ‘grok-bullets-leading-fallback’.\"\n       (set-default symbol value)\n       (when (characterp value)\n         (set-default 'grok-bullets-leading-fallback value)))\n   #+end_src\n   Validating a customized value works similarly using the ~:validate~ [[info:Elisp#Type Keywords][keyword]] in\n   a given customization type.  Here, we ensure that the number of bullets to\n   cycle through does not exceed the actual number of bullet items.  The way we\n   have to communicate errors to custom is a little unusual, as it involves\n   handing the error information to the responsible widget and returning it.\n   Widgets on their own can fill an entire manual (in fact, [[info:Widget][they do]]), but all we\n   need to know here is that they are the buttons, text fields and check boxes\n   we interact with in the custom interface, and that we can manipulate them\n   with various functions through lisp.  A validation function receives the\n   widget as its argument.  We can \"unpack\" the user-set value with ~widget-value~\n   and override it with a valid input using ~widget-value-set~, should the user\n   input be incorrect.  Finally, we can pass an error message to the widget\n   using ~(widget-put WIDGET :error ERROR-MESSAGE-STRING)~.  We should only\n   manipulate the widget if the user input is erroneous, and return nil if it\n   isn't.  With this knowledge we can write perfectly fine validation functions\n   such as the one the template already defines.\n   #+begin_src elisp\n     (defun grok-bullets--validate-hcycle (text-field)\n       \"Raise an error if TEXT-FIELD’s value is an invalid hbullet number.\n     This function is used for ‘grok-bullets-cycle-headline-bullets’.\n     If the integer exceeds the length of\n     ‘grok-bullets-headline-bullets-list’, set it to the length and\n     raise an error.\"\n       (let ((ncycle (widget-value text-field))\n             (maxcycle (grok-bullets--hbullets-length)))\n         (unless (\u003c= 1 ncycle maxcycle)\n           (widget-put\n            text-field\n            :error (format \"Value must be between 1 and %i\"\n                           maxcycle))\n           (widget-value-set text-field maxcycle)\n           text-field)))\n   #+end_src\n\n** Hiding and the Invisibility Spec\n   :PROPERTIES:\n   :CUSTOM_ID: hiding-and-the-invisibility-spec\n   :END:\n   With prettifiers and their internals and interfaces out of the way, there is\n   only one more aspect to the Font Lock code that has not been looked at in\n   greater detail.\n   #+begin_src elisp\n     (defun grok-bullets--update-font-lock-keywords ()\n       ;; docstring\n       (setq grok-bullets--font-lock-keywords\n             `((\"^\\\\(?2:~*?\\\\)\\\\(?1:~\\\\) \"\n                ;; ... (we already covered this part)\n                ,@(when grok-bullets-remove-leading-chars\n                    '((2 (grok-bullets--make-invisible 2))))))))\n   #+end_src\n   Making text in a buffer [[info:Elisp#Invisible Text][invisible]] is another lower-level feature of Emacs.\n   It does exactly what it sounds like, and requires nothing beyond adding a\n   simple [[info:Elisp#Text Properties][text property]] to the region in question.  What essentially happens in\n   the background is that Emacs stores a small bit of metadata (the symbol\n   ~grok-bullets-hide~) in the buffer region.  That symbol needs to be added to\n   the so-called \"invisibility spec\" to function correctly, necessitating one\n   more line of boilerplate in our mode setup.\n   #+begin_src elisp\n     (define-minor-mode grok-bullets-mode\n       ;; etc.\n       (cond\n        ;; Set up Grok Bullets.\n        (grok-bullets-mode\n         ;; ... (as before)\n         (add-to-invisibility-spec '(grok-bullets-hide)))\n        ;; ...\n        ))\n\n   #+end_src\n   Implementing support for making the leading characters invisible then turns\n   out to be rather straightforward.\n   #+begin_src elisp\n     (defcustom grok-bullets-remove-leading-chars nil\n       ;; docstring\n       :group 'grok-bullets\n       :type 'boolean)\n\n     ;; some code\n\n     (defun grok-bullets--make-invisible (subexp)\n       \"Make part of the text matched by the last search invisible.\n     SUBEXP, a number, specifies which parenthesized expression in the\n     last regexp.  If there is no SUBEXPth pair, do nothing.\"\n       (let ((start (match-beginning subexp))\n             (end (match-end subexp)))\n         (when start\n           (add-text-properties\n            start end '(invisible grok-bullets-hide)))))\n   #+end_src\n   This completes all features available to the basic mode.  All that remains is\n   some cleanup should the mode be disabled or restarted.\n\n** Disabling a Mode: Cleaning up\n   :PROPERTIES:\n   :CUSTOM_ID: disabling-a-mode-cleaning-up\n   :END:\n   Now that the worst part of defining the mode is over, all that is left are\n   cleanup functions.  First, the mode itself needs to handle the case of\n   (~grok-bullets-mode~) being nil.\n   #+begin_src elisp\n     (define-minor-mode grok-bullets-mode\n       \"Use UTF8 bullets for headlines and plain lists.\"\n       nil nil nil\n       :group 'grok-bullets\n       :require 'grok\n       (cond\n        ;; ...\n        ;; Clean up and exit.\n        (t\n         (remove-from-invisibility-spec '(grok-bullets-hide))\n         (font-lock-remove-keywords nil grok-bullets--font-lock-keywords)\n         (grok-bullets--unprettify-hbullets)\n         (grok-bullets--fontify-buffer))))\n   #+end_src\n   Apart from cleaning up the invisibility spec and Font Lock keywords all that\n   is left is undoing the work of the prettifiers with a corresponding\n   /unprettifier/.\n   #+begin_src elisp\n     (defun grok-bullets--unprettify-hbullets ()\n       \"Revert visual tweaks made to header bullets in current buffer.\"\n       (save-excursion\n         (goto-char (point-min))\n         ;; FIXME: Replace REGEXP to match your headlines.\n         (while (re-search-forward \"^\\\\*+ \" nil t)\n           (decompose-region (match-beginning 0) (match-end 0)))))\n   #+end_src\n   Unlike the prettifiers, which operate only on one match in the file, an\n   unprettifier traverses the entire file.  Undoing composing is done by the\n   aptly-named ~decompose-region~.  This is also the last part we have edit\n   manually for the mode to work.  We could use the same regex we used for the\n   Font Lock keyword, but since we don't need groups we get away just using\n   ~(re-search-forward \"^~+ \" nil t)~.\n\n* Extending the Minor Mode\n  :PROPERTIES:\n  :CUSTOM_ID: extending-the-minor-mode\n  :END:\n  After consulting the =CHECKLIST= file your minor mode should already work\n  decently and compile without warning.  However, the mode is rather bare bones,\n  which is why I want to give a minor example for how to implement a new\n  feature.  For this reason, we will now take a look at our hypothetical Hubert\n  Grokbold implementing a new feature for his ~grok-bullets~ mode.\n** Beginning with a Vision\n   :PROPERTIES:\n   :CUSTOM_ID: beginning-with-a-vision\n   :END:\n   Suppose Grok mode supports a fancy type of text block, called grok blocks.\n   Each line of a grok block begins with an integer enclosed in square brackets,\n   followed by a =\u003e=, like this:\n   #+begin_src fundamental\n     [0]\u003e Quote of the day: \"Stay hydrated, this is a threat.\"\n     [1]\u003e Buy eggs, milk, cereal, flour, toothpaste,\n     [1]\u003e 4 chicken thighs, 500g breast, celery.\n     [2]\u003e Remember to look up the tampon brand in the bathroom.\n     [3]\u003e Dentist appointment next week =\u003e calendar!\n     [1]\u003e Also, remember to take the trash out.\n   #+end_src\n   Possibly, the integers represent the importance of the note.  Hubert wants to\n   prettify grok blocks.  He imagines the following:\n   * Instead of =[1]=, he would like a symbol depending on the integer.\n   * Instead of =\u003e=, he would like some other character.\n   * A face for both.\n   * He wants to highlight important lines and de-emphasize unimportant ones.\n\n** Defining a New Keyword\n   :PROPERTIES:\n   :CUSTOM_ID: defining-a-new-keyword\n   :END:\n   How does one accomplish that?  It becomes clear that three components need to\n   be distinguished, =[1]=, =\u003e=, and the rest of the line.\n   #+begin_src elisp\n     (defun grok-bullets--update-font-lock-keywords ()\n       \"Set ‘grok-bullets--font-lock-keywords’ to reflect current settings.\n     You should not call this function to avoid confusing this mode’s\n     cleanup routines.\"\n       (setq grok-bullets--font-lock-keywords\n             `((\"^\\\\(?2:~*?\\\\)\\\\(?1:~\\\\) \"\n                (1 (grok-bullets--prettify-main-hbullet) prepend)\n                ,@(unless grok-bullets-remove-leading-chars\n                    '((2 (grok-bullets--prettify-leading-hbullets)\n                         t)))\n                ,@(when grok-bullets-remove-leading-chars\n                    '((2 (grok-bullets--make-invisible 2)))))\n               (\"^\\\\(?1:\\\\[[0-9]+\\\\]\\\\)\\\\(?2:\u003e\\\\)\\\\(?3: .*\\\\)$\"\n                (1 (grok-bullets--prettify-gb-priority))\n                (2 (grok-bullets--prettify-gb-delim))\n                (3 (grok-bullets--gb-face))))))\n   #+end_src\n** Prettifiers, Accessors, Variables\n   :PROPERTIES:\n   :CUSTOM_ID: prettifiers-accessors-variables\n   :END:\n    Hubert requires two prettifiers and one function that simply obtains the\n    face for the remaining line.  Since everything is already nicely packaged\n    away into neat groups, working on them is comparably easy.\n    #+begin_src elisp\n      (defun grok-bullets--prettify-gb-priority ()\n        \"Prettify the priority of a Grok block line.\"\n        (let ((priority (grok-bullets--priority)))\n          (compose-region (match-beginning 1) (match-end 1)\n                          (grok-bullets--gb-icon priority)))\n        'grok-bullets-priority-icon)\n    #+end_src\n    What remains to do for this prettifier are defining the function to compute\n    the priority, an accessor function obtaining the correct icon and a face.\n    Hubert looks at how bullets are stored in his mode and copies the approach.\n    However, it makes no sense to be able to cycle through icons for higher\n    priorities, so the last one just repeats.\n    #+begin_src elisp\n      (defcustom grok-bullets-priority-icons\n        '((\"　\" ?\\s) (\"　○\" ?○) (\"　❔\" ??) (\"　❗\" ?!))\n        \"List of icons used in Grok blocks.\n      It can contain any number of icons, the Nth entry usually\n      corresponding to the icon used for priority N.\n\n      Every entry in this list can either be a character or a list.\n      Characters are used as simple, verbatim replacements of the\n      headline character for every display (be it graphical or\n      terminal).  If the list element is a list, it should be of the\n      general form\n      \\(COMPOSE-STRING CHARACTER)\n\n      where COMPOSE-STRING should be a string according to the rules of\n      the third argument of ‘compose-region’.  It will be used to\n      compose the specific priority icon.  CHARACTER is the fallback\n      character used in terminal displays, where composing characters\n      cannot be relied upon.\n\n      You should re-enable Grok Bullets after changing this variable\n      for your changes to take effect.\"\n        :group 'grok-bullets\n        :type '(repeat (choice\n                        (character :value ?!\n                                   :format \"Icon: %v\\n\"\n                                   :tag \"Simple icon\")\n                        (list :tag \"Advanced string and fallback\"\n                              (string :value \"!\"\n                                      :format \"String of characters to compose: %v\")\n                              (character :value ?!\n                                         :format \"Fallback character for terminal: %v\\n\")))))\n    #+end_src\n    Next would be the function accessing the priority information, which simply\n    has to strip the surrounding brackets and turn the string to an integer, and\n    the function to access the custom variable.\n    #+begin_src elisp\n      (defun grok-bullets--priority ()\n        \"Return the priority of the Grok block line.\"\n        (let ((token (match-string 1)))\n          (string-to-number\n           (substring token 1 (1- (length token))))))\n\n      (defun grok-bullets--gb-icon (priority)\n        \"Obtain Grok block icon for the given PRIORITY.\n\n      If PRIORITY is greater than the number of icons specified in\n      ‘grok-bullets-priority-icons’, return the highest priority\n      icon.\"\n        (let* ((priority (min priority\n                              (1- (length grok-bullets-priority-icons))))\n               (entry (elt grok-bullets-priority-icons priority)))\n          (cond\n           ((characterp entry)\n            entry)\n           ((display-graphic-p)\n            (elt entry 0))\n           (t\n            (elt entry 1)))))\n    #+end_src\n    Prettifying the delimiter is trivial in comparison.\n    #+begin_src elisp\n      (defcustom grok-bullets-gb-delimiter ?»\n        \"Character to delimit Grok block lines.\n      This variable is a character replacing the default greater-than\n      in terminal displays instead of ‘grok-bullets-leading-bullet’.\n\n      You should re-enable Grok Bullets after changing this\n      variable for your changes to take effect.\"\n        :group 'grok-bullets\n        :type '(character :tag \"Character to display\"\n                          :format \"\\n%t: %v\\n\"\n                          :value ?\u003e))\n\n      ;; ...\n\n      (defun grok-bullets--prettify-gb-delim ()\n        \"Prettify the delimiter of a Grok block line.\"\n        (compose-region (match-beginning 2) (match-end 2)\n                        grok-bullets-gb-delimiter)\n        'grok-bullets-priority-icon)\n    #+end_src\n** Faces\n   :PROPERTIES:\n   :CUSTOM_ID: faces\n   :END:\n   Defining simple faces is comparably straightforward, although it is best to\n   still read up on it, both the [[info:Elisp#Faces][info node]] as well as the documentation of\n   ~defface~ could prove useful here.  Hubert believes that the best default is a\n   subtle default, so he just inherits the default face.\n   #+begin_src elisp\n     (defface grok-bullets-priority-icon\n       '((default . (:inherit default)))\n       \"Face used to display prettified Grok block icons.\"\n       :group 'grok-bullets)\n   #+end_src\n   For the final necessary element (a function providing priority-dependent\n   faces) Hubert wants to try something more extravagant.  Instead of creating a\n   fixed number of faces and potentially providing the user with some flags to\n   modify the mode's behavior he decides to mirror the way bullets are stored.\n   This is possible because faces don't /have/ to be symbols.  Instead, property\n   lists can be used.  These /anonymous faces/ can be stored in a list.  The face\n   function is then consequently straightforward.\n   #+begin_src elisp\n     (defcustom grok-bullets-priority-faces\n       '((:foreground \"gray70\" :slant italic)\n         default\n         (:weight bold)\n         (:weight bold :foreground \"red3\"))\n       \"Faces to use for Grok block lines of a given priority.\n\n     Should a Grok block line have a higher priority than the highest\n     specified by this variable, the highest available is used.\"\n       :group 'grok-bullets\n       :type '(repeat\n               (choice :tag \"Face spec\"\n                       (face :value default)\n                       (plist :key-type (symbol :tag \"Property\")\n                              :tag \"Face properties\"))))\n     ;; ...\n\n     (defun grok-bullets--gb-face ()\n       \"Return the appropriate face to use for the given priority.\"\n       (let* ((priority (grok-bullets--priority))\n              (facespec (elt grok-bullets-priority-faces\n                             priority)))\n         (or facespec\n             (last grok-bullets-priority-faces))))\n   #+end_src\n\n** Cleaning up\n   :PROPERTIES:\n   :CUSTOM_ID: cleaning-up\n   :END:\n   For each new set of /prettifiers/ there needs to be a corresponding\n   /unprettifier/ in case the user wants to disable your mode.  Consequently,\n   Hubert needs to implement an unprettifier for Grok blocks to have the mode\n   exit cleanly (as it should).\n   #+begin_src elisp\n     (defun grok-bullets--unprettify-gb ()\n       \"Revert visual tweaks made to grok blocks in current buffer.\"\n       (save-excursion\n         (goto-char (point-min))\n         (while (re-search-forward \"^\\\\[[0-9]+\\\\]\u003e \" nil t)\n           (decompose-region (match-beginning 0) (match-end 0)))))\n\n     ;; ...\n\n     (define-minor-mode grok-bullets-mode\n       ;; ... (nothing new)\n       (cond\n        ;; Set up Grok Bullets.\n        (grok-bullets-mode\n         ;; ...\n         )\n        ;; Clean up and exit.\n        (t\n         (remove-from-invisibility-spec '(grok-bullets-hide))\n         (font-lock-remove-keywords nil grok-bullets--font-lock-keywords)\n         (grok-bullets--unprettify-hbullets)\n         (grok-bullets--unprettify-gb)\n         (grok-bullets--fontify-buffer))))\n   #+end_src\n   With this, the mode is finally complete again and ready for shipping (after\n   some thorough testing, of course).\n* Quick Reference\n  :PROPERTIES:\n  :CUSTOM_ID: quick-reference\n  :END:\n  For the impatient, here is a list of all symbols with their original names, in\n  order of appearance in the [[A Guided Tour][guided tour]] above.  Implementation of functions is\n  often addressed later in dedicated sections, with the first mention usually\n  showing where it is utilized instead.\n\n  * Defining a Minor Mode ::\n    + ~superstar-kit-mode~ (minor mode)\n    + ~superstar-kit~ (group)\n  * Setting up Font Lock ::\n    + ~superstar-kit--update-font-lock-keywords~ (private function)\n    + ~superstar-kit--font-lock-keywords~ (private buffer local variable)\n    + ~superstar-kit--fontify-buffer~ (private function)\n  * Defining Font Lock Keywords ::\n    + ~superstar-kit-remove-leading-chars~ (custom variable)\n    + ~superstar-kit--prettify-main-hbullet~ (private function)\n    + ~superstar-kit--prettify-leading-hbullets~ (private function)\n    + ~superstar-kit--make-invisible~ (private function)\n    + The Quintessential Prettifier: ~--prettify-main-hbullet~ ::\n      - ~superstar-kit--heading-level~ (private function)\n      - ~superstar-kit-header-bullet~ (face)\n    + More Prettifiers ::\n      - ~superstar-kit-leading~ (face)\n      - ~superstar-kit-leading-bullet~ (custom variable)\n      - ~superstar-kit-leading-fallback~ (custom variable)\n      - ~superstar-kit--lbullet~ (private function)\n  * Custom Variables: Interfacing to the End User ::\n    + ~superstar-kit-headline-bullets-list~ (custom variable)\n    + ~superstar-kit-cycle-headline-bullets~ (custom variable)\n    + ~superstar-kit--nth-headline-bullet~ (private function)\n    + ~superstar-kit--hbullets-length~ (private function)\n    + ~superstar-kit--hbullet~ (private function)\n    + Advanced Custom Functionality ::\n      - ~superstar-kit--set-lbullet~ (private function)\n      - ~superstar-kit--validate-hcycle~ (private function)\n  * Hiding and the Invisibility Spec ::\n    + ~grok-bullets-hide~ (symbol)\n  * Disabling a Mode: Cleaning up ::\n    + ~superstar-kit--unprettify-hbullets~ (private function)\n    + ~superstar-kit-restart~ (interactive function)\n\n* NEWS\n  :PROPERTIES:\n  :CUSTOM_ID: news\n  :END:\n\n* Archive\n  :PROPERTIES:\n  :CUSTOM_ID: archive\n  :END:\n\n  #  LocalWords:  Grokbold fontification prettifiers prettifier accessors cdr\n  #  LocalWords:  accessor unprettifier\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fintegral-dw%2Fsuperstar-kit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fintegral-dw%2Fsuperstar-kit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fintegral-dw%2Fsuperstar-kit/lists"}