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.
- Alternatives to
setq
,set
, and similar functions that work with custom setters - Alternatives to
add-hook
andadvice-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.
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)))
- About
- Feature Summary
- Example Setup
- Relationship With =use-package=
- Setting/Variable Utilities
- Hook and Advice Utilities
- Function Definition Utilities
- Comparison With Other Packages
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.
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))
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.
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
.
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).
Like satch-setq
but it evaluates the variable positions like set
.
(defvar foo 2)
(satch-set 'foo 3)
Like satch-setq
but it falls back to set-default
when there is no custom setter.
Like satch-setq
but makes all variables buffer local.
Not yet implemented.
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)
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)
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.
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
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))
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
.
: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))
: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-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.
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.
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.
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))
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.
The helpers from add-hooks cannot act as drop-in replacements for add-hook
and have less functionality.
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.