github soundcloud
Doom Emacs config
HTML version
const.no/init (courtesy of ox-hugo and hugo)
Git repo
github.com/staticaland/doom-emacs-config

About Doom

I switched to Doom from my bespoke config. My main takeaways:

  • Doom is very well organized and is easy to reason about (easier than Spacemacs)
  • Doom hides away unneeded complexity for the most common use-cases
  • I prefer Doom over my previous handcrafted config

Someone with a lot more experience than me put it well:

After 30+ years of using Emacs, I’m more than happy to use a distro with a well curated set of built-in packages, and a clear set of config mechanisms. Also the startup time! I was happy with my handcrafted config when I got <10s, with Doom I get ~1.5s and a lot more functionality ready to go. Amazing.

When you own a car, you get a sense of freedom. Even though you theoretically could travel everywhere without one, you don’t. Why? To me, Emacs is like a car while something like VSCode is like a bus. It limits you on a subconscious level that is hard to even notice.

Some people really enjoy riding a good car and is motivated by the thrill. Seeing the task at hand getting done extremely fast is inherently motivating. Doom Emacs happens to be the best car I know. Especially when you ride hours every day. It’s like using Iron Man’s powered exoskeleton.

I sense there’s two types of developers. Some focus on creating products. The others love seeing machines do the work that humans shouldn’t have to do.

I do not buy excuses for using poor tools. The only thing it reveals to me is an inability to sense limitations. I automate to save mental energy, not time. Luke Smith also talks about these concepts in «Are You Just TOO SMART to Learn Anything?». To quote John D. Cook:

I suspect the time I’ve put into learning some features of Emacs, for example, will not pay for itself in terms of time invested versus time saved. But I’ve invested leisure time to save time when I’m working hard, not to save keystrokes but to save mental energy for the project at hand.

Besides, you have The Magical Number Seven, Plus or Minus Two:

The number of objects an average human can hold in short-term memory is 7 ± 2

I optimize to keep this in check.

Studies on highly successful people have proven again and again that success is not the result of strong willpower and the ability to overcome resistance, but rather the result of smart working environments that avoid resistance in the first place.

Instead of struggling with adverse dynamics, highly productive people deflect resistance, very much like judo champions. This is not just about having the right mindset, it is also about having the right workflow. It is the way Luhmann and his slip-box worked together that allowed him to move freely and flexibly between different tasks and levels of thinking. It is about having the right tools and knowing how to use them – and very few understand that you need both.

Anyway, back to Doom. Doom’s configuration framework is well thought out!

Doom uses straight.el and general.el under the hood. They are actually hiding behind package! and map!. One improvement over my config is that straight.el is used outside of Emacs itself - with the doom install CLI command. I like this a lot - especially since I would never bother to do that myself.

It seems easier to pin packages with Doom. I just use the package! with the :pin property. I was never a big fan of how straight-freeze-versions and straight-thaw-versions worked or how pinning packages is described in the straight.el README. I assume Doom does something clever to keep everything under control. To bump a package pin, run doom/bump-package-at-point while the cursor is on a package! expression. Again, something I’d never implement myself.

As always, use-package is used for configuring packages. Doom has its own version (use-package!) that will avoid loading disabled packages. However, I am not really able to use the :general property as before so I opted to combine :init with map!. Although one might argue that :map should not be a part of the use-package! declaration if you consider +evil-bindings.el. Perhaps you should rather create your own config module. The downside to that is that you could have keybindings to packages that may not be installed.

use-package! is a thin wrapper around use-package it is required that you use this in Doom’s modules, but not required to be used in your private config

It is important to note that omitting :defer, :hook, :commands or :after will make your package load immediately. This is bad for Doom’s startup time.

I like how !map hides away the complexities of general.el when you want to use leader keys. You get sane defaults with a pleasant wrapper.

I am unsure how to best utilize the after! function for configuring packages. I may have to move things away from use-package!’s :config property to after! instead.

The after! macro just expands to eval-after-load.

It’s pretty cool to have a community around your Emacs configuration. There is a friendly and active Discord community.

Hop on our Discord server and say hi! Help others, hang out or talk to me about Emacs, gamedev, programming, physics, pixel art, anime, gaming – anything you like. Nourish this lonely soul.

I have learnt a lot from in a short time by following the discourse.

Discoveries

aya-create with aya-expand is very useful:

count_of_~red = get_total("~red");

aya-persist-snippet wil save whatever is currently in aya-current.

