Skip to content

Non-keybinding init.el configuration utilities (general.el spinoff)

Notifications You must be signed in to change notification settings

emacs-magus/satch.el

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 

Repository files navigation

Satchel User Manual

About

NOTE: I do not recommend trying to use this package in your configuration until I have added back tests. The new functionality in particular has barely been tested and may not work correctly for all cases. Also, the syntax may change completely before the first release.

satch.el currently provides non-keybinding init.el configuration utilities for settings, hooks, and advice.

Satchel does not provide:

  • General or data structure utility functions like cl-lib, dash, map.el, etc.
  • Key definition helpers (see general and eventually familiar)
  • Dedicated delayed evaluation helpers (like :after-call and :defer-incrementally; see once)
  • A replacement for use-package (just use use-package or setup.el)

The point of this package is to extract the non-keybinding init.el helpers from my configuration and general.el (like general-setq, general-add-hook, general-advice-add, etc.) into a separate package.

Feature Summary

  • Alternatives to setq, set, and similar functions that work with custom setters
  • Alternatives to add-hook and advice-add that allow list arguments (add multiple functions to multiple hooks or as advice) and for “transient” functionality (once some condition is met, run once and remove from all hooks or advised symbols)
  • Utilities for defining functions to add to hooks or as advice
  • use-package keywords for some of these utilities with extra shorthand (e.g. automatically infer a hook name or a mode name from the package name)

Here are some quick examples of how these can be useful.

satch-setq is a lightweight setq alternative that works with variables with custom setters:

;; `setq' will not work correctly for these variables if the corresponding
;; package has already been loaded
(satch-setq auto-revert-interval 10
            evil-want-Y-yank-to-eol t
            evil-search-module 'evil-search)

;; add several items to `lsp-file-watch-ignored-directories' to prevent watching
;; hundreds/thousands of files in python
(satch-shove lsp-file-watch-ignored-directories
             '("[/\\\\]\\__pycache__\\'"
               "[/\\\\]\\.mypy_cache\\'"
               "[/\\\\]\\.venv\\'"))

Examples of the hook use-package keywords:

