Skip to content

emacs-magus/once

Repository files navigation

Once User Manual

Test action status Lint action status Code coverage

Once upon a time, there was a Princess who lived in a marzipan castle…

About

NOTE: I do not recommend trying to use this package in your configuration until it is on MELPA unless you are willing to deal with breaking changes.

once.el provides extra delayed evaluation utilities meant to be used in your init.el. The utilities are primarily meant to simplify specifying conditions for when packages should load (and/or minor modes be activated). It can also be used more generally as an eval-after-load replacement to run arbitrary code one time once some (simple or complex) condition is met.

If you are familiar with Doom’s :after-call (transient hooks and advice), :defer-incrementally, and hooks like doom-first-input-hook and doom-first-file-hook, this package provides a utilities to achieve the same thing. One difference is that the primary utility once is more flexible/generic and can be used to replace :after-call, Doom’s “first” hooks, and more. In combination with the other emacs-magus packages (especially satch.el), once.el may be useful for anyone coming from Doom Emacs or some other distribution/starter kit who wants some of their helpful init.el configuration utilities without having to copy the code. For more information on replacing previously Doom-only utilities with standalone packages, see Replacing Doom.

Table of Contents

Feature Summary

  • once - Run code one time once some simple or complex condition (involving hooks, advice, package/file loads, and extra checks) is met
  • once-x-require - Once “x” condition is met, require some package(s)
  • once-require-incrementally - Space out requiring many packages during idle time (equivalent to Doom’s :defer-incrementally)
  • once-eval-after-load / once-after-load - An eval-after-load alternative (see below for explanation of the difference from eval-after-load)
  • once-with-eval-after-load / once-with - A with-eval-after-load alternative (see below for explanation of the difference from with-eval-after-load)
  • Optional integration with use-package and setup.el

Examples

once is a swiss army knife for deferring packages/configuration.

It can be used to run code the first time a hook runs:

;; Enable `savehist-mode' once the first time `pre-command-hook' runs; during
;; idle time, incrementally load custom then savehist; this is how Doom loads
;; savehist at the time of writing
(setq once-shorthand t)
(once 'pre-command-hook #'savehist-mode)
(once-require-incrementally savehist custom)

With use-package, the mode to run can be inferred:

(use-package savehist
  :require-incrementally custom
  :once 'pre-command-hook)
;; OR for a more meaningful name
(require 'once-conditions)
(use-package savehist
  :require-incrementally custom
  :once once-input)

once can be used to run code the first time a package loads:

;; Enable `magit-todos-mode' after loading magit
(use-package magit-todos
  :once "magit")

once can be used to run code the first time a function runs:

(use-package magit-todos
  :once #'magit-status)

once can be used to run code the first time a hook or function runs (like :after-call):

(use-package reveal
  :once ((list 'on-switch-buffer-hook #'after-find-file)
         (global-reveal-mode)))
;; OR with the provided `once-buffer' condition
(require 'once-conditions)
(use-package reveal
  :once (once-buffer (global-reveal-mode)))

;; another example with winner; enable `winner-mode' on `once-buffer' condition;
;; this is the equivalent of how Doom runs `winner-mode'
(use-package winner
  :once once-buffer)

once can also be used to do one-time package configuration:

;; Install the fonts for all the icons if they have not been installed once the
;; first GUI frame is created (don't bother installing in tty Emacs or a daemon
;; with only tty frames)
(use-package all-the-icons
  :init
  (defun all-the-icons-maybe-install-fonts ()
    "Install fonts for all the icons if they have not been installed."
    ;; workaround for this functionality not being included by default
    ;; https://github.com/domtronn/all-the-icons.el/issues/120
    (when (not (find-font (font-spec :name "all-the-icons")))
      (all-the-icons-install-fonts t)))
  :once (once-gui #'all-the-icons-maybe-install-fonts))

once also allows more complex, user-defined conditions:

;; A more complex user-defined condition for evil users
(defvar once-my-writable-non-prog-mode-condition
  (list :initial-check (lambda ()
                         (and (once-in-an-evil-insert-state-p)
                              (not (or buffer-read-only
                                       (derived-mode-p 'prog-mode)))))
        ;; different check when the hook runs for illustrative purposes (even if
        ;; though it isn't necessary since `evil-insert-state-entry-hook' runs
        ;; after `evil-state' has already been changed)
        :check (lambda ()
                 (not (or buffer-read-only (derived-mode-p 'prog-mode))))
        :hooks 'evil-insert-state-entry-hook))

;; Set up fcitx once in insert state in a writable non-programming buffer
(use-package fcitx
  :once (my-writable-non-prog-mode-condition
         (fcitx-aggressive-setup)))

Example Setup

Use Package

(eval-and-compile
  (setq use-package-always-defer t))
(eval-when-compile
  (require 'use-package))

(use-package once
  :demand t
  :init
  (require 'once-conditions)
  (setq once-shorthand t))

(use-package once-use-package
  :init
  (eval-and-compile
    ;; must set before loading
    (setq once-use-package-aliases
          '(":once-x-require" ":require-once"
            ":once-require-incrementally" ":require-incrementally")))
  (require 'once-use-package))

Setup.el

(setup (:package once)
  (:require once once-conditions)
  (setq once-shorthand t))

(setup (:package once-setup)
  (eval-and-compile
    ;; must set before loading
    (setq once-setup-keyword-aliases
          '(":once-x-require" ":require-once"
            ":once-require-incrementally" ":require-incrementally")))
  (:require once-setup))

Not Requiring at Load Time

Like use-package, once-use-package and once-setup are not required at load time if you are compiling your init.el. This means you can require them at compile time only (e.g. (eval-when-compile (require 'once-use-package))). Note that if you then will use use-package or setup with any once.el keyword after loading your init.el file (e.g. evaluating a new use-package statement in your init.el or in a scratch buffer), you will need to manually require once-use-package or once-setup. If you do not understand what this means, it is recommended that you use the above configuration instead, which should work in all cases. eval-when-compile here will not save a significant amount of startup time in the first place.

Also 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.

Provided Utilities

eval-after-load Alternatives

Once provides once-eval-after-load, once-with-eval-after-load / once-with as eval-after-load alternatives. The difference is that if a package has already loaded, the once.el versions will not needlessly add anything to after-load-alist. eval-after-load will always add to after-load-alist. See here for some background information. If the package/file has not yet loaded, the once.el versions will also remove from after-load-alist after the file loads.

The functional difference is that with eval-after-load, the given form will run every time the specified file is loaded. With once-eval-after-load, the given form will only run once. This difference usually should not matter, though I think the once.el version is usually what a user wants. For example, if you were incrementally testing some use-package :config block, and added a malformed statement that caused an error, you would get that error every time you loaded the package (e.g. if you updated the package and the re-evaluated the file with the provide call, you would get the error again; the only simple way to fix this is to restart Emacs).

If for whatever reason you do need the original behavior for some situation, just keep using eval-after-load. These alternatives are mainly provided for consistency with once (run some code only once) and because I already had to implement the underlying functionality for once.

once-with-eval-after-load is aliased to once-with. If you have a lot of configuration for a particular package and want to split it up (especially in an org configuration if you want to split package configuration between multiple headings), you can use use-package for the initial setup and use once-with afterwards.

(use-package foo)
;; ...
(once-with 'foo
  (more-configuration))

Once Only Deferred Evaluation

once-x-call

once-x-call is a more flexible way of deferring code/package loading. If using just hooks, advice, or eval-after-load is not good enough, once-x-call can be used to create a condition to run the code that combines them along with various optional checks.

The “once” has two meanings:

  • Run something once some condition is met (hook run OR advised function run OR package load and optional extra checks)
  • Run it only once (unlike normal hooks, normal advice, and eval-after-load)

It is inspired by evil-delay, Doom’s :after-call, Doom’s defer-until!, etc.

The reason it is named once-x-call is to prevent confusion about the arguments. The argument order is the condition followed by functions to call (“once x condition happens, call functions”). It is not named once-call is because this sounds similiar to :after-call (“do something after this hook”), which is not the behavior/argument order provided.

Here is an example of once-x-call being used in place of Doom’s :after-call:

;; This is how Doom loads pyim at the time of writing; I believe Doom has
;; switched to just using `pre-command-hook' for some packages, but the point is
;; that `once-x-call' can use both hooks and advice (whichever runs first)
(use-package pyim
  :after-call after-find-file pre-command-hook
  ;; ...
  )

;; with `once-x-call'
(once-x-call (list :hooks pre-command-hook :before #'after-find-file)
             (lambda () (require 'pyim)))

;; or with `once-shorthand' enabled and :once
(use-package pyim
  :once ((list 'pre-command-hook #'after-find-file)
         (require 'pyim)))

;; or with `once-shorthand' enabled and :require-once
;; quoting is required so that variables can be used for the condition(s)
(use-package pyim
  :require-once 'pre-command-hook #'after-find-file)

Unlike satch-add-hook and satch-advice-add (from satch.el), the functions specified to run for call-x-once should take no arguments.

call-x-once is more generic than :after-call and the other mentioned utilities and can handle most conditions for which you want to load a package or run some code. For more information on specifying conditions, see Condition System. For examples see below and the pre-defined conditions in once-conditions.el.

once

once is a convenience macro over once-x-call that can act as a drop-in replacement for sane invocations. If the first argument is something that could be a function (symbols, functions, variables, and lambdas), all arguments are considered to be functions. Otherwise, all arguments are considered to be body forms to run.
(once-x-call condition #'foo 'bar some-func-in-var (lambda () (require 'baz)))
;; same as
(once condition #'foo 'bar some-func-in-var (lambda () (require 'baz)))

;; body instead of function list
(once condition
  (foo)
  (bar)
  (funcall some-func-in-var)
  (require 'baz))
;; expands to
(once-x-call
 condition
 (lambda ()
   (foo)
   (bar)
   (funcall some-func-in-var)
   (require 'baz)))

Use-package :once

:once is a use-package keyword that just takes a once argument list:
(require 'once-conditions)

(use-package which-key
  :once (once-input #'which-key-mode))

(use-package editorconfig
  :once (once-buffer #'editorconfig-mode))

(use-package magit-todos
  :once ((list :packages 'magit) #'magit-todos-mode))
;; or with `once-shorthand'
(use-package magit-todos
  :once ("magit" #'magit-todos-mode))

You can specify as many argument lists as you want to:

(use-package foo
  :once
  ('bar-hook #'foo-mode)
  ('baz-hook #'foo2-mode))

When once-shorthand is non-nil, you can use shorthand to infer a function (i.e. the minor mode to enable):

;; enable `which-key-mode' on the first input
(use-package which-key
  :once 'pre-command-hook)
;; or
(require 'once-conditions)
(use-package which-key
  :once once-input)

(use-package editorconfig
  :once once-buffer)

(use-package magit-todos
  :once "magit")

Note that any list (other than (quote some-hook) or (function some-function)) will considered to be an argument list. You cannot do this:

;; WRONG!
(use-package editorconfig
  :once (some-function-that-generates-a-condition-list))


;; Ok
(use-package editorconfig
  :once ((some-function-that-generates-a-condition-list) #'editorconfig-mode))

;; Ok
(use-package editorconfig
  ;; argument list with inferred function (`editorconfig-mode')
  :once ((some-function-that-generates-a-condition-list)))

Setup.el :once

:once is a setup.el keyword that just takes a once argument list:
(require 'once-conditions)

(setup (:package which-key)
  (:once once-input #'which-key-mode))

(setup (:package editorconfig)
  (:once once-buffer #'editorconfig-mode))

(setup (:package magit-todos)
  (:once (list :packages 'magit) #'magit-todos-mode))
;; or with `once-shorthand'
(setup (:package magit-todos)
  (:once "magit" #'magit-todos-mode))

It is not a repeating keyword. All arguments after the first are considered to be functions or forms to run once the condition is met (exactly like once). If you need to do something based on a different condition, specify :once again:

(use-package (:package foo)
  (:once condition1 #'func1 #'func2)
  (:once condition2 #'func3 #'func4))

The benefit over just using (once ...) is that when once-shorthand is non-nil, you can use shorthand to infer a function (i.e. the minor mode to enable):

;; enable `which-key-mode' on the first input
(setup (:package which-key)
  (:once 'pre-command-hook))
;; or
(require 'once-conditions)
(setup (:package which-key)
  (:once once-input))

(setup (:package editorconfig)
  (:once once-buffer))

(setup (:package magit-todos)
  (:once "magit"))

Condition System Details

At least one of :hooks, :packages (a.k.a. :files), :variables (a.k.a. :vars), or the advice keywords should be specified. When more than one is specified, any of them can trigger the condition (the behavior is OR not AND). The check keywords are optional.

:hooks

:hooks should be 1 hooks that can trigger the function(s) to run.
;; call `cl-lib-highlight-initialize' the first time `emacs-lisp-mode-hook' runs
(once (list :hooks 'emacs-lisp-mode-hook) #'cl-lib-highlight-initialize)
;; or with `once-shorthand' enabled
(once 'emacs-lisp-mode-hook #'cl-lib-highlight-initialize)

:packages

:packages (or :files, which is an alias) should be 1 files or features (i.e. either a string or symbol just like the FILE argument to eval-after-load) that can trigger the function(s) to run when loaded.
;; load yasnippet-snippets as soon as yasnippet is loaded
(once (list :packages 'yasnippet)
  (require 'yasnippet-snippets))
;; or if `once-shorthand' is enabled
(once "yasnippet"
  (require 'yasnippet-snippets))

advice

Any WHERE keyword (e.g. :before) can be used to specify advice:
;; enable `global-hardhat-mode' after finding a file
(once (list :before #'after-find-file) #'global-hardhat-mode)
;; or if `once-shorthand' is enabled
(once #'after-find-file #'global-hardhat-mode)

:variables

:variables (or :vars, which is an alias) should be 1 variables that can trigger the function(s) to run when set. Local checks can be used to only trigger the function(s) when a variable is set to a specific value.

:check

:check can be specified as a function to run to determine whether to run now. It will be passed no arguments.

When a check is given and it returns non-nil, the code will be run immediately. Otherwise, it will be delayed. Then later when a hook runs, package loads, or advised symbol is called, the check will run again to determine whether to run the delayed code now or continue to wait.

;; run `unicode-fonts-setup' for the first GUI frame
(once (list :check #'display-graphic-p
            :hooks 'server-after-make-frame-hook)
  (unicode-fonts-setup))

If no check is given, the code to run will always be delayed. The only exception is if you use :packages and a package has already been loaded. If no :check is given and any specified package has loaded, the code will run immediately. On the other hand, if :check is specified and fails initially, the code will always be delayed even if one of the packages has already loaded. In that case, some other method (a different package load or a hook or advice) will have to trigger later when the :check returns non-nil for the code to run.

:initial-check

:initial-check is like :check but happens only at the beginning to determine whether to initially delay or run the code. When both are specified, :check only applies later. If you always want to delay initially and but have a check later, you can use :check #'some-check :initial-check (lambda () nil).
;; this is not the most practical example but should illustrate that different
;; checks can potentially make sense
(once (list :initial-check (lambda () after-init-time)
            :hooks 'after-init-hook)
  (column-number-mode)
  (size-indication-mode))
;; or
(require 'once-conditions)
(once-init
  (column-number-mode)
  (size-indication-mode))

Local Checks

Having checks for individual triggers is also possible:
(list :hooks (list 'some-hook #'some-check) 'no-check-hook
      :before (list #'some-fun #'another-check) #'no-check-fun
      :packages (list 'evil #'yet-another-check) 'no-check-package)

Unlike :check and :initial-check, local checks are passed the arguments the hook (in the case of run-hook-with-args) or advised symbol was run with. For :variables, the local check is passed the arguments SYMBOL NEWVAL OPERATION WHERE (just like the arguments to WATCH-FUNCTION for add-variable-watcher). Packages can take a local check, but it won’t be passed any arguments and may not be useful.

Local checks allow mimicking the behavior of evil-delay and Doom’s defer-until!.

(once (list :hooks (list 'after-load-functions
                         (lambda (&rest _)
                           (boundp 'evil-normal-state-map))))
  (define-key evil-normal-state-map ...))
;; better to just do this; `once' is not needed
(once-with 'evil
  (define-key evil-normal-state-map ...))

For use cases, search how evil-delay and defer-until! are used (e.g. in Doom and on github in general). Hooks you might use would be after-load-functions and post-command-hook. You probably will not normally need this functionality, but it is there if you do need it.

Condition Shorthand

It is generally recommended that you store more complex conditions in a reusable, named variable using the full condition syntax like once-conditions.el does. However, it is possible to use shorthand for more simple cases. This means you do not need to explicitly specify :hooks, :before (and other advice keywords), or :packages.

The type will be inferred as follows:

  • Packages must be specified as strings (file name not feature name)
  • Hooks must be symbols ending in -hook or -functions
  • All other symbols will be considered to be functions to advise :before

Any keyword arguments (:check and :initial-check) must be specified last.

To enable shorthand, set once-shorthand to non-nil. By setting this, you confirm that you understand how the inference works (e.g. you should not be surprised if you try (once '(evil ...) ...) and it does not work).

(setq once-shorthand t)
(once (list #'foo 'bar-mode-hook "some-file") ...)
(once "evil" ...)

once-x-require

once-x-require is a more limited version of once that will require a package once some condition “x” is met. It is a more generic version of Doom’s :after-call:
;; require forge after magit loads
(once-x-require (list :packages 'magit) 'forge)

When once-shorthand is enabled, you can also use shorthand:

(once-x-require "magit" 'forge)

Any number of features to require can be specified:

(once-x-require "magit" 'forge 'magit-todos ...)

Use-package :once-x-require / :require-once

It is recommend you alias :once-x-require to :require-once using once-use-package-aliases.

The keyword takes any number of once-x-require argument lists:

(use-package forge
  :require-once ((list :packages 'magit) 'forge))
;; or
(use-package forge
  :require-once ((:after #'magit-status) 'forge))

;; with `once-shorthand' enabled
(use-package forge
  :require-once ("magit" 'forge))
;; or
(use-package forge
  ;; :before advice unlike above (though order is not really important in this
  ;; case)
  :require-once (#'magit-status 'forge))

The benefit over using once-x-require directly is that the package to require can be inferred:

(use-package forge
  :require-once ("magit"))
;; or just
(use-package forge
  :require-once "magit")

The same caveat as for :once applies. All lists (except a quoted symbol or function) are considered argument lists, so this is invalid:

You can use nil in place of the package name if you want to additionally require some other extension:

(use-package foo
  :require-once condition
  :require-once (condition 'foo-extension))
;; or just
(use-package foo
  :require-once (condition nil 'foo-extension ...))

Setup.el :once-x-require / :require-once

It is recommend you alias :once-x-require to :require-once using once-setup-keyword-aliases.

The keyword takes any number of once-x-require argument lists:

(setup (:package forge)
  (:require-once (list :packages 'magit) 'forge))
;; or
(setup (:package forge)
  (:require-once (:after #'magit-status) 'forge))

;; with `once-shorthand' enabled
(setup (:package forge)
  (:require-once "magit" 'forge))
;; or
(setup (:package forge)
  ;; :before advice unlike above (though order is not really important in this
  ;; case)
  (:require-once #'magit-status 'forge))

The benefit over using once-x-require directly is that the package to require can be inferred:

(setup (:package forge)
  (:require-once "magit"))

You can use nil in place of the package name if you want to additionally require some other extension:

(setup (:package foo)
  (:require-once condition)
  (:require-once condition 'foo-extension))
;; or just
(setup (:package foo)
  (:require-once condition nil 'foo-extension ...))

Note that the keyword is not repeatable. If you want to specify a different condition, you will have to use a different :require-once:

(setup (:package foo)
  (:require-once condition 'foo 'foo-extension ...)
  (:require-once condition2 'foo-extension2 ...))

:require-after, :once-call, etc.

I have not added these but am considering them. :require-after would be like :require-once but only support packages. The difference from :after <package> :demand t would be that :init would still run immediately, and all it would do would be to require the package. It probably also would not support a complex condition system.
;; instead of this
(use-package tree-sitter-langs
  :after tree-sitter
  :demand t)
; this
(use-package tree-sitter-langs
  :require-after tree-sitter)

Whether this justifies a new keyword just to avoid the need to have quotes, I’m not sure.

:once-call would just be like Doom’s :after-call. It would disallow packages and only accept hooks/functions (and no variables), which would allow not needing to quote them. Again, I’m not sure this is worth a new keyword that is far more limited than the others (only supports hooks/functions and does not support variables for conditions).

Pre-defined Conditions

For some predefined once conditions, (require 'once-conditions). All conditions come with a corresponding macro and variable of the same name, e.g. once-gui.

once-gui and once-tty

once-gui and once-tty can be used to run some code once once the first graphical or terminal frame is created (now if the current frame already is). Here is an example use case:
(require 'once-conditions)

(use-package clipetty
  :init
  ;; only need to load if create a terminal frame
  ;; `global-clipetty-mode' will not cause issues if enabled for a server with
  ;; both graphical and terminal frames
  (once-tty (global-clipetty-mode)))

;; or
(use-package clipetty
  :once (once-tty #'global-clipetty-mode))

Similarly, some packages are only needed or only work in graphical frames, which is the use case of once-gui.

once-init

This basic condition will run after Emacs initialization has finished (now if it already has).
(once-init
  (column-number-mode)
  (size-indication-mode))

Most of the time, you should use a more specific condition.

once-buffer

This condition will activate when switching buffers or after finding a file (the first time the current buffer changes after init). It is the equivalent of using Doom’s doom-first-buffer-hook or on-first-buffer-hook from on.el but is implemented using once rather than creating a new hook.
(once-buffer (winner-mode))
;; or
(use-package winner
  :once once-buffer)

once-file

This condition will activate when finding the first file or opening dired. It is the equivalent of using Doom’s doom-first-file-hook or on-first-file-hook from on.el but is implemented using once rather than creating a new hook.
(once-file (recentf-mode))
;; or
(use-package recentf
  :once once-file)

once-writable

This condition is similar to once-file but only activates in a writable buffer.

once-evil-insert-and-writable

This condition is similar to once-file but only activates in a writable buffer when entering an evil “insert” state. Which states are considered insert states can be changed by customizing once-evil-insert-states (insert and emacs by default).

once-require-incrementally

Not yet implemented.

Use-package :once-require-incrementally / :require-incrementally

Setup.el :once-require-incrementally / :require-incrementally

About

Extra init.el deferred evaluation utilities

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published