org-re-reveal is set up to download the JS files locally. Great! Making presentations are a breeze now.

#+REVEAL_EXTRA_CSS: url-to-custom-stylesheet.css

git-timemachine (g t) is nice. C-j and C-k to browse the revisions.

What is a module?

A module is something you can tell Doom to load by listing the module name in init.el.

A module can have a packages.el and a config.el, so: only use package! in the former and use-package! in the latter. use-package! is formerly known as def-package! (which is deprecated).

Doom consists of around 160 modules and growing. A Doom module is a bundle of packages, configuration and commands, organized into a unit that can be toggled easily by tweaking your doom! block (found in $DOOMDIR/init.el).

You can think of your $DOOMDIR kind of like a Doom module.

package! is declarative: meant only for telling Doom what this module’s dependencies are (and optionally, where to find them).

!map is a convenience macro for defining keybinds, powered by general.el.

The canonical way to reconfigure packages is with (after! PACKAGE ...).

Base settings

Make this file run (slightly) faster with lexical binding (see this blog post for more info).

;;; config.el -*- lexical-binding: t; -*-

Personal Information

It’s useful to have some basic personal information

(setq user-full-name "Anders K. Pettersen"
      user-mail-address "john@doe.com")

Line numbers…

(setq display-line-numbers-type nil)

Modus themes

I prefer light themes when working and writing.