;; Run `company-posframe-mode' on `company-mode-hook' (mode name inferred)
(use-package company-posframe
  :hooks 'company-mode-hook)
;; same as
(use-package company-posframe
  :hooks ('company-mode-hook #'company-posframe-mode))


(defun my-lsp-pyright ()
  "Require lsp-pyright and run `lsp'."
  (require 'lsp-pyright)
  ;; ...
  (lsp))

;; Run `my-lsp-pyright' on `python-mode-hook' (mode hook name inferred)
(use-package python
  :config-hooks #'my-lsp-pyright)
;; same as
(use-package python
  :config-hooks ('python-mode-hook #'my-lsp-pyright))


;; Use `global-display-line-numbers-mode' but disable it in some childframes
(use-package lsp-ui
  :config-hooks
  ('lsp-ui-doc-frame-mode-hook (satch-disable display-line-numbers-mode)))

Example of using satch-advice-add building on the prior all-the-icons-example:

(use-package all-the-icons
  ;; ...
  :config
  ;; prevent breakage in tty
  ;; https://github.com/hlissner/doom-emacs/blob/29e4719a7d3c9991445be63e755e0cb31fd4fd00/core/core-ui.el#L479
  (cond
   ((daemonp)
    (defun doom--conditionally-disable-all-the-icons-in-tty-a
        (orig-fn &rest args)
      "Return a blank string in tty Emacs which doesn't support multiple fonts."
      (if (or (not after-init-time) (display-multi-font-p))
          (apply orig-fn args)
        ""))
    (satch-advice-add
     '(all-the-icons-octicon
       all-the-icons-material
       all-the-icons-faicon all-the-icons-fileicon
       all-the-icons-wicon all-the-icons-alltheicon)
     :around #'doom--conditionally-disable-all-the-icons-in-tty-a))
   ((not (display-graphic-p))
    (defun doom--disable-all-the-icons-in-tty-a (&rest _)
      "Return a blank string for tty users."
      "")
    (satch-advice-add
     '(all-the-icons-octicon
       all-the-icons-material
       all-the-icons-faicon all-the-icons-fileicon
       all-the-icons-wicon all-the-icons-alltheicon)
     :override #'doom--disable-all-the-icons-in-tty-a))))

Here are what the above examples would look like with setup.el:

(setup (:package lsp-ui)
  (:config-hooks
   'lsp-ui-doc-frame-mode-hook
   (list (satch-disable display-line-numbers-mode)
         #'my-pyright-setup)))

(setup (:package company-posframe)
  (:hooks 'company-mode-hook))

Planned features:

  • Optionally record settings, hooks, advice, etc. with annalist.el (issue #3). The priority of implementing this is currently low since I don’t need this functionality. Annalist already does all of the work though, so if this is something you really want, please comment on that issue.

Example Setup

Since satchel is meant to be used in your init.el, you will be requiring it immediately.

(use-package satch
  :demand t)

(use-package satch-use-package
  :init
  ;; for use-package keywords; see below for a more detailed explanation
  (eval-and-compile
    (setq satch-use-package-keyword-prefix "s"
          ;; OR set specific aliases
          satch-use-package-keyword-aliases
          '(":satch-hooks" ":hooks"
            ":satch-config-hooks" ":config-hooks"
            ;; ...
            )
          ;; READ to understand shorthand; don't blindly copy
          satch-use-package-hook-shorthand t)
    (require 'satch-use-package)))

Table of Contents

Relationship With use-package

Satchel is orthogonal to use-package. It can be used with or without it and provides use-package keywords if you install and require satchel-use-package. The philosophy of the use-package keywords is to match the syntax of the underlying utilities as closely as possible, providing extra functionality only when it is possible to allow shorthand given the package name.

Keyword Prefixes

By default, all keywords are prefixed with :satch (e.g. :satch-hooks) to prevent clashes with other builtin or extra keywords. It is recommend you set this to something shorter after confirming there are no clashes with the keywords in your current use-package-keywords. This must be done before requiring satch-use-package:

(eval-and-compile
  (setq satch-use-package-keyword-prefix "s")
  (require 'satch-use-package))

This variable needs to be set at compile time if you are compiling your init. Like use-package, satch-use-package is not required at load time when compiling, and you can optionally use eval-when-compile instead. This will require manually manually configuring and requiring satch-use-package after loading your init.el if you want to use it with use-package (e.g. in a scratch buffer). If you are not sure what this means, just follow the example above, which will work in all cases. Note that compiling your init file is not generally recommended, and if you are not aware of the caveats, you probably should not be compiling your init file.

You can also change individual keywords that do not conflict with others by setting satch-use-package-keyword-aliases. This compares with the full starting keyword name and has precedence over satch-use-package-keyword-prefix, which only applies to keywords not found in the aliases plist:

(eval-and-compile
  (setq satch-use-package-keyword-aliases
        '(":satch-hooks" ":hooks"
          ":satch-config-hooks" ":config-hooks"
          ;; ...
          ))
  (require 'satch-use-package))

New Keywords

Satchel does not currently support use-package keywords for all functionality mostly because I do not see the point in adding a keyword for every helper instead of just using them in :config or :init. The current logic is to mainly provide keywords that can make use of the package name to guess some of the arguments.

If you want extra keywords, feel free to open an issue explaining why (e.g. a keyword for satch-setq for organizational reasons).

Each use-package keyword is explained in the same section for that functionality below.

Setting/Variable Utilities

setq has a some downsides. If a defcustom variable used :set to define a custom setter (e.g. auto-revert-interval), using setq for it will not work correctly if the package has been loaded. customize-variable can be used but also has some annoyances. For example, it doesn’t support defining multiple variables at once. There are other alternatives, but they are not as lightweight as setq and they all do extra things you probably don’t need. For example, customize-variable can be called interactively, will attempt to load variable dependencies, and allows the user to specify comments. From some basic testing satch-setq is 10x to 100x faster because it does not include this functionality, but the speed difference should not really be noticeable if you aren’t setting thousands of variables during initialization.

satch.el provides setters that are more similar to what most people use but still handle custom setters correctly. They will also eventually optionally record settings for later display with annalist.el.

satch-setq

It has the same syntax as setq but supports custom setters.

Here’s an example using variables that have a custom setter:

(satch-setq auto-revert-interval 10
            evil-want-Y-yank-to-eol t
            evil-search-module 'evil-search)
;; if you use it a lot, you can always define a shorter alias
(defalias 'ssetq  #'satch-setq)

Note that setq will work as expected as long it is used before the corresponding package is loaded, but with customize-set-variable or satch-setq, you do not need to worry about whether or not the package has been loaded.

One major difference from customize-set-variable that you should be aware of is that satch-setq falls back to using set instead of set-default. This means that when there is no custom setter, like setq, it will alter the local value of buffer-local variables instead of the default value. You can use satch-setq-default to instead fall back to altering the default value, but really it shouldn’t matter. I have not seen custom setters for for variables that are buffer-local. The custom setters just use set-default (e.g. if you make auto-revert-interval into a buffer-local variable, and then call its custom setter, it will change the default value).

satch-set

Like satch-setq but it evaluates the variable positions like set.

(defvar foo 2)
(satch-set 'foo 3)

satch-setq-default

Like satch-setq but it falls back to set-default when there is no custom setter.

satch-setq-local

Like satch-setq but makes all variables buffer local.

setq-hooks and :setq-hooks use-package keyword

Not yet implemented.

satch-pushnew

Like cl-pushnew, but :test defaults to equal, and it will call a custom setter afterwards if one exists.

(satchel-pushnew 'python-mode aggressive-indent-excluded-modes)

satch-shove

This has the same functionality as satch-pushnew, but the place comes first and the second argument is a list of values to add (more like setq, add-to-list, and nconc though it is still wrapping cl-pushnew and accepts cl-pushnew keywords).

(satch-shove lsp-file-watch-ignored-directories
             '("[/\\\\]\\__pycache__\\'"
               "[/\\\\]\\.mypy_cache\\'"
               "[/\\\\]\\.venv\\'"))
;; same as
(satch-pushnew "[/\\\\]\\__pycache__\\'" lsp-file-watch-ignored-directories)
(satch-pushnew "[/\\\\]\\.mypy_cache\\'" lsp-file-watch-ignored-directories)
(satch-pushnew "[/\\\\]\\.venv\\'" lsp-file-watch-ignored-directories)

Hook and Advice Utilities

Unlike, use-package’s :hook and other commonly used hook and advice helpers, satchel’s hook and advice helpers try to mirror the syntax of the builtin add-hook and advice-add, so that they can be used as drop-in replacements. If you prefer a different syntax like (advise :around <oldfun> <newfun>), it is trivial to write a macro around the utilities satchel provides to support this.

Note on Hook Shorthand

As shown in Feature Summary, satchel provides some shorthand to guess mode and hook names and to guess if symbols are hooks or functions. It may be surprising then to learn that satch-add-hook does not currently support the common functionality of adding -hook to the end of symbols in the hook position. I don’t think there is any real benefit of this. It only saves typing 5 characters. Having the full hook name makes it more immediately obvious that a symbol is a hook. For example, this helps distinguish the usage difference between :satch-hooks and :satch-config-hooks. Otherwise, you could do something like this:

(use-package lispy
  :hooks 'lisp-mode)

(use-package org
  :config-hooks 'visual-line-mode)

This is less clear than it could be. Here I’ve incorrectly forgotten to sharp-quote visual-line-mode, so until I examine the surrounding context, it’s not clear if it’s a function or a hook. Even if I hadn’t done this, what about the first use-package? Normally something ending in -mode is a function. Did I forget to sharp quote here? Well no, I can see the :hooks and know that lispy provides a minor mode to know that this is fine, but I don’t think having to type 5 less characters is worth this reduction in clarity.

A better functional example of an upside of explicitly specifying a hook name is that it allows using helpful-at-point on the symbol.

The point is that explicitly including hook is not needless verbosity, and I think it is better to write a configuration prioritizing readability over verbosity when the two cannot be reconciled.

It may seem inconsistent for me to not provide this shorthand but provide other forms of shorthand, but these are the guidelines I’ve tried to follow when introducing shorthand in satch.el and related packages (specifically once.el):

  • If shorthand can handle all reasonable situations correctly, then allow it by default. This is why once (from once.el) currently accepts either a list of forms or functions without configuration.
  • Shorthand with serious caveats is opt-in. In general.el, there was a lot of functionality provided by default that could not perfectly handle every situation and required special handling of some cases. This can be confusing. Therefore in satchel, users must confirm they understand the limitations of shorthand before using it by setting the corresponding variables.
  • Don’t include shorthand that excludes part of symbols rather than entire symbols
  • Don’t include shorthand when there is no way to fall back to full syntax to handle special cases

The last two points are related and are why I have not added -hook addition shorthand.

For example, the :hooks shorthand can guess a mode name, but you still specify the full hook name. When the mode name is not guessable from the package, you can fall back to the full satch-add-hook argument list to specify mode to run on the hook, so it meets these requirements.

In the case of adding -hook, some hooks end with -functions instead. The shorthand could check for this, but the problem is that how a hook name ends is not enforced. You can name a hook whatever you want to. While you should not encounter a situation with a differently named hook, it would be impossible to handle without introducing new syntax, and I’d like satch-add-hook to remain compatible with add-hook. Therefore, adding -hook automatically will never be the default.

Unlike -hook addition shorthand, once condition shorthand can be also replaced with the full condition syntax if there happened to be any unusually named hooks. It’s also worth noting, that it would be impossible to combine this -hook addition shorthand with once shorthand, which depends on the -hook or -functions being present to distinguish hooks from symbols to advise.

If you still want the option to use this shorthand, please make an issue. I might consider adding it with a warning, making it opt-in.

satch-add-hook and satch-remove-hook

satch-add-hook can act as a drop-in replacement for add-hook, but it supports lists for hooks and functions.

For example:

(satch-add-hook my-lisp-mode-hooks
                (list #'lispy-mode #'rainbow-delimiters-mode))

satch-add-hook can also add “transient” functions to hooks that will run once and then remove themselves from all hooks (inspired by eval-after-load and Doom Emacs).

For example, cl-lib-highlight-initialize from the cl-lib-highlight package only needs to be run once:

(satch-add-hook 'emacs-lisp-mode-hook #'cl-lib-highlight-initialize
                :transient t)

The argument to :transient can also be a check function. In this case, the function added to the hook will only run and then be removed once the check function returns non-nil. For example, here is an alternative to once-gui using satch-add-hook:

(defmacro satch-after-gui (&rest body)
  "Run BODY once after the first GUI frame is created."
  (declare (indent 0) (debug t))
  `(if (and (not (daemonp)) (display-graphic-p))
       (progn ,@body)
     (satch-add-hook 'server-after-make-frame-hook
                     (lambda ()
                       ,@body)
                     :transient #'display-graphic-p)))

Note that if the argument to :transient is a function, it will be passed any arguments (i.e. if the hook is run with run-hook-with-args).

You can always use once (from once.el) in place of transiently adding a hook. If more complex conditions are required, you may be better off using (or have to use) once instead.

The only additional functionality of satch-remove-hook is to support lists:

(satch-remove-hook my-lisp-mode-hooks
                   (list #'lispy-mode #'rainbow-delimiters-mode))

hook use-package keywords

Satchel provides two alternatives to use-package’s :hook that use satch-add-hook called :satch-hooks and :satch-config-hooks. Both take any number of arguments of symbols or lists. List arguments work the same for both; they correspond to a list of arguments for satch-add-hook. The primary difference between the two is that symbol arguments to :satch-hooks are hooks, but they are functions for :satch-config-hooks (functions to run as configuration/setup). :satch-hooks is intended for loading the package, and :satch-config-hooks is meant for configuring it. From now on, these keywords will be referred to by their suggested aliases :hooks and :config-hooks.

:satch-hooks / :hooks

:hooks is for specifying a hook to load a package. The primary use case is to add a package’s minor mode function to some user-specified hook, so that when hook is run, the package will be loaded and the mode enabled. This means that :hooks will usually imply :defer t. While it does not always imply :defer t, it will add any non-lambda functions to :commands (this is the same behavior as :hook). Though this is usually unnecessary (the functions probably already have autoloads unless you’ve defined them in :config), it will in turn imply :defer t.

Symbols specified with :hooks correspond to hooks, and the function to add to each hook is inferred from the package’s name (i.e. -mode is automatically added to the package name unless the package’s name already ends in -mode). For example, these are all the same:

;; setup
(eval-and-compile
  ;; required to specify a hook alone instead of the full argument list; by
  ;; setting this, you confirm that the shorthand only works for symbols (quoted
  ;; or unquoted) not lists/function calls (see the next heading for an example)
  (setq satch-use-package-hook-shorthand t)
  (require 'satch-use-package))

;; add `rainbow-delimiters-mode' to `prog-mode-hook'
(use-package rainbow-delimiters
  :hooks 'prog-mode-hook)

(use-package rainbow-delimiters
  ;; a `satch-add-hook' arglist
  ;; a missing FUNCTIONS argument will be replaced with inferred minor mode
  :hooks ('prog-mode-hook))

(use-package rainbow-delimiters
  ;; a null or non-symbol placeholder for FUNCTIONS will be replaced with
  ;; inferred minor mode command; this may be useful if you want to keep the
  ;; inferred command but also want to set the DEPTH and/or LOCAL arguments
  ;; afterwards; for this specific example, you don't actually need to change
  ;; DEPTH
  :hooks ('prog-mode-hook nil t))

(use-package rainbow-delimiters
  ;; the full arglist for `satch-add-hook' can be specified
  ;; this is necessary if inference is not possible (see below for an example)
  :hooks ('prog-mode-hook #'rainbow-delimiters-mode))

;; without :hooks
(use-package
  ;; :commands implies :defer t
  :commands rainbow-delimiters-mode
  :init (satch-add-hook 'prog-mode-hook #'rainbow-delimiters-mode))

If you are already familiar with :hook, you should note that there are quite a few syntactic differences between :hooks and :hook. Firstly, quoting the hooks and functions is required. :hooks uses the same syntax as (satch-)add-hook for both clarity and convenience. For example, the user may want to specify a variable containing a list of hooks instead of an actual hook name:

(defconst my-lisp-mode-hooks
  '(lisp-mode-hook
    emacs-lisp-mode-hook
    clojure-mode-hook
    scheme-mode-hook
    ;; ...
    ))

(use-package lispy
  :hooks my-lisp-mode-hooks)

;; same as
(use-package lispy
  :hooks (my-lisp-mode-hooks))

;; same as
(use-package lispy
  ;;  `satch-add-hook' can take a list of hooks for the HOOK argument
  :hooks ('(lisp-mode-hook
            emacs-lisp-mode-hook
            clojure-mode-hook
            scheme-mode-hook
            ;; ...
            )))

Furthermore, :hooks will not automatically add -hook to specified hook symbols (i.e. you must specify prog-mode-hook; prog-mode is not sufficient). See Note on Hook Shorthand for the reasoning.

Lastly, :hook only takes one argument, whereas :hooks can take an arbitrary number of arguments:

(use-package lispy
  ;; any number of symbols (or argument lists) is allowed
  :hooks
  'lisp-mode-hook
  'emacs-lisp-mode-hook
  'clojure-mode-hook
  'scheme-mode-hook)

Note that if the function name cannot be inferred from the package name (i.e. the package name or the package name with -mode appended is not correct), you need to specify a full satch-add-hook arglist:

(use-package yasnippet
  :hooks ('(text-mode-hook prog-mode-hook) #'yas-minor-mode))

:satch-config-hooks / :config-hooks

:config-hooks is for specifying functions to add to a package’s mode hook. It is suited for enabling minor modes or running setup/configuration functions. The hook is inferred from the package’s name (by appending either -mode-hook or just -hook if the package’s name ends in -mode). If the hook cannot be inferred from the package name, then the full arglist must be specified just as with :hooks. Unlike :hooks, :config-hooks never adds functions to :commands and therefore never implies :defer t. This is because the functions specified are ones that should be run when turning on (or toggling) the mode(s) the package provides. The specified functions are external to the package, could be called elsewhere, and therefore should not trigger the package to load. The following use-package statements all have the same effect:

(eval-and-compile
  ;; required to specify a function alone instead of the full argument list; by
  ;; setting this, you confirm that the shorthand only works for symbols (quoted
  ;; or unquoted) not lists/function calls
  (setq satch-use-package-hook-shorthand t)
  (require 'satch-use-package))

(use-package org
  ;; For a major-mode package, you might use :mode to imply :defer t (or just
  ;; use :defer t; or just `use-package-always-defer' which I personally prefer)
  :config-hooks
  #'visual-line-mode
  #'my-org-setup
  ;; ...
  )

;; this is also valid but less concise
(use-package org
  ;; specify null or non-symbol placeholder for HOOKS to use inferred hook
  :config-hooks (nil (list #'visual-line-mode #'my-org-setup)))

;; without satch-use-package
(use-package org
  :init
  (satch-add-hook 'org-mode-hook (list #'visual-line-mode #'my-org-setup)))

Like with :hooks, :config-hooks still requires quoting, so you can use variables and function/macro calls to generate the function to add to the hook:

(use-package proced
  :config-hooks (nil (satch-disable visual-line-mode)))

Note that even with satch-use-package-hook-shorthand enabled, you cannot simplify the above case. The shorthand only supports symbols and functions like 'symbol and #'function.

;; INVALID! it will be interpreted as an argument list where
;; satch-disable is a variable containing hooks
(use-package proced
  :config-hooks (satch-disable visual-line-mode))

Although you could use :config-hooks to enable minor modes for some major mode (e.g. enable flyspell inside (use-package org)), it is probably more logical/organized to group these hooks along with their minor modes’ use-package declarations (e.g. using :hooks). :config-hooks is more suited for setup functions. Expanding on the proced example:

(defun my-proced-setup ()
  (visual-line-mode -1)
  ;; not global; has to be run in buffer
  (proced-toggle-auto-update t))

(use-package proced
  :config-hooks #'my-proced-setup)

satch-advice-add / satch-add-advice and satch-advice-remove / satch-remove-advice

satch-add-hook can act as a drop-in replacement for add-hook, but it supports lists for hooks and functions.

;; run these commands in the base buffer when using polymode
(satch-advice-add '(outline-toggle-children
                    counsel-outline
                    counsel-semantic-or-imenu
                    consult-outline
                    consult-org-heading
                    org-edit-special
                    worf-goto)
                  :around #'polymode-with-current-base-buffer)

Like satch-add-hook, it supports “transient” advice. See ~satch-add-hook~ and ~satch-remove-hook~ for more information.

(use-package ox-hugo
  :init
  ;; only require ox-hugo-auto-export if I visit my blog directory
  (satch-advice-add 'after-find-file
                    :before
                    (lambda (&rest _)
                      (org-hugo-auto-export-mode))
                    :transient #'my-blog-dir-p))

Because I don’t like the difference in naming between default advice and hook functions, satch-add-advice and satch-remove-advice are also provided as aliases.

Function Definition Utilities

These are mainly provided to make generating commands or functions to add to hooks a little easier. You can alias these to something shorter if you use them often.

satch-defun

This is defun, but it is guaranteed to return the generated function. defun has an undefined return value. Though defun currently returns the created function, that could potentially change. Even if it is unlikely to change, it is best to be safe and not rely on undefined behavior.

satch-disable

Returns a named function to disable a mode. This is useful for generating a function to add to a hook.

(satch-disable display-line-numbers-mode)
;; expands to
(satch-defun satch-disable-display-line-numbers-mode (&rest _)
  "Disable display-line-numbers-mode."
  (display-line-numbers-mode -1))

Example usage:

(use-package lsp-ui
  :config-hooks
  ('lsp-ui-doc-frame-mode-hook (satch-disable display-line-numbers-mode))

satch-fn - does not exist

Like clojure’s fn, generate a lambda with implicit arguments. This is too general purpose for satchel and also does not exist because l (on MELPA) and short-lambda exist, which I will recommend using instead.

Comparison With Other Packages

Comparison With add-hooks

The helpers from add-hooks cannot act as drop-in replacements for add-hook and have less functionality.

Comparison with general.el

Most of these utilities initially came from general.el. No more non-keybinding configuration utilities will be added to general.el, and its successor will include no non-keybinding configuration utilities. I am only leaving the old utilities in general.el for backwards-compatibility. It is recommended you use this package instead.

About

Non-keybinding init.el configuration utilities (general.el spinoff)

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published