doom-emacs/docs/examples.org
Henrik Lissner f3edcb9f3d
refactor(emacs-lisp): elisp-demos: reorganize Doom demos
- Move Doom core elisp API demos out of docs/examples.org into lisp/demos.org.
- Recognize and search demos.org file in modules for additional
  demos (including $DOOMDIR/demos.org).
- Refactor emacs-lisp module to use new elisp-demos-user-files variable
  instead of an advice. This way, elisp-demo's commands (such as
  `elisp-demos-find-demo` and `elisp-demos-add-demo`) will include
  Doom's demos.
2024-05-21 17:16:37 +02:00

12 KiB
Raw Blame History

Examples

󰐃 Our documentation was designed to be read in Doom Emacs (M-x doom/help) or online at https://docs.doomemacs.org. Avoid reading it elsewhere (like Github), where it will be rendered incorrectly.

Introduction

Examples speak louder than technical explanations, so this file exists to house examples of Doom's (and Emacs') concepts, libraries, dotfiles, and more, for your own reference. They are divided into Emacs-specific and Doom-specific examples; where the former can also be useful to users who don't use Doom.

Some of Doom's components will read this file to generate documentation for you, for example:

If you're interested in adding to this document, read the documentation section of our contributing guide first.

TODO Emacs

This section is dedicated to examples of concepts and libraries that can benefit all Emacs users, whether or not they use Doom.

TODO Templates

TODO Emacs package

TODO Dynamic module

TODO Doom Emacs

This section is dedicated to examples of concepts and libraries only relevant to Doom and its users. These are intended to be demonstrations, not substitutes for documentation.

TODO Configuration files

profiles.el

This file can live in any of:

  • $DOOMDIR/profiles.el
  • $EMACSDIR/profiles.el
  • ~/.config/doom-profiles.el
  • ~/.doom-profiles.el

Here is an exhaustive example of all its syntax and capabilities:

;; -*- mode: emacs-lisp; -*-
((profile1
  ;; The permitted formats of each entry:
  (var . value)
  ("envvar" . value)
  (var :directive values...)

  ;; `user-emacs-directory' is often the first variable you want to set, so
  ;; Emacs knows where this profile lives. If you don't, it'll use the config
  ;; living in the default locations (~/.config/emacs or ~/.emacs.d).
  (user-emacs-directory . "~/another/emacs/config/")
  ;; If this is a Doom config, you'll also want to set `doom-user-dir', which
  ;; defaults to ~/.config/doom or ~/.doom.d:
  (doom-user-dir . "~/another/doom/config/")
  ;; If a CAR is a string, it is assumed you want to set an environment
  ;; variable. (Side-note: setting DOOMDIR will be unnecessary if you're setting
  ;; `doom-user-dir' above).
  ("DOOMDIR" . "~/another/doom/config/")

  ;; Doom profiles support a number of special directives. They are:
  ;;
  ;; (VAR :path SEGMENTS...) -- set VAR to an exapnded path built from SEGMENTS,
  ;; relative to `user-emacs-directory', unless an absolute path is in SEGMENTS.
  (doom-cache-dir :path doom-user-dir ".local/cache")
  (doom-data-dir  :path doom-user-dir ".local/data")
  (doom-state-dir :path doom-user-dir ".local/state")
  ;; (VAR :plist VALUE) -- use VALUE as a literal plist; ignoring any profile
  ;; directives that may be in it.
  (some-plist :plist (:foo bar :baz womp))
  ;; (VAR :eval FORMS...) -- use to evaluate arbitrary elisp forms. Note that
  ;; his runs early in early-init.el. It's wise to assume no APIs are available
  ;; or loaded, only the previous bindings in this profile.
  (doom-theme :eval (if (equal (system-name) "foo") 'doom-one 'doom-dracula))
  ;; Though discouraged, you may evaluate forms without a binding by using `_'.
  ;; You really should be doing this in the profile though...
  (_ :eval (message "Hello world!"))
  (_ :eval (with-eval-after-load 'company (setq-default company-idle-delay 2.0)))
  ;; (VAR :prepend FORMS...) or (VAR :append FORMS...) -- prepend or append the
  ;; evaluated result of each form in FORMS to VAR (a list). If VAR is undefined
  ;; at startup, it will be deferred until the variable is available.
  (load-path :prepend (expand-file-name "packages/" doom-user-dir))
  (load-path :prepend (expand-file-name "lisp/" doom-user-dir))
  (load-path :append  (expand-file-name "fallback/" doom-user-dir))
  (exec-path :prepend (expand-file-name "bin/" doom-user-dir))
  (auto-mode-alist :prepend '("\\.el\\'" . lisp-mode)))

 (profile2
  ...)

 (profile3
  ...))

.doomprofile

This file takes after the second level of profiles.el's format (see a more complete example in the previous section). For example:

;;; -*- mode: emacs-lisp -*-
;; A .doomprofile can be placed under an implicit profile. Same rules as
;; .doom-profiles.el, but one level deeper.

((var . value)
 ("envvar" . value)
 (var :directive values...))

TODO .doomrc

TODO .doomproject

TODO .doommodule

TODO Templates

TODO User configuration

TODO Module

TODO Project

TODO Theme

TODO Command-line interface

Unix utilities, rewritten as Doom scripts

To show off the syntax and capabilities of Doom's CLI framework, here are some popular scripts ported to doomscripts for reference. They will all operate under these assumptions:

  1. The script lives somewhere in your $PATH,
  2. $EMACSDIR/bin/doomscript lives in your $PATH.
  3. The script is executable,
  4. The script's filename matches the first argument of run! (by convention, not a requirement),
mkdir
#!/usr/bin/env doomscript

(defcli! mkdir
    ((mode     ("-m" "--mode" mode))
     (parents? ("-p" "--parents"))
     (verbose? ("-v" "--verbose"))
     &args directories)
  "Create the DIRECTORIES, if do not already exist.

Mandatory arguments to long options are mandatory for short options too.

OPTIONS:
  -m, --mode
    set file mode (as in chmod), not a=rwx - umask.
  -p, --parents
    no error if existing, make parent directories as needed, with their file
    modes unaffected by any `-m' option.
  -v, --verbose
    print a message for each created directory

AUTHOR:
  Original program by David MacKenzie. Doomscript port by Henrik Lissner.

SEE ALSO:
  `mkdir(2)`

  Full documentation <https://www.gnu.org/software/coreutils/mkdir>
  or available locally via: info '(coreutils) mkdir invocation'

  Packaged by https://nixos.org
  Copyright © 2022 Free Software Foundation, Inc.
  License  GPLv3+:  GNU  GPL  version 3 or later <https://gnu.org/li
  censes/gpl.html>.
  This is free software: you are free to change and redistribute it.
  There is NO WARRANTY, to the extent permitted by law."
  (dolist (dir directories)
    (unless (file-directory-p dir)
      (make-directory dir parents?)
      (when mode
        (set-file-modes dir mode))
      (when verbose?
        (print! "mkdir: created directory '%s'" dir)))))
Notes
  • Docstrings for Doom CLIs recognize indented sections with a capitalized heading followed by a colon (like SEE ALSO:, OPTIONS:, etc). They will be appended to the help output for this command. OPTIONS and ARGUMENTS are special, in that they decorate pre-existing documentation for referenced options/arguments.
  • The options were documented in the CLI's docstring, instead of inline like so:

        ((mode     ("-m" "--mode" mode) "set file modes (as in chmod), not a=rwx - umask.")
         (parents? ("-p" "--parents") "no error if existing, make parent directories as needed, with their file modes unaffected by any `-m' option.")
         (verbose? ("-v" "--verbose") "print a message for each created directory")
         &args directories)

    Either is acceptable, but for long docs like this, it's better suited to the docstring. If both were present, Doom's help docs would have concatenated them (separated by two newlines).

  • The mode option takes one argument, a chmod mask. I indicate this with "`MODE'". This is a special syntax for highlighting arguments in the help docs of this command. If I had used a symbol, instead (one of the predefined types in doom-cli-argument-value-types), I would've gotten free type-checking and error handling, but there is no predefined type for chmod masks (yet), so I'd have to do my own checks:

    (defcli! mkdir
        ((mode     ("-m" "--mode" "`MODE'"))
         (parents? ("-p" "--parents"))
         (verbose? ("-v" "--verbose"))
         &args directories)
      (unless (string-match-p "^[0-9]\\{3,4\\}$" mode)
        (user-error "Invalid mode: %s" mode))
      (setq mode (string-to-number mode 8))
      (dolist (dir directories)
        (unless (file-directory-p dir)
          (make-directory dir parents?)
          (when mode
            (set-file-modes dir mode))
          (when verbose?
            (print! "mkdir: created directory '%s'" dir)))))

    That said, set-file-modes will throw its own type error, but it likely won't be as user friendly.

TODO say
#!/usr/bin/env doomscript

(defcli! say
    ((name ("--speaker" name) "Who is speaking?")
     &args args)
  "This command repeats what you say to it.

It serves as an example of the bare minimum you need to write a Doom-based CLI.
Naturally, it could be more useful; it could process more complex options and
arguments, call other Doom CLIs, read/write data from files or over networks --
but that can wait for more complicated examples.

ARGUMENTS:
  ARGS
    The message to be repeated back at you.

OPTIONS:
  --speaker
    If not specified, it is assumed that Emacs is speaking."
  (print! "%s says: %S"
          (or name "Emacs")
          (string-join args " ")))

(run! "say" (cdr (member "--" argv)))
$ say hello world
Emacs says: "Hello world"
$ say --speaker Henrik "I've doomed us all"
Henrik says: "I've doomed us all"
$ say --help
TODO
emacs

This isn't useful, but it should hopefully demonstrate the full spectrum of Doom's CLI, by reimplementing a subset of emacs's options and arguments (and none of its documentation). It will simply forward them to the real program afterwards.

Since I don't want to override the real emacs in the $PATH, I'll just call it demacs:

#!/usr/bin/env doomscript

(defcli! demacs
    ((cd ("--chdir" dir))
     (quick?     ("-Q" "--quick"))
     (no-init?   ("-q" "--no-init-file"))
     (no-slisp?  ("-nsl" "--no-site-lisp"))
     (no-sfile?  ("--no-site-file"))
     (initdir    ("--init-directory" dir))
     (batch?     ("--batch"))
     (batch      (("-l" "--load" (file) ...))
                 (("-e" "--eval" (form) ...))
                 (("-f" "--funcall" (fn) ...))
                 (("-L" "--directory" (dir) ...))
                 (("--kill")))
     (script     ("--script" (file)))
     &args (args (file linecol)))
  "Demacs is a thin wrapper around Emacs, made to demo of Doom's CLI Framework.

Since documentation isn't the focus of this example, this is all you'll get!"
  (cond (script (load script))
        (batch?
         (dolist (do batch)
           (pcase do
             (`(,(or "-l" "--load") . ,file) (load file))
             (`(,(or "-e" "--eval") . ,form) (eval (read form) t))
             (`(,(or "-f" "--funcall") . ,fn) (funcall (read fn)))
             (`("--kill" . t) (kill-emacs 0)))))
        ((exit! :then (cons "emacs"
                            (append (if quick '("-Q"))
                                    (if no-init? '("-q"))
                                    (if no-slisp? '("-nsl"))
                                    (if no-sfile? '("--no-site-file"))
                                    (if initdir `("--init-directory" ,initdir))
                                    args))))))
Notes

There's a lot of (intentional) redundancy here, for posterity. A much simpler (and more reliable) version of this command would've looked like this:

(defcli! demacs (&rest args)
  (exit! :then (cons "emacs" args)))

But that wouldn't demonstrate enough. Though, it wouldn't forward --version or --help either.

TODO Use cases

TODO Note-taking

TODO Game development

TODO Web development

TODO Emacs as your terminal