(setq doom-theme 'modus-operandi)

Base typeface configurations

Variable pitch means that the distance between characters can vary, as opposed to fixed pitch. The small features on the ends of strokes in some fonts are known as serifs. It is typically what is used for printed text.

(setq doom-themes-treemacs-enable-variable-pitch nil)

(when (eq system-type 'darwin)
  (setq doom-font (font-spec :family "Menlo" :size 14))
  (setq doom-big-font (font-spec :family "Menlo" :size 36))
  (setq doom-big-font-increment 4)
  (setq doom-serif-font (font-spec :family "EtBembo"))
  (setq doom-variable-pitch-font (font-spec :family "EtBembo"))
  (font-put doom-font :weight 'semi-light))

On Linux, I just go with the defaults for now.

(when (eq system-type 'gnu/linux)
  (set-fontset-font t 'symbol "Noto Color Emoji" nil 'append))

General movements and motions

Evil

(use-package! evil
  :init
  (setq evil-want-Y-yank-to-eol t))

Evil-snipe

Zaiste Programming has a nice video about Evil-snipe.

(after! evil-snipe
  (setq evil-snipe-scope 'visible))

Try it here with f followed by ; and , to traverse matches:

Alice took up the fan and gloves, and, as the hall was very hot, she kept fanning herself all the time she went on talking: `Dear, dear! How queer everything is to-day! And yesterday things went on just as usual. I wonder if I’ve been changed in the night? Let me think: was I the same when I got up this morning? I almost think I can remember feeling a little different. But if I’m not the same, the next question is, Who in the world am I? Ah, that’s the great puzzle!' And she began thinking over all the children she knew that were of the same age as herself, to see if she could have been changed for any of them.

I think avy-goto-char-2 is better though (g s s). Speaking of jumping around, SPC s s is a fine command for invoking Swiper. Most of the times I am looking for a file in a project, I should probably use SPC s p instead of using SPC p f.

Directory, buffer, window management

Projectile project management

I want to always create a new (or matching existing) project when switching to a project.

I also want to ignore the package repositories cloned by straight.el. Having a lot of projects seem to slow down Emacs.

(after! projectile
  (setq +workspaces-on-switch-project-behavior t)

  (setq projectile-ignored-projects '("~/" "/tmp" "~/.emacs.d/.local/straight/repos/"))
  (defun projectile-ignored-project-function (filepath)
      "Return t if FILEPATH is within any of `projectile-ignored-projects'"
      (or (mapcar (lambda (p) (s-starts-with-p p filepath)) projectile-ignored-projects))))

Dired file explorer

(after! dired

  (add-hook! 'dired-mode-hook 'dired-hide-details-mode)
  (add-hook! 'dired-mode-hook 'hl-line-mode)

  (if (executable-find "gls")
      (progn
        (setq insert-directory-program "gls")
        (setq dired-listing-switches "-lFaGh1v --group-directories-first"))
    (setq dired-listing-switches "-ahlF"))

  (setq ls-lisp-dirs-first t)

  (setq dired-listing-switches "-lat") ; sort by date (new first)
  (put 'dired-find-alternate-file 'disabled nil)

  (setq delete-by-moving-to-trash t)
  (setq dired-dwim-target t)
  (setq dired-recursive-copies (quote always))
  (setq dired-recursive-deletes (quote top)))

(use-package! dired-narrow
  :after dired
  :config
    (map! :map dired-mode-map
      :n  "/" 'dired-narrow-fuzzy))

(use-package! dired-open
  :after dired
  :config
  (setq open-extensions
      '(("webm" . "mpv")
        ("avi" . "mpv")
        ("mp3" . "mpv")
        ("mp4" . "mpv")
        ("m4a" . "mpv")
        ("mkv" . "mpv")
        ("ogv" . "mpv")
        ("pdf" . "zathura")))
    (setq dired-open-extensions open-extensions))

Treemacs file tree

(use-package! treemacs
  :commands treemacs
  :init
    (map! :leader
      (:prefix ("f" . "file")
        :desc "Open Treemacs" "t" #'+treemacs/toggle))
  :config
  (treemacs-git-mode 'extended)
  (setq treemacs-is-never-other-window nil)
    (add-to-list 'treemacs-pre-file-insert-predicates #'treemacs-is-file-git-ignored?))

General interface and interactions

Font locks and faces

Working with faces…

(set-face-attribute 'cursor nil :background "red")

Theme

(use-package! modus-operandi-theme
  :defer
  :init
  (setq modus-operandi-theme-scale-headings t)
  (setq modus-operandi-theme-diffs 'desaturated)
  (setq modus-operandi-theme-intense-paren-match t))

Focus

Dim the font color of text in surrounding sections. I like to use this on chaotic Terraform code.

(use-package focus
  :hook terraform-mode
  :config
  ;; Modes inheriting prog-mode will focus on functions.
  (add-to-list 'focus-mode-to-thing '(prog-mode . defun))
  ;; Modes inheriting text-mode will focus on sentences.
  (add-to-list 'focus-mode-to-thing '(text-mode . sentence))
  ;; Terraform
  (add-to-list 'focus-mode-to-thing '(terraform-mode . paragraph)))

Window splits

(setq evil-vsplit-window-right t)
(setq evil-split-window-below t)

(defadvice! prompt-for-buffer (&rest _)
  :after '(evil-window-split evil-window-vsplit)
  (+ivy/switch-workspace-buffer))

The Mode Line

I use the +light Doom mode line.

Outline

I first learned about outline-minor-mode from Emacs: outline-minor-mode and imenu.

imenu is a very simple package that builds index of interesting positions in the current buffer and presents them as a menu. You pick the item and the point moves there. There is a built-in interface and also one in sallet, helm or counsel.

In Doom, it is bound to SPC s i by default (counsel-imenu).

The most common way to add items to the index is by modifying imenu-generic-expression which is a list of lists of the form (GROUP-NAME REGEX MATCH-GROUP). Then imenu searches for the REGEX and adds the corresponding MATCH-GROUP and its match position to the index. This is done by imenu-default-create-index-function which is the default value of imenu-create-index-function.

https://tychoish.com/post/imenu-for-markdown-and-writing/

The form is (nil "regex" 1).

In Python, PEP8 style guidelines recommend two lines between functions (see blank lines). I agree that this makes the code easier to navigate (with the eyes). Therefore it makes little sense for outline-minor-mode to remove this spacing. By setting outline-blank-line to non-nil I get at least one line of space between the headings. I do not know how to add more space.

(use-package! outshine)

(use-package! outline
  :config
  (setq outline-blank-line t))

(use-package! outline-minor-faces
  :after outline
  :config (add-hook 'outline-minor-mode-hook
                    'outline-minor-faces-add-font-lock-keywords))

(use-package backline
  :after outline
  :config (advice-add 'outline-flag-region :after 'backline-update))

Related concepts:

The number of objects an average human can hold in short-term memory is 7 ± 2

Finally, pretty-outlines for prettier ellipses. I did not get the pretty bullets working. Some variants I have tried:

Symbol Description
+ Plus
Bullet
Horizontal ellipsis
Rightwards arrow with corner downwards
Midline horizontal ellipsis
Black down-pointing small triangle
White down-pointing small triangle
White bullet
Arrow pointing rightwards then curving downwards
Arrow pointing downwards then curving rightwards
Right-side arc clockwise arrow
Rightwards arrow with tip downwards
Lightning

https://github.com/integral-dw/org-superstar-mode#where-do-i-find-utf8-bullets-to-use

(use-package! pretty-outlines
  :config
  (setq pretty-outlines-ellipsis " ↴")
  ;; (setq pretty-outlines-bullets-bullet-list '("⁖"))
  :hook (outline-minor-mode . pretty-outlines-set-display-table))

Bicycle

Bicycle provides commands for cycling the visibility of outline sections and code blocks.

(use-package! bicycle
  :config
  (map! :map outline-minor-mode-map
        :n "<tab>" #'bicycle-cycle
        :n "<backtab>" #'bicycle-cycle-global))

By running M-x macrostep-expand over the map! call you will get this:

(general-define-key :states 'normal :keymaps
		    '(outline-minor-mode-map)
		    "<tab>"
		    (function bicycle-cycle)
		    "<backtab>"
		    (function bicycle-cycle-global))

Proving that the map! macro is syntactic sugar for general.el (see macrostep). Also see Doom naming conventions for rationale behind the use of the exclamation point.

Outline for Python code

Note: I disabled outline for Python for now. It caused too much trouble. Leaving the config here for posterity.

Here I set up a outline-regexp for python-mode.

A great tip for rx is to place the cursor at the last parenthesis and do C-x C-e (eval-last-sexp) to see what regex is being produced. To get Perl Compatible Regular Expressions you can do counsel--elisp-to-pcre on the regex string. Now you can explore it with regex101.com. In the same vein, pcre2el is probably worth checking out.

(add-hook 'outline-minor-mode-hook
          (defun contrib/outline-overview ()
              (outline-show-all)
              (outline-hide-body)))

(add-hook 'org-src-mode-hook
          (defun const/show-all-outlines-in-org-src ()
            (outline-show-all)))

(defun python-mode-outline-hook ()
  "Fold only definitions in Python."
  (setq-local outline-regexp
        (rx (or
             ;; Definitions
             (group (group (* space)) bow (or "class" "def" "async") eow)

             ;; Decorators
             (group (group (* space)) "@"))))
  (outline-minor-mode))

;; (add-hook 'python-mode-hook 'python-mode-outline-hook)

outline-mode needs some way to know what a heading looks like. It uses outline-regexp for this. At this point it does not know the level of the heading. The default behaviour is either to look at the length of the outline-regexp match, or an association in outline-heading-alist. You can override the logic by setting outline-level to a function that returns a integer based on your calculation of choice. You can also set outline-heading-alist to whatever you’d like. For example:

(setq outline-heading-alist
      '(("@chapter" . 2) ("@section" . 3) ("@subsection" . 4)
        ("@subsubsection" . 5)
        ("@unnumbered" . 2) ("@unnumberedsec" . 3)
        ("@unnumberedsubsec" . 4)  ("@unnumberedsubsubsec" . 5)
        ("@appendix" . 2) ("@appendixsec" . 3)...
        ("@appendixsubsec" . 4) ("@appendixsubsubsec" . 5) ..))

I have used this for my Cloud Custodian config below.

Since the Python regex accounts for whitespace at the beginning of the line, the level will be set accordingly.

If Outline should cover 100% of Python, it would need more work. If you define a variable after a function, the variable would be nested under the function, even though it is not a part of the function:

def something():
    print("hello")

some_variable = "hi"

However, I find the benefits outweigh the drawbacks.

Outline for Terraform

Terraform is a declarative configuratiion language for cloud resources - you write down what you want and Terraform performs the correct API calls.

For terraform-mode I have decided to use a function that always returns level 1. If you do not do this, you may find that some blocks get nested in a way that doesn’t make sense.

(defun terraform-mode-outline-hook ()
  (setq-local outline-regexp (rx
                        (or "resource" "data" "provider" "module" "variable" "output")
                        (one-or-more (not "{"))
                        "{"
                        line-end))
  (defun terraform-outline-level () 1)
  (setq-local outline-level 'terraform-outline-level)
  (outline-minor-mode t))


(add-hook 'terraform-mode-hook 'terraform-mode-outline-hook)

The regular expression for Terraform looks like this:

(counsel--elisp-to-pcre (rx
 (or "resource" "data" "provider" "module" "variable" "output")
 (one-or-more (not "{"))
 "{"
 line-end))
(?:data|module|output|provider|(?:resourc|variabl)e)[^{]+{$

Or:

(rxt-elisp-to-pcre (rx
 (or "resource" "data" "provider" "module" "variable" "output")
 (one-or-more (not "{"))
 "{"
 line-end))

Outline for Cloud Custodian policy files

Low effort solution that gets the job done.

(defun c7n-outline-hook ()
  (setq-local outline-heading-alist '(("policies:" . 1)
                                ("- name:" . 2)))
  (setq-local outline-regexp (rx (or "policies:" "- name:")))
  (outline-minor-mode))

(add-hook 'yaml-mode-hook 'c7n-outline-hook)

Outline for Terragrunt (HCL) files

Another low effort solution that gets the job done.

(defun terragrunt-outline-hook ()
  (setq-local outline-regexp "^in")
  (outline-minor-mode))

(add-hook 'hcl-mode-hook 'terragrunt-outline-hook)

Olivetti mode

Olivetti is a Italian manufacturer of typewriters, so I suppose the goal of olivetti is to capture the feeling of typing on one.

(use-package! olivetti
  :init
  (setq-default olivetti-body-width 0.618)
  :commands olivetti-mode)

About the value chosen for olivetti-body-width:

The first known decimal approximation of the (inverse) golden ratio was stated as “about 0.6180340” in 1597 by Michael Maestlin of the University of Tübingen in a letter to Kepler, his former student.

About the use of setq-default:

You can set any Lisp variable with setq, but with certain variables setq won’t do what you probably want in the .emacs file. Some variables automatically become buffer-local when set with setq; what you want in .emacs is to set the default value, using setq-default.

Applications and utilities

Epub reader

(use-package! nov
  :mode ("\\.epub\\'" . nov-mode)
  :config
  (setq nov-save-place-file (concat doom-cache-dir "nov-places")))

Occur mode

(use-package! replace
  :init
    (map! :map occur-mode-map
      :n  "e" 'occur-edit-mode)

  (add-hook 'occur-hook
          '(lambda ()
             (switch-to-buffer-other-window "*Occur*"))))

Characters

(use-package! emacs
  :config
  ;; Got those numbers from `string-to-char'
  (defconst contrib/insert-pair-alist
    '(("' Single quote" . (39 39))           ; ' '
      ("« Εισαγωγικά Gr quote" . (171 187))  ; « »
      ("\" Double quotes" . (34 34))         ; " "
      ("` Elisp quote" . (96 39))            ; ` '
      ("‘ Single apostrophe" . (8216 8217))  ; ‘ ’
      ("“ Double apostrophes" . (8220 8221)) ; “ ”
      ("( Parentheses" . (40 41))            ; ( )
      ("{ Curly brackets" . (123 125))       ; { }
      ("[ Square brackets" . (91 93))        ; [ ]
      ("< Angled brackets" . (60 62))        ; < >
      ("= Equals signs" . (61 61))           ; = =
      ("* Asterisks" . (42 42))              ; * *
      ("_ underscores" . (95 95)))           ; _ _
    "Alist of pairs for use with `prot/insert-pair-completion'.")

  (defun contrib/insert-pair-completion (&optional arg)
    "Insert pair from `contrib/insert-pair-alist'."
    (interactive "P")
    (let* ((data contrib/insert-pair-alist)
           (chars (mapcar #'car data))
           (choice (completing-read "Select character: " chars nil t))
           (left (cadr (assoc choice data)))
           (right (caddr (assoc choice data))))
      (insert-pair arg left right))))

Gnus

(after! gnus
  (setq gnus-select-method '(nntp "news.gwene.org")))

Tmux

Sometimes I want to dump the current tmux pane into Emacs.

(use-package! emacs
  :init
    (map! :leader
      (:prefix ("ø" . "utils")
        :desc "tmux buffer" "t" #'const/tmux-capture-pane))
  :config
  (setq display-line-numbers-type nil)
  (defun const/tmux-capture-pane()
    (interactive)
    (with-output-to-temp-buffer "*tmux-capture-pane*"
      (shell-command "tmux capture-pane -p -S -"
                     "*tmux-capture-pane*"
                     "*Messages*")
	(pop-to-buffer "*tmux-capture-pane*"))))
(map! :leader
      (:prefix ("f" . "file")
       :desc "tmux cd to here" "T" #'+tmux/cd-to-here))

Elfeed

I use the Doom RSS module and just set some keybinds here. I opt for SPC m r for elfeed-update which is a pleasing left, right, left key sequence. Yes, I press SPC with my left thumb.

(use-package! elfeed
  :commands elfeed
  :init
  (map! :leader
    (:prefix ("o" . "open")
      :desc "Open elfeed" "e" #'=rss)))

(after! elfeed
  (map! :map elfeed-search-mode-map
        :localleader
        :desc "Elfeed update" "r" #'elfeed-update))

(use-package! elfeed-web
    :defer t
    :commands elfeed-web-stop)

Keycast

(use-package! keycast
  :commands keycast-mode
  :config
  (define-minor-mode keycast-mode
    "Show current command and its key binding in the mode line."
    :global t
    (if keycast-mode
        (progn
          (add-hook 'pre-command-hook 'keycast-mode-line-update t)
          (add-to-list 'global-mode-string '("" mode-line-keycast " ")))
      (remove-hook 'pre-command-hook 'keycast-mode-line-update)
      (setq global-mode-string (remove '("" mode-line-keycast " ") global-mode-string))))
  (custom-set-faces!
    '(keycast-command :inherit doom-modeline-debug
                      :height 0.9)
    '(keycast-key :inherit custom-modified
                  :height 1.1
                  :weight bold)))

Regular expressions: re-builder

Rex the dog…

(use-package re-builder
  :config
  (setq reb-re-syntax 'string))

Visual regex:

(use-package visual-regexp
  :commands vr/query-replace
  :config
  (setq vr/default-replace-preview nil)
  (setq vr/match-separator-use-custom-face t))

Emoji cheat sheet

(use-package emoji-cheat-sheet-plus
  :commands emoji-cheat-sheet-plus-insert)
(use-package ivy-emoji
  :commands ivy-emoji)

Org-mode (personal information manager)

Org mode is for keeping notes, maintaining TODO lists, planning projects, and authoring documents with a fast and effective plain-text system.

I have these feature flags enabled:

+hugo
For exporting my blog from Org to Hugo flavoured markdown
+pretty
Mainly for pretty headings
+journal
For daily journals saved to ~/org/journal with one file for each day
+roam
For Zettelkasten style note taking
+present
For making reveal.js presentations from Org documents (reveal-md is a fine alternative btw)

I want to use https://sandyuraz.com/articles/orgmode-css/ (see https://news.ycombinator.com/item?id=23130104)

(after! org-journal (setq org-journal-file-format "%Y%m%d.org"))

First I set my org-directory and bind SPC f o to open my main Org-mode file.

(setq org-directory "~/org/")

(map! :leader
    (:prefix ("f" . "file")
     :desc "Open init.org" "o" '(lambda () (interactive) (find-file "~/org/org.org"))))
  • Use mixed-pitch-mode by default
  • Use olivetti-mode by default
  • Show at least one line break between headings
  • Never indent SRC blocks
(after! org
  (add-hook 'org-mode-hook (lambda () (electric-indent-local-mode -1)))
  (setq org-todo-keywords
      '((sequence "TODO(t!)" "TODAY(a!)" "NEXT(n!)" "STARTED(s!)" "IN-PROGRESS(p!)" "UNDERWAY(u!)" "WAITING(w@)" "SOMEDAY(o!)" "MAYBE(m!)" "|" "DONE(d@)" "CANCELED(c@)")
        (sequence "CHECK(k!)" "|" "DONE(d@)")
        (sequence "TO-READ(r!)" "READING(R!)" "|" "HAVE-READ(d@)")
        (sequence "TO-WATCH(!)" "WATCHING(!)" "SEEN(!)")))
    (setq org-imenu-depth 7)
  (setq org-ellipsis " ▾ ")
  (setq org-superstar-headline-bullets-list '("⁖"))
  ;;(add-hook! 'org-mode-hook #'mixed-pitch-mode)
  ;;(add-hook! 'org-mode-hook #'olivetti-mode)
  (setq org-babel-python-command "python3")
  (setq org-cycle-separator-lines 1)
  (setq org-edit-src-content-indentation 0)
  (setq org-export-initial-scope 'subtree)
  (setq org-image-actual-width 400)
  (setq org-src-window-setup 'current-window)
  (setq org-startup-indented t))

Org-capture templates from Protesilaos Stavrou be serving me well.

A quoted list of lists:

  1. The key
  2. The description
  3. The type of entry (a symbol)
    • entry: An Org mode node, with a headline. Will be filed as the child of the target entry or as a top-level entry. The target file should be an Org file.
  4. The target
    • Like file+headline
  5. The template

If you say file+headline you would give it a filename and a headline to put the template under.

(after! org-capture
  (setq org-capture-templates
        '(("b" "Basic task for future review" entry
           (file+headline "tasks.org" "Basic tasks that need to be reviewed")
           "* %^{Title}\n:PROPERTIES:\n:CAPTURED: %U\n:END:\n\n%i%l"
           :empty-lines 1)

          ("w" "Work")
          ("wt" "Task or assignment" entry
           (file+headline "work.org" "Tasks and assignments")
           "\n\n* TODO [#A] %^{Title} :@work:\nSCHEDULED: %^t\n:PROPERTIES:\n:CAPTURED: %U\n:END:\n\n%i%?"
           :empty-lines 1)

          ("wm" "Meeting, event, appointment" entry
           (file+headline "work.org" "Meetings, events, and appointments")
           "\n\n* MEET [#A] %^{Title} :@work:\nSCHEDULED: %^T\n:PROPERTIES:\n:CAPTURED: %U\n:END:\n\n%i%?"
           :empty-lines 1)

          ("t" "Task with a due date" entry
           (file+headline "tasks.org" "Task list with a date")
           "\n\n* %^{Scope of task||TODO|STUDY|MEET} %^{Title} %^g\nSCHEDULED: %^t\n:PROPERTIES:\n:CAPTURED: %U\n:END:\n\n%i%?"
           :empty-lines 1)

          ("j" "Journal" entry
           (file+olp+datetree "journal.org")
           "* %?\n"
           :empty-lines 1)

          ("r" "Reply to an email" entry
           (file+headline "tasks.org" "Mail correspondence")
           "\n\n* TODO [#B] %:subject :mail:\nSCHEDULED: %t\n:PROPERTIES:\n:CONTEXT: %a\n:END:\n\n%i%?"
           :empty-lines 1)))

  (defun org-hugo-new-subtree-post-capture-template ()
      (let* ((title (read-from-minibuffer "Post Title: "))
           (fname (org-hugo-slug title)))
      (mapconcat #'identity
                 `(
                   ,(concat "* TODO " title)
                   ":PROPERTIES:"
                   ,(concat ":EXPORT_FILE_NAME: " fname)
                   ":END:"
                   "%?\n")
                 "\n")))

  (add-to-list 'org-capture-templates
               '("h" "Hugo blog post" entry
                 (file "~/Projects/org-blog/blog.org")
                 (function org-hugo-new-subtree-post-capture-template)
                 :empty-lines 1)))
(use-package org-web-tools
  :commands org-web-tools-insert-link-for-url)

The org-roam capture template:

(after! org-roam

(defun +org-notes-project-p ()
  "Return non-nil if current buffer has any todo entry.

TODO entries marked as done are ignored, meaning the this
function returns nil if current buffer contains only completed
tasks."
  (seq-find                                 ; (3)
   (lambda (type)
     (eq type 'todo))
   (org-element-map                         ; (2)
       (org-element-parse-buffer 'headline) ; (1)
       'headline
     (lambda (h)
       (org-element-property :todo-type h)))))

(defun +org-notes-project-update-tag ()
  "Update PROJECT tag in the current buffer."
  (when (and (not (active-minibuffer-window))
             (+org-notes-buffer-p))
    (let* ((file (buffer-file-name (buffer-base-buffer)))
           (all-tags (org-roam--extract-tags file))
           (prop-tags (org-roam--extract-tags-prop file))
           (tags prop-tags))
      (if (+org-notes-project-p)
          (setq tags (cons "project" tags))
        (setq tags (remove "project" tags)))
      (unless (eq prop-tags tags)
        (org-roam--set-global-prop
         "ROAM_TAGS"
         (combine-and-quote-strings (seq-uniq tags)))))))

(defun +org-notes-buffer-p ()
  "Return non-nil if the currently visited buffer is a note."
  (and buffer-file-name
       (string-prefix-p
        (expand-file-name (file-name-as-directory org-roam-directory))
        (file-name-directory buffer-file-name))))

(defun +org-notes-project-files ()
  "Return a list of note files containing Project tag."
  (seq-map
   #'car
   (org-roam-db-query
    [:select file
     :from tags
     :where (like tags (quote "%\"project\"%"))])))

(defun +agenda-files-update (&rest _)
  "Update the value of `org-agenda-files'."
  (setq org-agenda-files (+org-notes-project-files)))

(add-hook 'find-file-hook #'+org-notes-project-update-tag)
(add-hook 'before-save-hook #'+org-notes-project-update-tag)

(advice-add 'org-agenda :before #'+agenda-files-update)

  (push 'company-capf company-backends)
  (setq org-roam-graph-viewer "/Applications/Firefox.app/Contents/MacOS/firefox")
  (setq org-roam-completion-everywhere t)
  (setq org-roam-capture-templates
        '(("d" "default" plain (function org-roam-capture--get-point)
        "%?"
        :file-name "${slug}"
        :head "#+title: ${title}\n"
        :unnarrowed t))))

(use-package! org-transclusion)
(use-package! org-drill
  :after org)
(use-package! company
  :config
  (setq company-idle-delay 0.2)
  (setq company-minimum-prefix-length 3))

Selectrum

(use-package! ivy-prescient
  :config
  ;; to make sorting and filtering more intelligent
  (ivy-prescient-mode +1)
  (prescient-persist-mode +1))
(use-package! marginalia
  :config
  (setq marginalia-annotators
        '(marginalia-annotators-heavy
          marginalia-annotators-light))
  (marginalia-mode 1))

Languages

Flycheck

(after! flycheck
  (add-to-list 'flycheck-checkers 'terraform-tflint)
  (add-to-list 'flycheck-checkers 'terraform))

The list already contain these items, so the above is useless. I just want to remember that they exist.

Terraform

Snippets.

I want some snippets:

# -*- mode: terraform-mode -*-
# name: sb1-module
# --

source="${1:$$(yas-choose-value (directory-files "/Users/anders/projects"))}"

Anyway…

(after! yasnippet
  (add-to-list 'yas-snippet-dirs
               (concat
                (file-name-as-directory straight-base-dir)
                (file-name-as-directory "straight")
                (file-name-as-directory "build")
                (file-name-as-directory "yasnippet-terraform/terraform-mode"))))

Markdown

:mode is an implicit defer in any case.

(use-package! markdown-mode
  :mode (("README\\.md\\'" . gfm-mode)
         ("\\.md$" . markdown-mode)
         ("\\.pmd$" . markdown-mode)
         ("\\.cbmd$" . markdown-mode)
         ("\\.markdown\\'" . markdown-mode)))

Python 🐍

I don’t understand why setting the right virtual environment is not a common use case for people using lsp-mode. Anyway, I use poetry and enable poetry-tracking-mode which will set the correct environment right before starting lsp-mode. The function goes at the front of the hook list.

I also add some Poetry commands to the local leader of python-mode. It’s nice to be in control of the LSP beast.

There’s some other ways to set the virtual environment:

  • poetry-venv-toggle
  • pyvenv-activate

You must run lsp-workspace-restart for changes to take effect.

(add-hook! 'python-mode-hook 'poetry-tracking-mode)

(after! poetry
  (setq poetry-tracking-strategy 'projectile)
  (map! :map python-mode-map
        :localleader
        :desc "Activate Poetry tracking mode" "c" #'poetry-tracking-mode
        :desc "Restart LSP workspace" "r" #'lsp-workspace-restart
        :desc "Workon/off the Poetry venv" "w" #'poetry-venv-toggle
        :desc "Poetry menu" "p" #'poetry))

Actually, the logic for choosing a virtual environment depends on which language server you use. Consider lsp-pyright-locate-venv in lsp-pyright.el. Now what is the point of that?

The Spacemacs Python layer seems very nice - take a look at the Spacemacs Python layer and Python Development in Spacemacs - YouTube.

Keybindings

In this section I will plan out my keybinding strategy. The current one looks like this:

(after! elfeed
  (map! :map elfeed-search-mode-map
        :localleader
        :desc "Elfeed update" "r" #'elfeed-update))

Inspired by issue 814.

Mastering Key Bindings in Emacs - Mastering Emacs

general-override-mode-map is a minor mode map that will take precedence over all maps. The alias is :override.

https://github.com/noctuid/general.el#override-keymaps-and-buffer-local-keybindings

https://github.com/hlissner/doom-emacs/issues?q=override+label%3Are%3Akeybinds

Hi! Thanks for the PR but I cannot accept it. It is redundant with hideshow which the :editor fold module configures, and will handle folding lisp blocks, while outline-minor-mode is relegated to handling comment headings, and vimish-fold for arbitrary folds.