My Emacs Configuration

Table of Contents

I use my Emacs configuration both as a means to customize it and to document standard behavior. As a result much of it will strictly unnecessary.

I'll also be looking to build up the configuration incrementally along with supporting knowledge and will start with integrating desired functionality directly into this file rather than pulling in external packages. This ties in with an overall goal I have in software in general to make consistent reuse of abstractions, and also can hopefully address a concern I've had with Emacs in particular in the past where it felt as though there was both too much code floating around the ecosystem and too little readily composable functionality.

I'm currently using ^- as a prefix for my local definitions. This is inherited from a past project where I started to create a lot of macros and since the "macs" in Emacs is for macros I was thinking of it as macros squared…so ^- is in the spirit of that while also being nice and short.

Over time I'll likely also look to evolve the configuration so that it is increasingly declarative data structures operated upon by fewer functions. This may ease reuse of the same configuration across multiple targets as the number of operations to support will be reduced.

1. Define Header

Define some standard header boilerplate like copyright… most significantly define lexical binding.

;;; init.el -- Configure emacs to my liking. -*- lexical-binding: t; -*-

;; Copyright (C) 2023 Matt Whipple <mattwhipple@acm.org>

;; Author: Matt Whipple <mattwhipple@acm.org>
;; Maintainer: Matt Whiple <mattwhipple@acm.org>
;; Keywords: config
;; Package: ^-config
;; Package-Version: NA
;; Package-Requires: ((emacs "29.1") (dash "2.19.1")
;; Filename: init.el

;; This file is not part of GNU Emacs.

;; This is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This software is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this file.  If not, see <http://www.gnu.org/licenses/>.

;;; Commentary:

;; This file will collect my configuration and documentation of emacs behavior,
;; along with being the initial location of any extensions on which I work.

;;; Code:

2. Check Preconditions

I'll be making use of whatever seems readily available rather than carefully tracking what features are available on what systems, but as the lack of certain capabilities translate into errors on some system or other I'll add tests with more helpful messages here. If any of this code ends up being split out into libraries this should be handled with more discipline (and compatibility options should be explored).

2.1. JSON Support

This is required for EAF stuff and seems generally useful.

(unless (fboundp #'json-parse-buffer)
  (warn "Emacs does not have JSON support!"))

3. Configure Package Management

I'm currently ideally being cautious about pulling in packages due to the more ambitious goal of building out a more unified platform, but some packages will be embraced and others will be used out of pragmatism (particularly for professional work).

Here I'll configure some of the standard repositories.

(require 'package)
(setq package-archives '(("gnu" . "https://elpa.gnu.org/packages/")
                         ("melpa" . "https://melpa.org/packages/")))

4. Enable Desktop Preservation

Use desktop mode to save and restore desktops across invocations.

(desktop-save-mode)

5. Customize Styling

5.1. Show Some Stuff

I often have Emacs running full-screen so it's nice if it just provides me info that I need in the status bar such as what time it is and how much time until my battery will die and maybe other things that aren't time related.

(display-battery-mode)
(display-time-mode)
(setq display-time-day-and-date t)

5.2. Hide Some Stuff

Currently I'm very keyboard oriented and don't want to waste previous screen real estate so I'd rather get rid of user interface elements intended for other forms of interaction. I may revisit this as I look at using Emacs across a wider range of devices.

(tool-bar-mode -1)
(menu-bar-mode -1)

5.3. Go Dark

I (like many people) generally prefer themes with dark backgrounds. Emacs in particular looks particularly primitive with the default white GUI background whereas on black it looks at least a bit more retro. While I may be tempted at some point to fiddle with this a bit more (especially given that it comes with some disclaimers) I'll start with some code I stumbled upon within the Emacs code base (I think frame.el…I should link it when I come across it again).

(define-minor-mode dark-mode
  "Use light text on dark background."  
  :global t
  :group 'faces
  (when (eq dark-mode
            (eq 'light (frame--current-background-mode (selected-frame))))
    ;; FIXME: Change the face's SPEC instead?
    (set-face-attribute 'default nil
                        :foreground (face-attribute 'default :background)
                        :background (face-attribute 'default :foreground))
   (frame-set-background-mode (selected-frame))))
(dark-mode)

5.4. Tweak Mode Line

I don't remember what this setting does specifically, but I had it sitting around in an old configuration and figured it was worth pulling in to figure out later.

(setq-default
  mode-line-format
  (list "%b L%l %p " mode-line-misc-info))

6. Define Self Inserting Characters

Define and bind chose keys that result in the associated character being inserted. This is particularly standard behavior as provided by the terminal though Emacs does add some custom logic. These are defined in a variable for subsequent specialization (there is likely a more idiomatic way to do that such as using remap but I haven't gotten that far yet.

This makes use of the relatively new keymap-set function which replaces define-key. Elsewhere will primarily use the `bind-key` family of macros but this make more direct use of the core functionality as it lends itself more readily to composition (and there's no clear value in using anything fancier). I'd generally be on the fence about using bind-key but since it's now included in Emacs it's hard to resist. I'm fairly likely to switch that out later in favor of something simpler like define-keymap as I build out my Emacs knowledge.

(setq self-insert-keys
      '(
        "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l" "m"			   
        "A" "B" "C" "D" "E" "F" "G" "H" "I" "J" "K" "L" "M"
        "n" "o" "p" "q" "r" "s" "t" "u" "v" "w" "x" "y" "z"
        "N" "O" "P" "Q" "R" "S" "T" "U" "V" "W" "X" "Y" "Z"
        "0" "1" "2" "3" "4" "5" "6" "7" "8" "9" "." "-" "$"
        "SPC" "=" "_" "*" "\"" "'" "`" "+" "~" "?"
        "!" "@" "#" "%" "^" "&" "|" "," ";" ":"
        "(" ")" "{" "}" "[" "]" "<" ">" "/" "\\"
        ))
(dolist (k self-insert-keys)
  (apply #'keymap-set (list global-map k #'self-insert-command)))

7. Bind Some Low-Level Commands

Define some functions that seem relatively fundamental to dealing with commands in general.

(bind-keys :map global-map
           ("C-g" . keyboard-quit)
           ("M-x" . execute-extended-command)
           ("M-:" . eval-expression))

8. Provide Some Basic Editing

8.1. Insert Content

(bind-keys :map global-map
           ("C-q" . quoted-insert)
           ("RET" . newline)
           ("C-j" . electric-newline-and-maybe-indent)
           ("TAB" . indent-for-tab-command)
           ("C-o" . open-line))

8.2. Remove Content

(bind-keys :map global-map
           ("DEL"  . backward-delete-char-untabify)
           ("M-DEL" . backward-kill-word)
           ;; Chrome OS support
           ("<deletechar>" . backward-kill-word)
           ("C-d" .  delete-char)
           ("C-w" . kill-region)
           ("M-d" . kill-word)
           ("C-k" . kill-line)
           ("M-\\" . delete-horizontal-space)
           ("M-z" . zap-up-to-char))

9. Support Moving Point Around Within a Buffer

(bind-keys :map global-map
           ("C-f" . forward-char)
           ("C-b" . backward-char)
           ("C-n" . next-line)
           ("C-p" . previous-line)
           ("C-e" . move-end-of-line)
           ("C-a" . move-beginning-of-line)
           ("M-f" . forward-word)
           ("M-b" . backward-word)
           ("M-n" . forward-list)
           ("M-p" . backward-list)
           ("M->" . end-of-buffer)
           ("M-<" . beginning-of-buffer)
           ("M-}" . forward-paragraph)
           ("M-{" . backward-paragraph)
           ("C-v" . scroll-up-command)
           ("M-v" . scroll-down-command))

10. Change View of Buffer

(bind-keys :map global-map
           ("C-l" . recenter-top-bottom))

11. Play With Mark

(bind-keys :map global-map
           ("C-SPC" . set-mark-command)
           ("C-x h" . mark-whole-buffer))

12. Work with Kill Ring

(bind-keys :map global-map
           ("C-y" . yank)
           ("M-y" . yank-pop)
           ("M-w" . kill-ring-save))

13. Provide Some Contextual Editing

While much editing is fairly indifferent to the underlying content, some may vary behavior slightly based on its context. This does not extend to include more sophisticated behaviors that require deeper knowledge of syntactical elements.

(bind-keys :map global-map
           ("M-u" . upcase-word)
           ("M-;" . comment-dwim))

14. Generate Content

(bind-keys :map global-map
           ("M-/" . dabbrev-expand)
           ("M-SPC" . complete-symbol))

15. Searching and Potentially Replacing

(bind-keys :map global-map
           ("C-s" . isearch-forward)
           ("C-r" . isearch-backward)
           ("M-%" . query-replace)
           ("M-." . xref-find-definitions))

16. Support Some Inter-Process Communication

16.1. Support Invoking of External Commands

(bind-keys :map global-map
           ("M-!" . shell-command))

17. Provide Some Global Dispatching

(bind-keys :prefix "C-x C-z"
           :prefix-map dispatch-map
           ("a" . org-agenda)
           ("g" . gnus)
           ("m" . gnus-summary-mail-other-window)
           ("s" . shell)
           ("v" . view-mode)
           ("w" . eww))

17.1. Handle Escape Sequences from External Commands

(add-hook 'compilation-filter-hook 'ansi-color-compilation-filter)

18. Manage View Objects

Deal with buffers and windows. I'm currently lumping these together and customizing their binding a bit to make use of the `C-M` modifier combination. This is an experimental shift, but in a past adoption of Emacs I remember doing something or other to rebind window resizing such that it did not require a chain and as these operations are so fundamental this seems like a potentially nice route.

These are global and since the prefix is likely used elsewhere they are forced using `bind-keys*` to stick them in the override map.

(bind-keys* ("C-M-n" . next-buffer)
            ("C-M-<right>" . next-buffer)
            ("C-M-p" . previous-buffer)
            ("C-M-<left>" . previous-buffer)
            ("C-M-b" . switch-to-buffer)
            ("C-M-k" . kill-buffer)
            ("C-M-l" . list-buffers)
            ("C-M-s" . save-buffer)
            ("C-M-0" . delete-window)
            ("C-M-1" . delete-other-windows)
            ("C-M-2" . split-window-below)
            ("C-M-3" . split-window-right)
            ("C-M-o" . other-window)
            ("C-M-}" . enlarge-window-horizontally)
            ("C-M-{" . shrink-window-horizontally)
            ("C-M-]" . enlarge-window)
            ("C-M-[" . shrink-window))

19. Help Me!

(bind-keys :prefix "C-h"
           :prefix-map help-map
           :prefix-docstring "Help me!"
           ("b" . describe-bindings)
           ("f" . describe-function)
           ("i" . info)
           ("k" . describe-key)
           ("l" . view-lossage)
           ("m" . describe-mode)
           ("p" . describe-package)
           ("v" . describe-variable)
           ("w" . where-is))

20. Bookmark Management

Bookmarks are a feature I'm not actively using at the moment but this configuration has been carried forward from previous configuration.

(bind-keys :prefix "C-x C-r"
           :prefix-map bookmark-map
           ("m" . bookmark-set))
(setq bookmark-save-flag 1)

21. Define Some Minibuffer Bindings

(bind-keys :map minibuffer-mode-map
           ("TAB" . minibuffer-complete)
           ("RET" . exit-minibuffer)
           ("M-n" . next-history-element)
           ("M-p" . previous-history-element))

22. Define Some Navigation Through a Project

(bind-keys :map global-map
           ("C-x C-f" . ffap)
           ("C-x `" . next-error))

23. Interact With Source Control

Initially this section is just going to contain some bindings that I copied from a previous incarnation of my Emacs configuration.

(bind-keys :map global-map
           ("C-x v v" . vc-next-action)
           ("C-x v d" . vc-dir))

(use-package vc
  :bind (:map vc-dir-mode-map
              ("M-m" . vc-dir-mark)
              ("M-u" . vc-dir-unmark)
              ("M-=" . vc-diff)))

24. Configure Org Mode

(add-to-list 'auto-mode-alist '("\\.org\\'" . org-mode))

 (use-package org
   :bind
   (:prefix "C-x C-o"
    :prefix-map global-org-map
    :prefix-docstring "Globally used org mode keys"
    ("a" . org-agenda)
    ("c" . org-capture)
    ("C-<SPC>" . org-store-link))

   (:map org-mode-map
   ("DEL" . org-delete-backward-char)
   ("TAB" . org-cycle)
   ("M-RET" . org-meta-return)
   ("M-<right>" . org-metaright)
   ("M-<left>" . org-metaleft)
   ("M-<down>" . org-metadown)
   ;; Chrombook
   ("<next>" . org-metadown)
   ("M-<up>" . org-metaup)
   ("C-a" . org-beginning-of-line)
   ("C-k" . org-kill-line)
   ("C-c d" . org-deadline)
   ("C-c q" . org-set-tags-command)
   ("C-c s" . org-schedule)
   ("C-c t" . org-todo)
   ("C-c w" . org-refile)
   ("C-x RET" . org-open-at-point)
   ("C-M-_" . org-cycle)))

25. Configure Dired Mode

(use-package dired
  :bind (:map dired-mode-map
  ("C-p" . dired-previous-line)
  ("C-n" . dired-next-line)
  ("RET" . dired-find-file)))

26. Configure Org Agenda

(use-package org-agenda
  :bind (:map org-agenda-mode-map
  ("C-n" . org-agenda-next-line)
  ("C-p" . org-agenda-previous-line)
  ("TAB" . org-agenda-goto)
  ("g" . org-agenda-redo-all)
  ("t" . org-agenda-todo)
  ("u" . org-agenda-bulk-unmark)
  ("T" . org-agenda-show-tags))
  :config
  (setq org-agenda-span 'day)
  ;; Allow for very old scheduled items for catch-up behavior.
  (setq org-scheduled-past-days 99999)
  ;; Use hl-line mode in org-agenda for visibilitiy
  (add-hook 'org-agenda-mode-hook 'hl-line-mode))

27. Configure hexl Mode

(use-package hexl
  :bind (:map hexl-mode
  ("C-v" . hexl-scroll-up)
  ("M-v" . hexl-scroll-down)))

28. Configure Info Mode

(use-package info
  :bind (:map Info-mode-map
  ("SPC" . Info-scroll-up)))

29. Configure Emacs Lisp

(add-to-list 'auto-mode-alist '("\\.el\\'" . emacs-lisp-mode))
(global-dash-fontify-mode)
(with-eval-after-load 'info-look
  (dash-register-info-lookup))
(bind-keys :map global-map
           ("C-x C-e" . eval-last-sexp))

30. Manage Environment for Called Commands

Some operating systems (such as OS X) will invoke Emacs with a separate profile that may not include configuration that has been defined for login sessions such as environment variables.

This basically steals the logic from exec-path-from-shell (to link) but the code itself is a bit dumber and is more oriented towards building out a larger code base rather than providing drop-in functionality.

Most of the constructs here will be prefixed with ^-env but those that are more general will omit the env segment.

30.1. Customization Group

I have not used customize in the past, but in addition to providing a user interface which may be more usable across devices it also seems to offer a natural means to locally specialize behavior without worrying about what goes into which file.

These will be functions which in some places helps reduce the overall complexity while retaining flexibility, and others are done largely for consistency. The large possible drawback is that this pushes more power and required expertise to the user (but additional protections and conveniences could be layered off if desired).

Initially funcall will be used fairly liberally until I stumble across a means to identify variables as functions and push Emacs lisp closer to a lisp 1 (I know such mechanisms are readily available but I'm just not actively looking).

(defgroup ^-env
  nil
  "Tune the environment."
  :prefix "^-env"
  :group 'environment)

30.2. Expand Using Shell

The underlying functionality revolves around evaluating expressions in a shell which is invoked with the login profile. Much of this may be able to be further generalized but there's currently no clear path or reason to do so.

At the high level such expansion will be done by wrapping the name as a shell parameter and then passing it through the fairly ubiquitous printf function.

(defun ^-shell-expand (name)
  (let ((shell-variable (^-enveloper '("${" . "}"))))
    (^-shell-printf "%s" (funcall shell-variable name))))

30.2.1. Enveloper

The above uses an "enveloper" which wraps a string in the provided pair. There's some similar behavior that seems worth extracting into an object when I get to reading about some of the object libraries in Emacs (it doesn't seem worth going the manual route of routing messages).

This makes use of currying since…I like currying. More specifically I think use of partial applications in local variables can provide tidy invocations of general blocks. Later on I'll probably make use of some library or other (dash?) to provide a range of functional programming behavior for me.

(defun ^-enveloper (pair)
  "Produce a function which will wrap a string within pair."
  (lambda (s) (concat (car pair) s (cdr pair))))

30.2.2. Define the Called Commands

  1. Shell Supplier

    The evaluation itself will be performed through a shell command which is defined here, defaulting to the Emacs standard.

    (defcustom ^-env-shell-supplier
      (lambda () shell-file-name)
      "Define the function which will return the shell to invoke
       along with any additional arguments."
      :type 'function
      :group '^-env)
    
  2. Shell Login Argument Supplier

    The shell command may also require some additional arguments to make sure that it is executed using the login profile.

    (defcustom ^-env-shell-login-arg-supplier
      (lambda () '("-l" "-i"))
      "Define the function which will return arguments that when
       passed to the shell will operate in login mode."
      :type 'function
      :group '^-env)
    
  3. Printf Command Supplier

    As previously mentioned this will expect to be evaluated using some form of printf…how practically extensible this is given the current logic seems unclear - likely most of these functions would be better off as being passed the argument they're expanding but all of that feels like premature generalization and right now the goal is just to define some of the magic values.

    (defcustom ^-env-printf-supplier
      (lambda () "printf")
      "Define the function which will return the printf command
       to be invoked by the shell."
      :type 'function
      :group '^-env)
    

30.2.3. Support Invoking the Commands

There's likely some better constructs for this floating around somewhere, but for now I'll start with what was inherited.

  1. Warn On Slow Evaluation

    Warning on a slow call is carried forward from exec-from-shell…I'm not entirely sure of its value given that there's no timeout behavior and the execution is not typically done repeatedly. I'd conjecture that maybe this is useful as such slow executions could produce undesirably slow Emacs start times. In any case it seems worth keeping around for the time being.

    (defcustom ^-env-warn-evaluation-duration-millis-supplier
      (lambda () 500)
      "Print a warning if evaluation duration exceeds this number of milliseconds."
      :type 'function
      :group '^-env)
    
  2. Support Timed Evaluation
    1. TODO Replace with with-delayed-message

      There's nothing particularly specialized about timing a call so this will be implemented through providing a first class function which takes a curried handler to which the time will be passed and can then be passed any body as a thunk which will be evaluated and timed. This would almost certainly be more idiomatically a macro in Emacs lisp but I tend to avoid that (which will be covered separately at some point).

      The implementation is typical timer behavior of tracking start time and then reporting the difference between the start and end times (in this case passing it to the handler. A general note (not specific to this code or language) is that it is often desirable to consistently report the time and therefore any calls that return through alternate paths (such as exceptions) should also be tracked (and preferably annotated accordingly). This is optimistically ignored for this code (and I'd typically prefer Either semantics over exceptions which makes that issue simpler to reason about) but that scenario may need further attention with this code.

    2. Millisecond Duration

      The duration will be passed in milliseconds as that's what's inherited from the original logic. This also generally seems to be the most common unit for timing application code - it is appropriate for relatively slow actions like invoking another process and faster actions can be timed in aggregates which can help smooth out the many things can skew sub-millisecond timings.

      This provides a basic implementation to calculate such a duration from two timestamps using Emacs functionality. There may be a date/time library floating around that could replace this later.

      (defun ^-temporal-diff-millis (start end)
        (thread-last start
                    (time-subtract end)
                    float-time
                    (* 1000.0)))
      
    3. Implement Timer

      This function body is a fairly straightforward higher-order function. Currently the handler will be invoked with funcall (which should be modified later).

      (defun ^-timed (handler)
        "Produce a function that will invoke thunks and return their value
         while also passing the execution duration to <handler>."
        (lambda (thunk)
          (let* ((start-time (current-time))
                 (result (funcall thunk))
                 (duration (^-temporal-diff-millis start-time (current-time))))
            (funcall handler duration)
            result)))
      
    4. Wire Warning

      The specific handler for the duration warning needs to be configured to be plugged into the general timer. This will be provided by a closure over the setting which returns an appropriate handler.

30.2.4. Transplants

The environment variables that should be carried across profiles will be called "transplants".

(defcustom ^-env-variable-transplants-supplier
  (lambda '("MANPATH"))
  "List of environment variables which will be transplanted on initialization."
  :group '^-env)

31. Structurizr Mode

The details of some of these should be covered. Why does font-lock-defaults require a nested list?

For now there is no hook defined since it wouldn't be used (although it is likely to be standard).

(define-derived-mode structurizr-mode fundamental-mode "Structurizr"
  "Major mode for editing Structurizr dsl"
  (set-syntax-table structurizr-mode-syntax-table)
  (make-local-variable 'structurizr-indent-offset)
  (set (make-local-variable 'indent-line-function) 'structurizr-indent-line)
  (setq font-lock-defaults (list (funcall structurizr-font-lock-default-collector))))

31.1. Font Lock

The definitions below are borrowed from the original mode, this should tied back more directly to the DSL reference with any resulting adjustment to naming or membership.

31.1.1. Words for Face

The original code also defined and then operated on values whereas this will attempt to make more use of in-place definitions with supporting functions as warranted.

This seems like something that likely already exists but to start this will make use of what was originally in place but wrap it up in a function for more direct use.

(defun ^-words-for-face (face words)
  "Return a pair of a regexp matching <words> and the provided <face>."
  (cons (regexp-opt words 'words) face))

31.1.2. List Collector

There is likely something readily available somewhere else that does this. When defining keywords in this file it is nice to allow each block to be self-contained so that it can be evaluated easily, but when collecting all lists that invites some possibly fragile mutability. To make this slightly cleaner, at least according to my tastes, we'll use an accumulator closure over the list that appends an argument if provided and returns the contents if no argument is provided.

(defun ^-list-collector ()
  "Produce a list collector."
  (let ((l nil))
    (lambda (&optional arg)
      (if arg (push arg l) l))))

31.1.3. Collect Categories of Known Words

Each block will push onto the list to keep the blocks self-contained.

There's a note around ordering being significant…ideally this can be avoided if it is substantiated but otherwise it should be very clearly laid out or ideally encoded within the logic.

(setq structurizr-font-lock-default-collector (^-list-collector))
  1. Keywords
    (funcall structurizr-font-lock-default-collector
             (^-words-for-face
              'font-lock-keyword-face
              '(
                "enterprise"
                "model"
                "views"
                "workspace"
                )))
    
  2. Types
    (funcall structurizr-font-lock-default-collector
             (^-words-for-face
              'font-lock-type-face
              '(
                "branding"
                "component"
                "container"
                "containerInstance"
                "deployment"
                "deploymentEnvironment"
                "deploymentGroup"
                "deploymentNode"
                "dynamic"
                "element"
                "filtered"
                "group"
                "infrastructureNode"
                "person"
                "perspectives"
                "properties"
                "relationship"
                "softwareSystem"
                "softwareSystemInstance"
                "styles"
                "systemContext"
                "systemLandscape"
                "themes"
                )))
    
  3. Relationship
    (funcall structurizr-font-lock-default-collector
             (^-words-for-face
              'font-lock-function-name-face
              '("->")))
    
  4. Properties
    (funcall structurizr-font-lock-default-collector
             (^-words-for-face
              'font-lock-variable-name-face
              '(
                "autoLayout"
                "background"
                "border"
                "color"
                "colour"
                "dashed"
                "description"
                "exclude"
                "fontSize"
                "height"
                "icon"
                "include"
                "metadata"
                "opacity"
                "position"
                "routing"
                "shape"
                "stroke"
                "tags"
                "technology"
                "thickness"
                "title"
                "url"
                "width")))
    

31.2. Indentation

31.2.1. Configure Offset

(defvar structurizr-indent-offset 4
  "Define the indentation offset for `structurizr-mode'.
Lines will be indented this offset multiplied by the detected level.
Currently only spaces are supported.")

31.2.2. Determine Indentation Level

The basic algorithm here will start from what was defined upstream. This will be broken up a bit and in the future any other standard algorithms will be explored.

This currently adjusts the formatting after a newline rather than inserting the character itself so something more electric may be better.

(defun structurizr-indent-line ()
  "Indent current line as Structurizr dsl."
  (interactive)
  (let* ((initial-level (^-opener-count-to-top "{"))
         (closers (^-closers-on-line "}"))
         (level (max 0 (- initial-level closers))))
    (indent-line-to (* level structurizr-indent-offset))))

  1. Opener Count to Top

    The current approach involves ascending lists until an ignored error is encountered, and then reporting the count of the number of a particular opener that was encountered.

    This feels like it should be simpler in some way or another. This level should potentially be readily available and the ascension behavior should be tied to the relevant grammar which would obviate the need for the additional check. At the moment I don't know what options are available and unused versus those that would need further support so I'll circle back to this over time (especially as other modes are pulled in).

    (defun ^-opener-count-to-top (opener)
      "Ascend through levels and count the number of <opener>s seen."
      (let ((level 0))
        (save-excursion
          (beginning-of-line)
          (condition-case nil
              (while t
                (backward-up-list 1)
                (when (looking-at opener)
                  (setq level (+ level 1))))
            (error nil)))
        level))
    
  2. Closer Count

    The current logic only decrements one if the first character is a closer. This may be nicer to just count all instances but that feels like a taste based on the resulting behavior. In any case it should likely support either.

    In the current form this will either return 0 or 1.

    There was also a separate test initially which seemed to look to prevent over-outdenting which can be addressed by a ramp function on outside use.

    (defun ^-closers-on-line (closer)
      (save-excursion
        (back-to-indentation)
        (if (looking-at closer) 1 0)))
    

31.3. Syntax Table

The standard syntax table function newentry value is bit arcane so I'm going to try to define some more expressive functions to produce the values (and likely find something that exists elsewhere).

This can be generalized a bit as needed by producing functions which produce the first character but in the short term the only interesting one I need is for punctuation so that will be written as specialized. The underlying logic can then be a function which accepts named flags as keywords each of which will map to the underlying terse flag.

Initially I'll use a fairly clunky cond block though there is almost certainly something cleaner. This could be cleaned up through partial application but I'd imagine there's some better pattern matching stuff I need to stumble upon.

This should all be significantly cleaned up into a nicer DSL (or ideally an existing one found) but right now I'm starting with the basics. In particular the relationships between some of the flags should be modeled in that DSL whereas the naive approach allows for seemingly invalid combinations. These should also operate on a closure over a comment table.

(defun ^-syntax-table-punctuation (&rest flags)
  (apply #'concat (cons ". "
          (mapcar (lambda (flag)
                    (cond
                     ((eq flag :start-2char-comment-open) "1")
                     ((eq flag :end-2char-comment-open) "2")
                     ((eq flag :start-2char-comment-close) "3")
                     ((eq flag :end-2char-comment-close) "4")
                     ((eq flag :for-comment-sequence-b) "b")
                    (t (error "Unrecognized flag" flag))))
                  flags))))
(defun ^-syntax-table-comment-ender () ">")

(setq structurizr-mode-syntax-table
  (let ((syntax-table (make-syntax-table)))
    (modify-syntax-entry
      ?/
      (^-syntax-table-punctuation
        :start-2char-comment-open
        :end-2char-comment-open
        :end-2char-comment-close)
      syntax-table)
    (modify-syntax-entry
      ?*
      (^-syntax-table-punctuation
        :end-2char-comment-open
        :start-2char-comment-close
        :for-comment-sequence-b))
    (modify-syntax-entry ?\n (^-syntax-table-comment-ender) syntax-table)
    syntax-table))

31.4. Associate with Extension

dsl is certainly not unambiguously Strucutrizr files, but it's the only association I'm currently expecting.

(add-to-list 'auto-mode-alist '("\\.dsl\\'" . structurizr-mode))

32. Programming Support

This is currently copied forward but past configurations but not revisited. It looks as though this is the beginning of seeking to support code collapsing.

(add-hook 'prog-mode-hook hs-minor-mode)
(bind-keys :map prog-mode-map
           ("C-M-_". hs-toggle-hiding))

32.1. C

(add-to-list 'auto-mode-alist '("\\.c\\'" . c-mode))

33. Configure Lilypond

This is the very beginning of support for Lilypond mode, which I think I started to copy from the Lilypond source code years ago. This is unlikely to provide any valuable in its current form and so will either be built upon further later or removed.

  (defvar LilyPond-mode-map ()
    "Keymap used in `LilyPond-mode' buffers.")

  (defun LilyPond-mode ()
    "Major mode for editing LilyPond music files.

This mode knows about LilyPond keywords and line comments, not abou
indentation or block comments.  It features easy compilation, error
finding and viewing of a LilyPond source buffer or region.

COMMANDS
\\{LilyPond-mode-map}
VARIABLES

LilyPond-command-alist\t\talist from name to command"
    (interactive)
    ;; set up local variables
    (kill-all-local-variables))

(add-to-list 'auto-mode-alist '("\\.ly$" . LilyPond-mode))
(add-to-list 'auto-mode-alist '("\\.ily$" . LilyPond-mode)))

34. Configure Gnus

I'm currently using Gnus to read my mail but have not yet dug into it again - this like many others reflect inherited previous configuration options.

;; This should be cleaned up a bit but was just implemented in the most
;; obvious way for now.
(defun gnus-group-read-50 ()
  (interactive)
  (gnus-group-read-group 50))

(use-package gnus
  :bind (:map gnus-group-mode-map
         ("M-g" . gnus-group-get-new-news-this-group)
         ("M-<down>" . gnus-group-read-50)
         :map gnus-summary-mode-map
         ("M-<down>" . gnus-summary-next-page)
         ("C-c m" . gnus-summary-move-article)))

35. Configure EWW

On Linux I may explore EAF but for now I'll be using EWW since EAF is currently Linux only and isn't working particularly well as configured on my Sway installation.

(use-package eww
  :bind (:map eww-mode-map
              ("C-c w" . eww-copy-page-url)))
(setq browse-url-browser-function 'eww-browse-url)

36. Queue

36.1. TODO Find or define more expressive mode-alist registration

Created: 2023-07-28 Fri 15:16

Validate