doom-config/config.org

2950 lines
98 KiB
Org Mode

#+title: Doom Emacs Config
#+author: tokinanpa
#+email: kiana.a.sheibani@gmail.com
#+property: header-args:elisp :results replace :exports code
#+property: header-args :tangle no :results silent :eval no-export :mkdirp yes
* Introduction
#+begin_quote
Emacs outshines all other editing software in approximately the same way that
the noonday sun does the stars. It is not just bigger and brighter; it simply
makes everything else vanish.
-- Neil Stephenson, /In the Beginning was the Command Line/ (1998)
#+end_quote
*Hello!*
This is a literate configuration for [[https:github.com/doomemacs/doomemacs][Doom Emacs]].
** Background; or, My Emacs Story
Given that you are currently reading an Emacs config, I will assume that you
already have a moderate understanding of what Emacs is and the ideas behind its
configuration system. If you do not, then [[https://docs.doomemacs.org/v21.12/#/users/intro/why-emacs][this section]] of the official Doom
Emacs documentation makes for a decent introduction.
Rather than use this space to explain Emacs, I will instead use it to chronicle
my history with Emacs, how I got here, and what lessons should be taken away
from this experience. Don't worry, I promise it won't be long.
*** In The Beginning
My first brush with Emacs was in around 2019, when I installed it for use with
the proof assistant language Agda. I had vaguely heard tales about its beauty
and power, but I was nowhere near comfortable enough with config files and
programming in general to fully appreciate its capabilities (not to mention that
I was using Windows at the time). I bounced off of it pretty quickly because...
well, vanilla Emacs is just kinda terrible.
#+caption[Vanilla Emacs]: Look at this and tell me that it doesn't look at least a little awful.
#+name: vanilla-emacs
[[file:assets/vanilla_emacs.png]]
A few years later in 2022, after I had moved to the more sensible OS of Arch
Linux, I discovered that my preferred text editor [[https://atom-editor.cc/][Atom]] was in the process of
being discontinued and began to look for a replacement. I tried Visual Studio
Code for a little while, but after some serious use I became dissatisfied with
how few options there were to customize it to fit my workflow.
It was at this point that I started thinking about Emacs again. By chance, I
happened to stumble upon Doom Emacs, and it turned out to be exactly what I was
looking for:
- Extreme flexibility
- Robust modular configuration system
- Sensible defaults
- Extensive ecosystem
As I became more comfortable with configuration via scripting, I immersed myself
into the many utilities that make up the Emacs ecosystem: =org-mode=, =calfw=, =calc=,
=mu4e=. I started putting more and more time into tweaking these applications to
fit my needs, my files kept getting longer and longer, and eventually I fully
fell off the deep end and now we're here.
*** TODO Literate Programming
My first Doom Emacs config was hacked together directly from the generated
example config: no comments, no organization, nothing. ~after!~ and ~use-package!~
blocks were scattered about the file without rhyme or reason, making it very
difficult to remember what any particular line of code was actually doing. I was
able to mitigate some of this issue by categorizing my config into multiple
files, but at the end of the day it was a losing battle. The config directory
was at 1200 lines of code before I decided that something needed to be done.
I was considering what to do about this problem of organizational decay when I
came across [[https://tecosaur.github.io/emacs-config/config.html][Tecosaur's config]] and learned about =org-mode='s literate programming
support. I had been using =org-mode= for several months at this point and was very
comfortable with it, so utilizing it to better organize my code seemed like a
good idea.
*** =confpkg=
As part of their literate config, Tecosaur implemented =confpkg=, an embedded
Emacs Lisp library that manages multiple aspects of config tangling:
- Controlling what files each code block is tangled to
- Generating package files from templates
- Automatically detecting cross-section dependencies
- Reporting profiling information on config load times
It's an incredibly impressive utility, and I highly recommend reading
[[https://tecosaur.github.io/emacs-config/config.html#rudimentary-configuration-confpkg][the section in their config]] on its design. I tried to read through it myself,
but I don't understand half of it; it's a bizarre mixture of exploits to hook
into =org-mode='s tangling process, self-modifying buffer shenanigans, and abuse
of various features of =org-babel=.
Luckily, I don't need to be able to understand code in order to do what I do
best: press =Ctrl+C= and =Ctrl+V= in that order. Programming!
If you're reading the raw org file instead of the published version, the code
for =confpkg= is below. It is mostly unchanged, aside from these tweaks:
- Prevent the code from being exported
- Reorganize to get rid of superfluous noweb references
- Change the package template to contain my information
**** confpkg :noexport:
***** Preparation
#+name: confpkg-prepare
#+begin_src emacs-lisp
(condition-case nil
(progn
(message "Intitialising confpkg")
(org-fold-core-ignore-fragility-checks
(org-babel-map-executables nil
(when (eq (org-element-type (org-element-context)) 'babel-call)
(org-babel-lob-execute-maybe)))))
(quit (revert-buffer t t t)))
#+end_src
#+header: :tangle (expand-file-name (make-temp-name "emacs-org-babel-excuses/confpkg-prepare-") temporary-file-directory)
#+begin_src emacs-lisp :noweb no-export :export-embed no
<<confpkg-prepare()>>
#+end_src
***** Setup
#+name: confpkg-setup
#+begin_src emacs-lisp :results silent :noweb no-export
(setq confpkg--num 0
confpkg--list nil)
;; Dependency handling
(defun confpkg--rough-extract-definitions (file)
(with-temp-buffer
(insert-file-contents file)
(goto-char (point-min))
(let (symbols)
(while (re-search-forward
(rx line-start (* (any ?\s ?\t)) "("
(or "defun" "defmacro" "defsubst" "defgeneric" "defalias" "defvar" "defcustom" "defface" "deftheme"
"cl-defun" "cl-defmacro" "cl-defsubst" "cl-defmethod" "cl-defstruct" "cl-defgeneric" "cl-deftype")
(+ (any ?\s ?\t))
(group (+ (any "A-Z" "a-z" "0-9"
?+ ?- ?* ?/ ?_ ?~ ?! ?@ ?$ ?% ?^ ?& ?= ?: ?< ?> ?{ ?})))
(or blank ?\n))
nil t)
(push (match-string 1) symbols))
symbols)))
(defun confpkg--rough-uses-p (file symbols)
(with-temp-buffer
(insert-file-contents file)
(let ((symbols (copy-sequence symbols)) uses-p)
(while symbols
(goto-char (point-min))
(if (re-search-forward (rx word-start (literal (car symbols)) word-end) nil t)
(setq uses-p t symbols nil)
(setq symbols (cdr symbols))))
uses-p)))
(defun confpkg-annotate-list-dependencies ()
(dolist (confpkg confpkg--list)
(plist-put confpkg :defines
(confpkg--rough-extract-definitions
(plist-get confpkg :file))))
(dolist (confpkg confpkg--list)
(let ((after (plist-get confpkg :after))
requires)
(dolist (other-confpkg confpkg--list)
(when (and (not (eq other-confpkg confpkg))
(confpkg--rough-uses-p (plist-get confpkg :file)
(plist-get other-confpkg :defines)))
(push (plist-get other-confpkg :package) requires)))
(when (and after (symbolp after))
(push after requires))
(plist-put confpkg :requires requires))))
(defun confpkg-write-dependencies ()
(dolist (confpkg confpkg--list)
(when (plist-get confpkg :requires)
(with-temp-buffer
(setq buffer-file-name (plist-get confpkg :file))
(insert-file-contents buffer-file-name)
(re-search-forward "^;;; Code:\n")
(insert "\n")
(dolist (req (plist-get confpkg :requires))
(insert (format "(require '%s)\n" req)))
(write-region nil nil buffer-file-name)
(set-buffer-modified-p nil)))))
;; Commenting package statements
(defun confpkg-comment-out-package-statements ()
(dolist (confpkg confpkg--list)
(with-temp-buffer
(setq buffer-file-name (plist-get confpkg :file))
(insert-file-contents buffer-file-name)
(goto-char (point-min))
(while (re-search-forward "^;;; Code:\n[[:space:]\n]*(\\(package!\\|unpin!\\)[[:space:]\n]+\\([^[:space:]]+\\)\\b" nil t)
(plist-put confpkg :package-statements
(nconc (plist-get confpkg :package-statements)
(list (match-string 2))))
(let* ((start (progn (beginning-of-line) (point)))
(end (progn (forward-sexp 1)
(if (looking-at "[\t ]*;.*")
(line-end-position)
(point))))
(contents (buffer-substring start end))
paste-start paste-end
(comment-start ";")
(comment-padding " ")
(comment-end ""))
(delete-region start (1+ end))
(re-search-backward "^;;; Code:")
(beginning-of-line)
(insert ";; Package statement:\n")
(setq paste-start (point))
(insert contents)
(setq paste-end (point))
(insert "\n;;\n")
(comment-region paste-start paste-end 2)))
(when (buffer-modified-p)
(write-region nil nil buffer-file-name)
(set-buffer-modified-p nil)))))
(defun confpkg-create-config ()
(let ((revert-without-query '("config\\.el"))
(keywords (org-collect-keywords '("AUTHOR" "EMAIL")))
(original-buffer (current-buffer)))
(with-temp-buffer
(insert
(format ";;; config.el -*- lexical-binding: t; -*-
;; SPDX-FileCopyrightText: © 2020-%s %s <%s>
;; SPDX-License-Identifier: MIT
;; Generated at %s from the literate configuration.
(add-to-list 'load-path %S)\n"
(format-time-string "%Y")
(cadr (assoc "AUTHOR" keywords))
(cadr (assoc "EMAIL" keywords))
(format-time-string "%FT%T%z")
(replace-regexp-in-string
(regexp-quote (getenv "HOME")) "~"
(expand-file-name "subconf/"))))
(mapc
(lambda (confpkg)
(insert
(if (eq 'none (plist-get confpkg :via))
(format "\n;;; %s intentionally omitted.\n" (plist-get confpkg :name))
(with-temp-buffer
(cond
((eq 'copy (plist-get confpkg :via))
(insert-file-contents (plist-get confpkg :file))
(goto-char (point-min))
(narrow-to-region
(re-search-forward "^;;; Code:\n+")
(progn
(goto-char (point-max))
(re-search-backward (format "[^\n\t ][\n\t ]*\n[\t ]*(provide '%s)" (plist-get confpkg :package)))
(match-end 0))))
((eq 'require (plist-get confpkg :via))
(insert (format "(require '%s)\n" (plist-get confpkg :package))))
(t (insert (format "(warn \"%s confpkg :via has unrecognised value: %S\" %S %S)"
(plist-get confpkg :name) (plist-get confpkg :via)))))
(goto-char (point-min))
(insert "\n;;:------------------------"
"\n;;; " (plist-get confpkg :name)
"\n;;:------------------------\n\n")
(when (plist-get confpkg :defines)
(insert ";; This block defines "
(mapconcat
(lambda (d) (format "`%s'" d))
(plist-get confpkg :defines)
", ")
".")
(when (re-search-backward "\\([^, ]+\\), \\([^, ]+\\), \\([^, ]+\\).\\="
(line-beginning-position) t)
(replace-match "\\1, \\2, and \\3."))
(when (re-search-backward "\\([^, ]+\\), \\([^, ]+\\).\\="
(line-beginning-position) t)
(replace-match "\\1 and \\2."))
(insert "\n\n")
(forward-line -2)
(setq-local comment-start ";")
(fill-comment-paragraph)
(forward-paragraph 1)
(forward-line 1))
(if (equal (plist-get confpkg :package) "config-confpkg-timings")
(progn
(goto-char (point-max))
(insert "\n\n\
(confpkg-create-record 'doom-pre-config (float-time (time-subtract (current-time) before-init-time)))
(confpkg-start-record 'config)
(confpkg-create-record 'config-defered 0.0 'config)
(confpkg-create-record 'set-hooks 0.0 'config-defered)
(confpkg-create-record 'load-hooks 0.0 'config-defered)
(confpkg-create-record 'requires 0.0 'root)\n"))
(let ((after (plist-get confpkg :after))
(pre (and (plist-get confpkg :pre)
(org-babel-expand-noweb-references
(list "emacs-lisp"
(format "<<%s>>" (plist-get confpkg :pre))
'((:noweb . "yes")
(:comments . "none")))
original-buffer)))
(name (replace-regexp-in-string
"config--?" ""
(plist-get confpkg :package))))
(if after
(insert (format "(confpkg-with-record '%S\n"
(list (concat "hook: " name) 'set-hooks))
(if pre
(concat ";; Begin pre\n" pre "\n;; End pre\n")
"")
(format (if (symbolp after) ; If single feature.
" (with-eval-after-load '%s\n"
" (after! %s\n")
after))
(when pre
(insert "\n;; Begin pre (unnecesary since after is unused)\n"
pre
"\n;; End pre\n")))
(insert
(format "(confpkg-with-record '%S\n"
(list (concat "load: " name)
(if after 'load-hooks 'config)))))
(goto-char (point-max))
(when (string-match-p ";" (thing-at-point 'line))
(insert "\n"))
(insert ")")
(when (plist-get confpkg :after)
(insert "))"))
(insert "\n"))
(buffer-string)))))
(let ((confpkg-timings ;; Ensure timings is put first.
(cl-some (lambda (p) (and (equal (plist-get p :package) "config-confpkg-timings") p))
confpkg--list)))
(append (list confpkg-timings)
(nreverse (remove confpkg-timings confpkg--list)))))
(insert "\n(confpkg-finish-record 'config)\n\n;;; config.el ends here")
(write-region nil nil "config.el" nil :silent))))
;; Cleanup
(defun confpkg-cleanup ()
(org-fold-core-ignore-fragility-checks
(org-babel-map-executables nil
(when (and (eq (org-element-type (org-element-context)) 'babel-call)
(equal (org-element-property :call (org-element-context)) "confpkg"))
(org-babel-remove-result)
(org-entry-delete nil "header-args:emacs-lisp")))))
;; Finaliser
(defun confpkg-tangle-finalise ()
(remove-hook 'org-babel-tangle-finished-hook #'confpkg-tangle-finalise)
(revert-buffer t t t)
(confpkg-comment-out-package-statements)
(confpkg-annotate-list-dependencies)
(confpkg-create-config)
(confpkg-write-dependencies)
(message "Processed %s elisp files" (length confpkg--list)))
;; Clear old files
(make-directory "subconf" t)
(dolist (conf-file (directory-files "subconf" t "config-.*\\.el"))
(delete-file conf-file))
(add-hook 'org-babel-tangle-finished-hook #'confpkg-tangle-finalise)
#+end_src
#+call: confpkg-setup[:results none]()
***** Confpkg Dispatch
#+name: confpkg
#+begin_src elisp :var name="" needs="" after="" pre="" prefix="config-" via="copy" :results silent raw :noweb no-export
;; Babel block for use with #+call
;; Arguments:
;; - name, the name of the config sub-package
;; - needs, (when non-empty) required system executable(s)
;; - after, required features
;; - pre, a noweb reference to code that should be executed eagerly,
;; and not deferred via after. The code is not included in the
;; generated .el file and should only be used in dire situations.
;; - prefix, the package prefix ("config-" by default)
;; - via, how this configuration should be included in config.el,
;; the current options are:
;; + "copy", copy the configuration lisp
;; + "require", insert a require statement
;; + "none", do not do anything to load this configuration.
;; This only makes sense when configuration is either being
;; temporarily disabled or loaded indirectly/elsewhere.
(when (or (string-empty-p needs)
(cl-every #'executable-find (delq nil (split-string needs ","))))
(let* ((name (if (string-empty-p name)
(save-excursion
(and (org-back-to-heading-or-point-min t)
(substring-no-properties
(org-element-interpret-data
(org-element-property :title (org-element-at-point))))))
name))
(after
(cond
((string-empty-p after) nil)
((string-match-p "\\`[^()]+\\'" after)
(intern after)) ; Single feature.
(t after)))
(pre (and (not (string-empty-p pre)) pre))
(confpkg-name
(concat prefix (replace-regexp-in-string
"[^a-z-]" "-" (downcase name))))
(confpkg-file (expand-file-name (concat confpkg-name ".el")
"subconf")))
(unless (file-exists-p confpkg-file)
(make-empty-file confpkg-file t))
(cl-incf confpkg--num)
(org-set-property
"header-args:emacs-lisp"
(format ":noweb no-export :tangle no :noweb-ref %s" confpkg-name))
(push (list :name name
:package confpkg-name
:file confpkg-file
:after after
:pre pre
:via (intern via)
:package-statements nil)
confpkg--list)
(format-spec
"#+begin_src emacs-lisp :tangle %f :noweb no-export :noweb-ref none :comments no
<<confpkg-template>>
,#+end_src"
`((?n . ,confpkg--num)
(?p . ,confpkg-name)
(?f . ,confpkg-file)
(?Y . ,(format-time-string "%Y"))
(?B . ,(format-time-string "%B"))
(?m . ,(format-time-string "%m"))
(?d . ,(format-time-string "%d"))
(?M . ,(format-time-string "%M"))
(?S . ,(format-time-string "%S"))))))
#+end_src
#+name: confpkg-template
#+begin_src emacs-lisp :eval no
;;; %p.el --- Generated package (no.%n) from my config -*- lexical-binding: t; -*-
;;
;; Copyright (C) %Y Kiana Sheibani
;;
;; Author: Kiana Sheibani <kiana.a.sheibani@gmail.com>
;; Created: %B %d, %Y
;; Modified: %B %d, %Y
;; Version: %Y.%m.%d
;;
;; This file is not part of GNU Emacs.
;;
;;; Commentary:
;;
;; Generated package (no.%n) from my config.
;;
;; This is liable to have unstated dependencies, and reply on other bits of
;; state from other configuration blocks. Only use this if you know /exactly/
;; what you are doing.
;;
;; This may function nicely as a bit of self-contained functionality, or it
;; might be a horrid mix of functionalities and state.
;;
;; Hopefully, in future static analysis will allow this to become more
;; properly package-like.
;;
;;; Code:
<<%p>>
(provide '%p)
;;; %p.el ends here
#+end_src
***** Quieter Output
#+name: confpkg-quieter-output
#+begin_src emacs-lisp
(when noninteractive
(unless (fboundp 'doom-shut-up-a)
(defun doom-shut-up-a (fn &rest args)
(let ((standard-output #'ignore)
(inhibit-message t))
(apply fn args))))
(advice-add 'org-babel-expand-body:emacs-lisp :around #'doom-shut-up-a)
;; Quiet some other annoying messages
(advice-add 'sh-set-shell :around #'doom-shut-up-a)
(advice-add 'rng-what-schema :around #'doom-shut-up-a)
(advice-add 'python-indent-guess-indent-offset :around #'doom-shut-up-a))
#+end_src
#+call: confpkg-quieter-output()
***** CLI
#+begin_src emacs-lisp :tangle cli.el :noweb-ref none
;;; cli.el -*- lexical-binding: t; -*-
(setq org-confirm-babel-evaluate nil)
(defun doom-shut-up-a (orig-fn &rest args)
(quiet! (apply orig-fn args)))
(advice-add 'org-babel-execute-src-block :around #'doom-shut-up-a)
#+end_src
***** Timings
#+call: confpkg("Confpkg timings")
#+begin_src emacs-lisp
(defvar confpkg-load-time-tree (list (list 'root)))
(defvar confpkg-record-branch (list 'root))
(defvar confpkg-record-num 0)
(defun confpkg-create-record (name elapsed &optional parent enclosing)
(let ((parent (assoc (or parent (car confpkg-record-branch))
confpkg-load-time-tree))
(record (cons name (list (list 'self
:name (format "%s" name)
:num (cl-incf confpkg-record-num)
:elapsed elapsed
:enclosing enclosing)))))
(push record confpkg-load-time-tree)
(push record (cdr parent))
record))
(defun confpkg-start-record (name &optional parent)
(let ((record (confpkg-create-record name 0.0e+NaN parent t)))
(plist-put (cdadr record) :start (float-time))
(push name confpkg-record-branch)
record))
(defun confpkg-finish-record (name)
(let ((self-record (cdar (last (cdr (assoc name confpkg-load-time-tree))))))
(plist-put self-record :elapsed
(- (float-time) (plist-get self-record :start) 0.0))
(unless (equal (car confpkg-record-branch) name)
(message "Warning: Confpkg timing record expected to finish %S, instead found %S. %S"
name (car confpkg-record-branch) confpkg-record-branch))
(setq confpkg-record-branch (cdr confpkg-record-branch))))
(defmacro confpkg-with-record (name &rest body)
"Create a time record around BODY.
The record must have a NAME."
(declare (indent 1))
(let ((name-val (make-symbol "name-val"))
(record-spec (make-symbol "record-spec")))
`(let* ((,name-val ,name)
(,record-spec (if (consp ,name-val) ,name-val (list ,name-val))))
(apply #'confpkg-start-record ,record-spec)
(unwind-protect
(progn ,@body)
(confpkg-finish-record (car ,record-spec))))))
(defadvice! +require--log-timing-a (orig-fn feature &optional filename noerror)
:around #'require
(if (or (featurep feature)
(eq feature 'cus-start) ; HACK Why!?!
(assoc (format "require: %s" feature) confpkg-load-time-tree))
(funcall orig-fn feature filename noerror)
(confpkg-with-record (list (format "require: %s" feature)
(and (eq (car confpkg-record-branch) 'root)
'requires))
(funcall orig-fn feature filename noerror))))
(defun confpkg-timings-report (&optional sort-p node)
"Display a report on load-time information.
Supply SORT-P (or the universal argument) to sort the results.
NODE defaults to the root node."
(interactive
(list (and current-prefix-arg t)))
(let ((buf (get-buffer-create "*Confpkg Load Time Report*"))
(depth 0)
num-pad name-pad max-time max-total-time max-depth)
(cl-labels
((sort-records-by-time
(record)
(let ((self (assoc 'self record)))
(append (list self)
(sort (nreverse (remove self (cdr record)))
(lambda (a b)
(> (or (plist-get (alist-get 'self a) :total) 0.0)
(or (plist-get (alist-get 'self b) :total) 0.0)))))))
(print-record
(record)
(cond
((eq (car record) 'self)
(insert
(propertize
(string-pad (number-to-string (plist-get (cdr record) :num)) num-pad)
'face 'font-lock-keyword-face)
" "
(propertize
(apply #'concat
(make-list (1- depth) ""))
'face 'font-lock-comment-face)
(string-pad (format "%s" (plist-get (cdr record) :name)) name-pad)
(make-string (* (- max-depth depth) 2) ?\s)
(propertize
(format "%.4fs" (plist-get (cdr record) :elapsed))
'face
(list :foreground
(doom-blend 'orange 'green
(/ (plist-get (cdr record) :elapsed) max-time))))
(if (= (plist-get (cdr record) :elapsed)
(plist-get (cdr record) :total))
""
(concat " (Σ="
(propertize
(format "%.3fs" (plist-get (cdr record) :total))
'face
(list :foreground
(doom-blend 'orange 'green
(/ (plist-get (cdr record) :total) max-total-time))))
")"))
"\n"))
(t
(cl-incf depth)
(mapc
#'print-record
(if sort-p
(sort-records-by-time record)
(reverse (cdr record))))
(cl-decf depth))))
(flatten-records
(records)
(if (eq (car records) 'self)
(list records)
(mapcan
#'flatten-records
(reverse (cdr records)))))
(tree-depth
(records &optional depth)
(if (eq (car records) 'self)
(or depth 0)
(1+ (cl-reduce #'max (cdr records) :key #'tree-depth))))
(mapreduceprop
(list map reduce prop)
(cl-reduce
reduce list
:key
(lambda (p) (funcall map (plist-get (cdr p) prop)))))
(elaborate-timings
(record)
(if (eq (car record) 'self)
(plist-get (cdr record) :elapsed)
(let ((total (cl-reduce #'+ (cdr record)
:key #'elaborate-timings))
(self (cdr (assoc 'self record))))
(if (plist-get self :enclosing)
(prog1
(plist-get self :elapsed)
(plist-put self :total (plist-get self :elapsed))
(plist-put self :elapsed
(- (* 2 (plist-get self :elapsed)) total)))
(plist-put self :total total)
total))))
(elaborated-timings
(record)
(let ((record (copy-tree record)))
(elaborate-timings record)
record)))
(let* ((tree
(elaborated-timings
(append '(root)
(copy-tree
(alist-get (or node 'root)
confpkg-load-time-tree
nil nil #'equal))
'((self :num 0 :elapsed 0)))))
(flat-records
(cl-remove-if
(lambda (rec) (= (plist-get (cdr rec) :num) 0))
(flatten-records tree))))
(setq max-time (mapreduceprop flat-records #'identity #'max :elapsed)
max-total-time (mapreduceprop flat-records #'identity #'max :total)
name-pad (mapreduceprop flat-records #'length #'max :name)
num-pad (mapreduceprop flat-records
(lambda (n) (length (number-to-string n)))
#'max :num)
max-depth (tree-depth tree))
(with-current-buffer buf
(erase-buffer)
(setq-local outline-regexp "[0-9]+ *\\(?:• \\)*")
(outline-minor-mode 1)
(use-local-map (make-sparse-keymap))
(local-set-key "TAB" #'outline-toggle-children)
(local-set-key "\t" #'outline-toggle-children)
(local-set-key (kbd "<backtab>") #'outline-show-subtree)
(local-set-key (kbd "C-<iso-lefttab>")
(eval `(cmd! (if current-prefix-arg
(outline-show-all)
(outline-hide-sublevels (+ ,num-pad 2))))))
(insert
(propertize
(concat (string-pad "#" num-pad) " "
(string-pad "Confpkg"
(+ name-pad (* 2 max-depth) -3))
(format " Load Time (Σ=%.3fs)\n"
(plist-get (cdr (assoc 'self tree)) :total)))
'face '(:inherit (tab-bar-tab bold) :extend t :underline t)))
(dolist (record (if sort-p
(sort-records-by-time tree)
(reverse (cdr tree))))
(unless (eq (car record) 'self)
(print-record record)))
(set-buffer-modified-p nil)
(goto-char (point-min)))
(pop-to-buffer buf)))))
#+end_src
** Current Issues
*** Mail
My mail client currently requires GPG access to sync emails, which doesn't
properly work. Using the mail client requires running ~mbsync -a~ externally
instead.
*** Org Mode
A lot of my current Org mode configuration consists of relics of previous
organizational systems, including the TODO states and capture templates. I don't
currently use these, as most of my Org mode use has shifted towards Org-roam, so
it might be a good idea to replace these with something more useful.
* Doom Modules
One of Doom Emacs's most useful features is its modular configuration system,
allowing configuration code to be sectioned into modules that can be enabled or
customized individually. Doom provides a full suite of pre-written modules to
enable.
#+begin_src emacs-lisp :tangle init.el :noweb no-export
;;; init.el -*- lexical-binding: t; -*-
;; This file controls what Doom modules are enabled and what order they load in.
(doom! <<doom-input>>
<<doom-completion>>
<<doom-ui>>
<<doom-editor>>
<<doom-emacs>>
<<doom-term>>
<<doom-checkers>>
<<doom-tools>>
<<doom-os>>
<<doom-lang>>
<<doom-email>>
<<doom-app>>
<<doom-config>>
)
#+end_src
** Config Modules
Considering this is a literate config, the corresponding ~:config literate~ module
is necessary. We'll also turn on some of the default config options too.
#+name: doom-config
#+begin_src emacs-lisp
:config
literate
(default +bindings +smartparens)
#+end_src
** Completion
I'm a big fan of the Vertico ecosystem, as it's lightweight and easy to use.
Let's turn on that module, along with the icons flag because why not.
#+name: doom-completion
#+begin_src emacs-lisp
:completion
(vertico +icons)
(company +childframe)
#+end_src
** Checkers
The two most common syntax checking engines seem to be =flymake= and =flycheck=.
=flymake= is built in to Emacs, is generally faster and currently has better
support in the ecosystem, so let's use that one.
We'll also enable a dedicated spell checking module using ~aspell~, as that seems
to be the recommended option.
#+name: doom-checkers
#+begin_src emacs-lisp
:checkers
(syntax +flymake +childframe)
(spell +aspell)
;;grammar
#+end_src
** UI
Most of these are either defaults that come with Doom Emacs or just recommended,
but here are the highlights:
- ~vi-tilde-fringe~ because I like how it looks
- ~(window-select +numbers)~ because multiple windows are too inconvenient without
an easy way to switch between them
- ~file-templates~ and ~snippets~ because typing is hard
- ~(format +onsave)~ because I don't want to have to remember to run a formatter
- ~direnv~ because I'm a nix user
- Icons!
#+name: doom-ui
#+begin_src emacs-lisp
:ui
deft
doom
doom-dashboard
;;doom-quit
;;(emoji +unicode)
hl-todo
;;hydra
indent-guides
;;ligatures
;;minimap
modeline
;;nav-flash
;;neotree
ophints
(popup +all +defaults)
;;tabs
(treemacs +lsp)
unicode
(vc-gutter +diff-hl +pretty)
vi-tilde-fringe
(window-select +numbers)
workspaces
;;zen
#+end_src
#+name: doom-editor
#+begin_src emacs-lisp
:editor
(evil +everywhere)
file-templates
fold
(format +onsave)
;;god
;;lispy
;;multiple-cursors
;;objed
;;parinfer
;;rotate-text
snippets
;;word-wrap
#+end_src
#+name: doom-tools
#+begin_src emacs-lisp
:tools
;;ansible
biblio
;;collab
;;debugger
direnv
;;docker
;;editorconfig
;;ein
(eval +overlay)
;;gist
(lookup +docsets)
lsp
magit
make
;;pass
pdf
;;prodigy
;;rgb
;;terraform
tree-sitter
;;tmux
;;upload
#+end_src
#+name: doom-emacs
#+begin_src emacs-lisp
:emacs
(dired +icons)
electric
(ibuffer +icons)
(undo +tree)
vc
#+end_src
#+name: doom-os
#+begin_src emacs-lisp
:os
tty
#+end_src
** Apps
Who doesn't love doing everything in Emacs?
#+name: doom-term
#+begin_src emacs-lisp
:term
vterm
#+end_src
#+name: doom-email
#+begin_src emacs-lisp
:email
(mu4e +org +gmail)
#+end_src
#+name: doom-app
#+begin_src emacs-lisp
:app
calendar
;;emms
everywhere
;;irc
;;(rss +org) ; One day...
;;twitter
#+end_src
** Language Modules
Doom Emacs provides a large collection of modules for different languages. Which
is good, because setting up language mode packages is kind of annoying.
#+name: doom-lang
#+begin_src emacs-lisp
:lang
(agda +tree-sitter +local)
;;beancount
;;(cc +lsp)
;;clojure
;;common-lisp
;;coq
;;crystal
;;csharp
data
;;(dart +flutter)
dhall
;;elixir
;;elm
emacs-lisp
;;erlang
;;ess
;;factor
;;faust
;;fortran
;;fsharp
;;fstar
;;gdscript
;;(go +lsp)
;;(graphql +lsp)
(haskell +lsp)
;;hy
idris
;;json
;;(java +lsp)
;;javascript
;;julia
;;kotlin
(latex +lsp)
;;lean
;;ledger
;;lua
markdown
;;nim
(nix +tree-sitter)
;;ocaml
(org +pretty +roam2
+gnuplot +jupyter
+pandoc +journal
+present)
;;php
;;plantuml
;;purescript
(python +lsp +tree-sitter)
;;qt
;;racket
;;raku
;;rest
;;rst
;;(ruby +rails)
(rust +lsp +tree-sitter)
(scala +lsp +tree-sitter)
;;(scheme +guile)
(sh +fish +lsp +tree-sitter)
;;sml
;;solidity
;;swift
;;terra
(web +lsp +tree-sitter)
yaml
;;zig
#+end_src
* Basic Configuration
This is mostly config settings that don't belong to any particular package and
aren't important enough to get their own major section.
** Sensible Settings
#+call: confpkg("Settings")
It wouldn't be Emacs if there wasn't an endless list of config variables to
change every aspect of its function!
#+begin_src emacs-lisp
(setq-default tab-width 2 ; 2 width tabs
delete-by-moving-to-trash t ; Delete files to trash
window-combination-resize t ; Resize windows more evenly
)
(setq compile-command "nix build"
truncate-string-ellipsis "" ; Unicode!
shell-file-name (executable-find "bash") ; Use bash instead of fish for default shell
disabled-command-function nil ; Disabled commands are a stupid idea
password-cache-expiry nil ; Security? Never heard of it
scroll-margin 2 ; A few extra lines on each end of the window
)
(global-subword-mode 1) ; Trying this out
#+end_src
Thanks once again to Tecosaur for some of these settings.
** Personal Information
#+call: confpkg()
Emacs uses this basic personal information for a few different things, mostly
applications.
#+begin_src emacs-lisp
(setq user-full-name "Kiana Sheibani"
user-mail-address "kiana.a.sheibani@gmail.com")
#+end_src
** Authentication
#+call: confpkg("Auth")
I don't want my cache files to get deleted whenever I mess up my Doom install,
so let's move them to somewhere more safe.
#+begin_src emacs-lisp
(setq auth-sources '("~/.authinfo.gpg")
auth-source-cache-expiry nil)
#+end_src
** Aesthetics
#+call: confpkg("Visual")
My favorite color theme has always been Tokyo Night. I use it literally
everywhere I can, and Doom Emacs is no exception.
#+begin_src emacs-lisp
(setq doom-theme 'doom-tokyo-night)
#+end_src
As for font choice, Victor Mono is my preferred coding font. I also use Source
Sans Pro as my sans-serif font, though that is more out of obligation than
actually liking how it looks.
#+begin_src emacs-lisp
(setq doom-font (font-spec :family "VictorMono" :size 13)
doom-variable-pitch-font (font-spec :family "Source Sans Pro" :size 16))
#+end_src
I'm a very big fan of how italics look in this font, so let's make more things
italicized! While we're here, we'll also set doom's modified buffer font to be
red instead of yellow (I like how it looks better).
#+begin_src emacs-lisp
(custom-set-faces!
'(font-lock-comment-face :slant italic)
'(font-lock-variable-name-face :slant italic)
'(doom-modeline-buffer-modified :weight bold :inherit (doom-modeline error)))
#+end_src
Some other small aesthetic changes:
#+begin_src emacs-lisp
(setq nerd-icons-scale-factor 1.1 ; Make icons slightly larger
doom-modeline-height 24 ; Make Doom's modeline taller
display-line-numbers-type t) ; Line numbers (absolute)
#+end_src
** Bindings
#+call: confpkg()
*** Windows & Workspaces
I like using window numbers to navigate between splitscreen windows, but having
to type =SPC w <#>= every time is annoying. Let's shorten that key sequence by
67%, and also throw in a convenient binding for switching to =treemacs=.
#+begin_src emacs-lisp
(map! :leader
;; Bind "SPC 0" to treemacs
;; Map window bindings to "SPC 1" through "SPC 9"
"w 0" #'treemacs-select-window
:desc "Select project tree window" "0" #'treemacs-select-window
:desc "Select window 1" "1" #'winum-select-window-1
:desc "Select window 2" "2" #'winum-select-window-2
:desc "Select window 3" "3" #'winum-select-window-3
:desc "Select window 4" "4" #'winum-select-window-4
:desc "Select window 5" "5" #'winum-select-window-5
:desc "Select window 6" "6" #'winum-select-window-6
:desc "Select window 7" "7" #'winum-select-window-7
:desc "Select window 8" "8" #'winum-select-window-8
:desc "Select window 9" "9" #'winum-select-window-9)
#+end_src
Now =SPC 1= will work equivalently to =SPC w 1=. Efficiency!
I like to reorganize my workspaces, so we can also add bindings to change the
workspace order.
#+begin_src emacs-lisp
(map! :leader
:desc "Move workspace left"
"TAB h" #'+workspace/swap-left
:desc "Move workspace right"
"TAB l" #'+workspace/swap-right)
#+end_src
*** Leader Key
It's sometimes useful to have a ~universal-argument~ binding that doesn't go
through the leader key.
#+begin_src emacs-lisp
(map! :map global-map
"M-u" #'universal-argument)
#+end_src
It's also sometimes useful to have an ~evil-ex~ binding that /does/ go through the
leader key.
#+begin_src emacs-lisp
(map! :leader
"w :" nil
":" #'evil-ex)
#+end_src
*** Evil Macros
Seeing as it's practically the Evil Emacs version of =C-g=, I often end up
accidentally pressing =q= in a non-popup buffer, which starts recording a macro.
That's very annoying, and I don't use macros enough to justify that annoyance.
#+begin_src emacs-lisp
(map! :map evil-normal-state-map
"q" nil
"C-q" #'evil-record-macro)
#+end_src
*** Creating New Projects
Whenever I want to make a new project, having to create a new directory,
initialize Git, and register it with Projectile is cumbersome. A new command to
do all of those steps in one go sounds like a good idea.
#+begin_src emacs-lisp
(defun create-new-project (dir type &optional parents)
"Create a new directory DIR and add it to the list of known projects.
TYPE specifies the type of project to create. It can take the following values:
- `git', which creates a new Git repository.
- `projectile', which creates a .projectile file in the project root.
- A string, which is used as a filename to create in the project root.
- A function, which is called with no arguments inside the root of the project.
When called interactively, this defaults to `git' unless a prefix arg is given.
If PARENTS is non-nil, the parents of the specified directory will also be created."
(interactive (list (read-directory-name "Create new project: ")
(if current-prefix-arg
(intern (completing-read "Project type: "
'("git" "projectile") nil t))
'git) t))
(make-directory dir parents)
(let ((default-directory dir))
(pcase type
('git
(shell-command "git init"))
('projectile
(make-empty-file ".projectile"))
((pred stringp)
(make-empty-file type))
((pred functionp)
(funcall type))))
(projectile-add-known-project dir))
(map! :leader
:desc "Create new project"
"p n" #'create-new-project)
#+end_src
*** Misc.
#+begin_src emacs-lisp
(map! :leader
:desc "Open URL"
"s u" #'goto-address-at-point)
#+end_src
*** ... This is Also Here
I'm not even going to bother explaining this one. Emacs is just janky sometimes
lol
#+begin_src emacs-lisp
(defadvice! ~/projectile-find-file (invalidate-cache &optional ff-variant)
:override #'projectile--find-file
(projectile-maybe-invalidate-cache invalidate-cache)
(let* ((project-root (projectile-acquire-root))
(file (read-file-name "Find file: " project-root project-root
(confirm-nonexistent-file-or-buffer) nil
))
(ff (or ff-variant #'find-file)))
(when file
(funcall ff (expand-file-name file project-root))
(run-hooks 'projectile-find-file-hook))))
#+end_src
** Automated Nix Builds
#+call: confpkg("Nix")
Some packages in this config such as =treemacs=, =org-roam=, etc. require certain
tools to be in the environment. On a Nix-based system, there are a few different
ways to handle this:
1. Put that tool in the actual environment, e.g. in a profile. This makes sense
for simple things (=ripgrep=, =sqlite=, etc) but for more opinionated things like
an instance of Python it becomes less desirable.
2. Build the tool and put a symlink to the output somewhere, e.g. in the HOME
directory. This avoids polluting the environment, but you still have to deal
with an unwieldy symlink that breaks Emacs if you accidentally delete it.
This was my approach before coming up with the third option:
3. Build the tool and point Emacs directly to the store path. This is the
simplest solution, but requires the most complex Emacs configuration.
This section is an implementation of that third solution.
We first need a function to build a flake reference:
#+begin_src emacs-lisp
(defun nix-build-out-path (out &optional impure)
"Build the given flake output OUT and return the output path.
If IMPURE is t, then allow impure builds."
(message "Building \"%s\" ..." out)
(require 's)
(s-trim (shell-command-to-string
(concat "nix build --no-link --print-out-paths "
(when impure "--impure ") out))))
#+end_src
This works well enough if we just want to build something, but there's a
problem: we haven't indicated to Nix that we want this output to stick around,
so it will be deleted the next time we garbage collect. To fix this, we can
write a wrapper function that also makes the output path a garbage collection
root.
#+begin_src emacs-lisp
(defun nix-build-out-path-gcroot (name out &optional impure)
"Build the given flake output OUT, register its output path as
a garbage collection root under NAME, and return the output path.
The GC root is placed under \"/nix/var/nix/gcroots/emacs/NAME\". If
a call to this function reuses the same NAME argument, then the
symlink is overwritten.
If IMPURE is t, then allow impure builds."
(let* ((gcdir "/nix/var/nix/gcroots/emacs")
(sym (expand-file-name name gcdir))
(path (nix-build-out-path out impure)))
(unless (equal path (file-symlink-p sym))
(require 'epg)
(make-directory (concat "/sudo::" gcdir) t)
(make-symbolic-link path (concat "/sudo::" sym) t))
path))
#+end_src
* Packages
Now that we've enabled our preferred modules and done some basic configuration,
we can install and configure our packages.
Our ~package!~ declarations go in ~packages.el~, which must not be byte-compiled:
#+begin_src emacs-lisp :tangle packages.el
;; -*- no-byte-compile: t; -*-
#+end_src
Everything else goes in ~config.el~, which is managed by [[*=confpkg=][confpkg]] as outlined
earlier.
** Company
#+call: confpkg("!Pkg company")
*** TODO Optimization
*** Bindings
When Company is active, its keybindings overshadow the default ones, meaning
keys like =RET= no longer work. To prevent this from happening, let's rebind
~company-complete-selection~ to =TAB= (less useful in the middle of typing), and
only allow =RET= to be used if Company has been explicitly interacted with.
#+begin_src emacs-lisp
(after! company
(let ((item `(menu-item nil company-complete-selection
:filter ,(lambda (cmd)
(when (company-explicit-action-p)
cmd)))))
(map! :map company-active-map
"RET" item
"<return>" item
"TAB" #'company-complete-selection
"<tab>" #'company-complete-selection
"S-TAB" #'company-complete-common)))
#+end_src
*** Spell Correction
#+call: confpkg("!Pkg company-spell")
I've been having problems with ~company-ispell~, mainly due to Ispell requiring a
text-based dictionary (unlike Aspell, which uses a binary dictionary). So let's
switch to ~company-spell~:
#+begin_src emacs-lisp :tangle packages.el
(package! company-spell)
#+end_src
#+begin_src emacs-lisp
(after! company-spell
(map! :map evil-insert-state-map
"C-x s" #'company-spell))
#+end_src
We should make sure that ~company-spell~ uses Ispell's personal dictionary too:
#+begin_src emacs-lisp
(after! (company-spell ispell)
(setq company-spell-args
(concat company-spell-args " -p " ispell-personal-dictionary)))
#+end_src
*** Icons
The ~company-box~ front-end adds support for icons, but there aren't many
providers for them, especially in text. We'll add two new icon providers:
- ~~/company-box-icons--text~, which directly targets the output of ~company-spell~
- ~~/company-box-icons--spell~, which is a fallback for all text completions
#+begin_src emacs-lisp
;; Mark candidates from `company-spell' using a text property
(defadvice! ~/company-spell-text-property (words)
:filter-return #'company-spell-lookup-words
(dolist (word words)
(put-text-property 0 1 'spell-completion-item t word))
words)
(defun ~/company-box-icons--spell (candidate)
(when (get-text-property 0 'spell-completion-item candidate)
'Text))
(defun ~/company-box-icons--text (candidate)
(when (derived-mode-p 'text-mode) 'Text))
(after! company-box
(pushnew! company-box-icons-functions #'~/company-box-icons--text)
;; `~/company-box-icons--text' is a fallback, so it has to go at the end of
;; the list
(setq company-box-icons-functions
(append company-box-icons-functions '(~/company-box-icons--text))))
#+end_src
** Eldoc
#+call: confpkg("!Pkg eldoc")
We'll switch the default docstring handler to ~eldoc-documentation-compose~, since
that provides the most information and I don't mind the space it takes up.
#+begin_src emacs-lisp
(after! eldoc
(setq eldoc-documentation-strategy 'eldoc-documentation-compose))
#+end_src
** Marginalia
#+call: confpkg("!Pkg marginalia")
Marginalia mostly works fine on its own, but we should add a few more
Doom-specific prompt categories to its registry.
#+begin_src emacs-lisp
(after! marginalia
;; Workspace and project categories
(pushnew! marginalia-prompt-categories
'("\\<workspace\\>" . workspace)
'("\\<projects?\\>" . known-project))
;; Annotate equivalently to files
(pushnew! marginalia-annotator-registry
'(known-project marginalia-annotate-file builtin none))
;; Remove special case for projectile-switch-project
;; (now covered by known-project category)
(setf (alist-get #'projectile-switch-project marginalia-command-categories nil t) nil))
#+end_src
These new categories can then be used to define [[*Keymaps][Embark keymaps]] for minibuffer
completion.
** Embark
#+call: confpkg("!Pkg embark")
When I first learned about Embark and began to use it, I was a bit disappointed
by its defaults, especially since Doom Emacs is normally great when it comes to
ensuring good defaults. I eventually went ahead and looked through every aspect
of Embark to see what needed to change.
*** Targets
Some of the targeting functions are a bit too general in what they accept. We'll
adjust the expression and identifier targeters to only work in ~prog-mode~ and the
"defun" targeter to only work in Emacs Lisp code.
#+begin_src emacs-lisp
(defun ~/embark-target-prog-mode (old-fn)
"Advise an embark target to only activate in `prog-mode'."
(when (derived-mode-p 'prog-mode) (funcall old-fn)))
(defun ~/embark-target-identifier (old-fn)
"Advise an embark target to only activate in `prog-mode' and not in `lsp-mode'."
(when (and (derived-mode-p 'prog-mode) (not (bound-and-true-p lsp-mode))) (funcall old-fn)))
(advice-add #'embark-target-expression-at-point :around #'~/embark-target-prog-mode)
(advice-add #'embark-target-identifier-at-point :around #'~/embark-target-identifier)
(after! embark
(embark-define-thingatpt-target defun emacs-lisp-mode))
#+end_src
We'll also define a word targeter, since that case was previously handled by the
identifier one.
#+begin_src emacs-lisp
(defun embark-target-word-at-point ()
"Target word at point."
(when (or (derived-mode-p 'text-mode 'help-mode 'Info-mode 'man-common)
(doom-point-in-comment-p))
(when-let ((bounds (bounds-of-thing-at-point 'word)))
(cons 'word (cons (buffer-substring (car bounds) (cdr bounds)) bounds)))))
(after! embark
(pushnew! embark-target-finders #'embark-target-word-at-point))
#+end_src
*** LSP Integration
The provided action types related to programming only apply to Emacs Lisp code,
so we'll add a new one that integrates with LSP.
#+begin_src emacs-lisp
(defun embark-target-lsp-symbol-at-point ()
"Target the LSP symbol at point."
(when (bound-and-true-p lsp-mode)
(require 'lsp-ui-doc)
;; Use hover request (meant for highlighting) to get the current symbol
(when-let ((bounds (lsp-ui-doc--extract-bounds
(lsp-request "textDocument/hover"
(lsp--text-document-position-params)))))
(cons 'lsp-symbol
(cons (buffer-substring (car bounds) (cdr bounds))
bounds)))))
(after! embark
(pushnew! embark-target-finders #'embark-target-lsp-symbol-at-point))
#+end_src
*** Hooks
The hook ~embark--mark-target~ normally sets the mark to the end and puts the
point at the beginning. This is the opposite of the usual order, so let's
override it to flip the order.
#+begin_src emacs-lisp
(after! embark
(cl-defun embark--mark-target (&rest rest &key run bounds &allow-other-keys)
"Mark the target if its BOUNDS are known.
After marking the target, call RUN with the REST of its arguments."
(cond
((and bounds run)
(save-mark-and-excursion
(set-mark (car bounds))
(goto-char (cdr bounds))
(apply run :bounds bounds rest)))
(bounds ;; used as pre- or post-action hook
(set-mark (car bounds))
(goto-char (cdr bounds)))
(run (apply run rest)))))
#+end_src
*** Actions
This
We'll be using a lot of new actions, so let's set their hooks.
#+begin_src emacs-lisp
(after! embark
(cl-pushnew #'embark--mark-target
(alist-get #'evil-change embark-around-action-hooks))
(cl-pushnew #'embark--mark-target
(alist-get #'+eval/region embark-around-action-hooks))
(cl-pushnew #'embark--mark-target
(alist-get #'+eval/region-and-replace embark-around-action-hooks))
(cl-pushnew #'embark--beginning-of-target
(alist-get #'backward-word embark-pre-action-hooks))
(cl-pushnew #'embark--end-of-target
(alist-get #'forward-word embark-pre-action-hooks))
(cl-pushnew #'embark--ignore-target
(alist-get #'lsp-rename embark-target-injection-hooks))
(cl-pushnew #'embark--ignore-target
(alist-get #'+spell/correct embark-target-injection-hooks))
(cl-pushnew #'embark--universal-argument
(alist-get #'+workspace/delete embark-pre-action-hooks))
(cl-pushnew #'embark--restart
(alist-get #'+workspace/delete embark-post-action-hooks))
(cl-pushnew #'embark--restart
(alist-get #'projectile-remove-known-project embark-post-action-hooks))
; Actions that retrigger Embark
(pushnew! embark-repeat-actions
#'lsp-ui-find-next-reference
#'lsp-ui-find-prev-reference
#'forward-word
#'backward-word
#'org-table-next-row
#'+org/table-previous-row
#'org-table-next-field
#'org-table-previous-field)
; Don't require confirmation on these actions
(setf (alist-get #'kill-buffer embark-pre-action-hooks nil t) nil
(alist-get #'embark-kill-buffer-and-window embark-pre-action-hooks nil t) nil
(alist-get #'bookmark-delete embark-pre-action-hooks nil t) nil
(alist-get #'tab-bar-close-tab-by-name embark-pre-action-hooks nil t) nil))
#+end_src
*** Keymaps
Here's the big one.
#+begin_src emacs-lisp
(defmacro ~/embark-target-wrapper (fn prompt)
"Wrap the command FN to take its argument interactively."
(let ((fsym (make-symbol (symbol-name fn))))
;;; Love me some uninterned symbols
`(progn
(defun ,fsym (ident &optional arg)
,(documentation fn)
(interactive (list (read-from-minibuffer ,prompt) current-prefix-arg))
(,fn ident arg))
#',fsym)))
(after! embark
(defvar-keymap embark-word-map
:doc "Keymap for Embark word actions."
:parent embark-general-map
"j" #'forward-word
"k" #'backward-word
"$" #'+spell/correct)
(defvar-keymap embark-lsp-symbol-map
:doc "Keymap for Embark LSP symbol actions."
:parent embark-identifier-map
"j" #'lsp-ui-find-next-reference
"k" #'lsp-ui-find-prev-reference
"r" #'lsp-rename)
(defvar-keymap embark-workspace-map
:doc "Keymap for Embark workspace actions."
:parent embark-general-map
"RET" #'+workspace/switch-to
"d" #'+workspace/delete)
(defvar-keymap embark-known-project-map
:doc "Keymap for Embark known project actions."
:parent embark-file-map
"RET" #'projectile-switch-project
"d" #'projectile-remove-known-project)
(pushnew! embark-keymap-alist
'(word . embark-word-map)
'(lsp-symbol . embark-lsp-symbol-map)
'(workspace . embark-workspace-map)
'(known-project . embark-known-project-map))
(map! (:map embark-general-map
"SPC" #'doom/leader
"C-SPC" #'embark-select
"X" #'embark-export
"W" #'+vertico/embark-export-write
"y" #'embark-copy-as-kill
"v" #'mark
"C-q" #'embark-toggle-quit
"d" #'kill-region
"c" #'evil-change
"/" #'evil-ex-search-forward
"?" #'evil-ex-search-backward
"E" nil "w" nil "q" nil "C-s" nil "C-r" nil)
(:map embark-heading-map
"v" #'mark
"V" #'outline-mark-subtree
"j" #'outline-next-visible-heading
"k" #'outline-previous-visible-heading
"J" #'outline-forward-same-level
"K" #'outline-backward-same-level
"h" #'outline-up-heading
"M-j" #'outline-move-subtree-down
"M-k" #'outline-move-subtree-up
"M-l" #'outline-demote
"M-h" #'outline-promote
"n" nil "p" nil "f" nil "b" nil "^" nil
"u" nil "C-SPC" nil)
(:map embark-prose-map
"c" #'evil-change
"u" #'downcase-region
"U" #'upcase-region
"q" #'fill-region
"C" #'capitalize-region
"l" nil "f" nil)
(:map embark-sentence-map
"j" #'forward-sentence
"k" #'backward-sentence
"n" nil "p" nil)
(:map embark-paragraph-map
"j" #'forward-paragraph
"k" #'backward-paragraph
"n" nil "p" nil)
(:map embark-identifier-map
"j" #'embark-next-symbol
"k" #'embark-previous-symbol
"d" #'kill-region
"RET" (~/embark-target-wrapper +lookup/definition "Identifier: ")
"K" (~/embark-target-wrapper +lookup/documentation "Identifier: ")
"D" (~/embark-target-wrapper +lookup/definition "Identifier: ")
"R" (~/embark-target-wrapper +lookup/references "Identifier: ")
"n" nil "p" nil "r" nil "a" nil "o" nil "H" nil "$" nil)
(:map embark-expression-map
"j" #'forward-list
"k" #'backward-list
"h" #'backward-up-list
"=" #'indent-region
"RET" #'+eval/region
"e" #'+eval/region
"E" #'+eval/region-and-replace
"TAB" nil "<" nil "u" nil "n" nil "p" nil)
(:map embark-defun-map
"c" #'evil-change
"C" #'compile-defun
"RET" nil "e" nil)
(:map embark-symbol-map
"s" nil "h" nil "d" nil "e" nil)
(:map embark-variable-map
"Y" #'embark-save-variable-value
"K" #'helpful-variable
"RET" #'+eval/region
"e" #'+eval/region
"E" #'+eval/region-and-replace
"i" #'embark-insert-variable-value
"v" #'mark
"c" #'evil-change
"<" nil)
(:map embark-function-map
"e" #'debug-on-entry
"E" #'cancel-debug-on-entry
"j" #'embark-next-symbol
"k" #'embark-previous-symbol
"K" #'helpful-callable)
(:map embark-command-map
"w" #'where-is
"b" nil "g" nil "l" nil)
(:map embark-package-map
"Y" #'embark-save-package-url
"i" #'embark-insert
"a" nil "I" nil "d" nil "r" nil "W" nil)
(:map embark-unicode-name-map
"Y" #'embark-save-unicode-character
"W" nil)
(:map embark-flymake-map
"j" #'flymake-goto-next-error
"k" #'flymake-goto-prev-error
"n" nil "p" nil)
(:map embark-tab-map
"d" #'tab-bar-close-tab-by-name)
(:map embark-region-map
"u" #'downcase-region
"U" #'upcase-region
"C" #'capitalize-region
"w" #'write-region
"W" #'count-words-region
"q" #'fill-region
"Q" #'fill-region-as-paragraph
"N" #'narrow-to-region
"D" #'delete-duplicate-lines
"=" #'indent-region
"g" #'vc-region-history
"d" #'kill-region
"c" #'evil-change
"TAB" nil "n" nil "l" nil "f" nil "p" nil
"*" nil ":" nil "_" nil)
(:map embark-file-map
"g" 'embark-vc-file-map
"w" #'embark-save-relative-path
"W" #'+vertico/embark-export-write
"Y" #'copy-file
"v" #'mark
"c" #'evil-change)
(:map embark-become-file+buffer-map
"." #'find-file
"b" #'+vertico/switch-workspace-buffer
"B" #'consult-buffer
"p" #'projectile--find-file)
(:map embark-become-help-map
"b" #'embark-bindings
"v" #'helpful-variable
"f" #'helpful-callable
"F" #'describe-face
"o" #'helpful-symbol
"s" #'helpful-symbol
"p" #'doom/help-packages)))
(after! embark-org
(map! (:map embark-org-table-cell-map
"RET" #'+org/dwim-at-point
"v" #'mark
"-" #'org-table-insert-hline
"l" #'org-table-next-field
"h" #'org-table-previous-field
"j" #'org-table-next-row
"k" #'+org/table-previous-row
"H" #'org-table-move-column-left
"L" #'org-table-move-column-right
"J" #'org-table-move-row-down
"K" #'org-table-move-row-up
(:prefix ("i" . "insert")
"h" #'+org/table-insert-column-left
"l" #'org-table-insert-column
"j" #'+org/table-insert-row-below
"k" #'org-table-insert-row
"-" #'org-table-insert-hline)
"^" nil "<" nil ">" nil "o" nil "O" nil)
(:map embark-org-table-map
"p" #'org-table-paste-rectangle
"C" #'org-table-convert
"D" #'org-table-toggle-formula-debugger
"y" #'embark-copy-as-kill
"d" #'kill-region
"c" #'evil-change)
(:map embark-org-link-copy-map
"y" #'embark-org-copy-link-in-full
"w" nil)
(:map embark-org-link-map
"e" #'org-insert-link
"y" 'embark-org-link-copy-map
"w" nil)
(:map embark-org-heading-map
">" #'org-do-demote
"<" #'org-do-promote
"j" #'org-next-visible-heading
"k" #'org-previous-visible-heading
"J" #'org-forward-heading-same-level
"K" #'org-backward-heading-same-level
"q" #'org-set-tags-command
"o" #'org-set-property
"D" #'org-cut-subtree
"s" #'org-sort
"S" #'embark-collect
"i" #'embark-insert
"d" #'kill-region
"I" #'org-insert-heading-respect-content
"l" #'org-store-link
"L" #'embark-live
(:prefix ("t" . "time")
"d" #'org-deadline
"s" #'org-schedule)
(:prefix ("c" . "clock")
"i" #'org-clock-in
"o" #'org-clock-out))
(:map embark-org-src-block-map
"v" #'org-babel-mark-block
"y" #'embark-org-copy-block-contents
"Y" #'embark-copy-as-kill
"D" #'org-babel-remove-result-one-or-many
"j" #'org-babel-next-src-block
"k" #'org-babel-previous-src-block
"e" #'org-edit-special
"=" #'org-indent-block
"c" #'evil-change)
(:map embark-org-inline-src-block-map
"e" #'org-edit-inline-src-code
"D" #'org-babel-remove-inline-result
"k" nil)
(:map embark-org-babel-call-map
"D" #'org-babel-remove-result
"k" nil)
(:map embark-org-item-map
"j" #'org-next-item
"k" #'org-previous-item
"M-j" #'org-move-item-down
"M-k" #'org-move-item-up
"c" #'evil-change
"n" nil "p" nil)
(:map embark-org-plain-list-map
"c" #'evil-change
"C" #'org-toggle-checkbox)
(:map embark-org-agenda-item-map
"RET" #'org-agenda-switch-to
"TAB" #'org-agenda-goto
"j" #'org-agenda-next-item
"k" #'org-agenda-previous-item
"d" #'org-agenda-kill
"q" #'org-agenda-set-tags
"o" #'org-agenda-set-property
(:prefix ("t" . "time")
"d" #'org-agenda-deadline
"s" #'org-agenda-schedule)
(:prefix ("c" . "clock")
"i" #'org-agenda-clock-in
"o" #'org-agenda-clock-out)
"u" nil "i" nil ":" nil "s" nil "P" nil)))
#+end_src
** Evil
#+call: confpkg("!Pkg evil")
#+begin_src emacs-lisp
(after! evil
(setq evil-shift-width 2 ; 2 width tabs (again)
evil-want-fine-undo t ; More fine-grained undos
evil-ex-substitute-global t ; s/../../ is global by default
evil-kill-on-visual-paste nil ; Don't copy text overwritten on paste
))
#+end_src
While we're here, we'll also set my preferred =evil-escape= keys:
#+begin_src emacs-lisp
(after! evil-escape
(setq evil-escape-key-sequence "fd"))
#+end_src
** Flymake
#+call: confpkg("!Pkg flymake")
I really like Flycheck's double-arrow fringe indicator, so let's quickly steal
that:
#+begin_src emacs-lisp
(after! flymake
(define-fringe-bitmap 'flymake-double-left-arrow
[#b00011011
#b00110110
#b01101100
#b11011000
#b01101100
#b00110110
#b00011011])
(setf (car flymake-error-bitmap) 'flymake-double-left-arrow
(car flymake-warning-bitmap) 'flymake-double-left-arrow
(car flymake-note-bitmap) 'flymake-double-left-arrow))
#+end_src
Flymake normally uses italics for warnings, but my italics font being cursive
makes that a bit too visually noisy.
#+begin_src emacs-lisp
(custom-set-faces!
'(compilation-warning :slant normal :weight bold)
'(flymake-note-echo :underline nil :inherit compilation-info))
#+end_src
And just to make sure nothing else accidentally starts running:
#+begin_src emacs-lisp :tangle packages.el :noweb-ref none
(package! flycheck :disable t)
(package! flyspell :disable t)
#+end_src
*** Bindings
#+begin_src emacs-lisp
(map! :leader
:desc "Open errors buffer"
"c X" #'flymake-show-project-diagnostics)
#+end_src
*** Tooltips
Having an IDE-style tooltip pop up is nice, but ~flymake-popon~ is a bit ugly by default.
#+begin_src emacs-lisp
(after! flymake-popon
; Widen popon
(setq flymake-popon-width 120)
; Add visible border
(set-face-foreground 'flymake-popon-posframe-border (doom-color 'selection)))
#+end_src
*** Popups
#+begin_src emacs-lisp
(after! flymake
(set-popup-rule! "^\\*Flymake" :vslot 1 :side 'bottom))
#+end_src
** Indent Guides
#+call: confpkg("!Pkg highlight-indent-guides")
I've found that character-based indent guides work best.
#+begin_src emacs-lisp
(after! highlight-indent-guides
(setq highlight-indent-guides-method 'character
highlight-indent-guides-character 9615
highlight-indent-guides-responsive 'top
highlight-indent-guides-auto-character-face-perc 90
highlight-indent-guides-auto-top-character-face-perc 200))
#+end_src
** Language Servers
#+call: confpkg("!Pkg lsp")
~lsp-mode~ requires ~avy~, but doesn't load it for some reason.
#+begin_src emacs-lisp
;; (advice-add #'lsp-avy-lens :before (cmd! (require 'avy)))
#+end_src
Here's a convenient leader key binding as well:
#+begin_src emacs-lisp
(map! :leader
:desc "Select LSP code lens"
"c L" #'lsp-avy-lens)
#+end_src
** Magit
#+call: confpkg("!Pkg magit")
[[https://magit.vc/][Magit]] is already great, but it could use some proper syntax highlighting!
#+begin_src emacs-lisp :tangle packages.el
(package! magit-delta)
#+end_src
#+begin_src emacs-lisp
(use-package! magit-delta
:hook (magit-mode . magit-delta-mode))
#+end_src
** Treemacs
#+call: confpkg("!Pkg treemacs")
Treemacs is a really useful package, but it also has a lot of defaults I don't
like. Let's add a ~use-package!~ declaration to fix some of them:
#+begin_src emacs-lisp
(use-package! treemacs
:defer t
:init
; More accurate git status
(setq +treemacs-git-mode 'deferred
treemacs-python-executable
(concat (nix-build-out-path-gcroot "treemacs-python" "nixpkgs#python3")
"/bin/python"))
:config
(setq ; Child-frame reading is broken (and sucks anyways)
treemacs-read-string-input 'from-minibuffer
; Make "SPC 0" work like other window select commands
treemacs-select-when-already-in-treemacs 'stay)
; Better font styling
(custom-set-faces!
; Variable pitch fonts
'((treemacs-root-face
treemacs-file-face) :inherit variable-pitch)
'(treemacs-tags-face :height 0.95 :inherit variable-pitch)
'(treemacs-directory-face :inherit treemacs-file-face)
'((treemacs-git-added-face
treemacs-git-modified-face
treemacs-git-renamed-face
treemacs-git-conflict-face) :inherit treemacs-file-face)
; Better colors
`(treemacs-git-ignored-face
:foreground ,(doom-color 'base1) :slant italic :inherit treemacs-file-face)
`(treemacs-git-untracked-face
:foreground ,(doom-color 'base1) :inherit treemacs-file-face)
'(treemacs-async-loading-face
:height 0.8 :inherit (font-lock-comment-face treemacs-file-face)))
(treemacs-hide-gitignored-files-mode) ; Hide git-ignored files by default
(treemacs-fringe-indicator-mode -1) ; No fringe indicator
(treemacs-resize-icons 16) ; Make icons smaller
)
#+end_src
*** Project Integration
I often accidentally open the project tree before I've even selected a project,
which I don't want because it messes up =treemacs-projectile=. Let's fix that
problem:
#+begin_src emacs-lisp
(defun ~/treemacs-restrict (&rest _)
(unless (doom-project-p)
(user-error "Must be in a project to open project tree")))
(advice-add #'treemacs-select-window :before #'~/treemacs-restrict)
(advice-add #'+treemacs/toggle :before #'~/treemacs-restrict)
#+end_src
When I do have a project open, Treemacs is flexible and allows you to open
directories other than that project. This /would/ be great and convenient if it
weren't for the fact that it doesn't do so very well, often opening the wrong
directories entirely. This convenience function ensures that only the project
directory is open.
#+begin_src emacs-lisp
(defun ~/treemacs-fix-project ()
"Modify the current `treemacs' workspace to only include the current project."
(interactive)
(require 'treemacs)
(let* ((name (concat "Perspective " (doom-project-name)))
(project (treemacs-project->create! :name (doom-project-name) :path (directory-file-name (doom-project-root))
:path-status 'local-readable :is-disabled? nil))
(workspace (treemacs-workspace->create! :name name :projects (list project) :is-disabled? nil)))
;; Only rebuild workspace if it doesn't have the structure we expect
(unless (equal (treemacs-current-workspace) workspace)
(setq treemacs--workspaces
(append (remove-if (lambda (w) (string= (treemacs-workspace->name w) name))
treemacs--workspaces)
(list workspace)))
(treemacs-do-switch-workspace workspace)
(treemacs--invalidate-buffer-project-cache)
(treemacs--rerender-after-workspace-change))))
#+end_src
** TODO VTerm
#+call: confpkg("!Pkg vterm")
Set ~vterm~ to use =fish= as its shell:
#+begin_src emacs-lisp
(after! vterm
(setq-default vterm-shell (executable-find "fish")))
#+end_src
** Operation Hints
I like having hints that show how large the editing operation I just performed
was, but the =ophints= module in Doom doesn't look very good to me (it gets rid of
pulses and color), so I'll override it.
#+begin_src emacs-lisp :tangle modules/ui/ophints/packages.el
;; -*- no-byte-compile: t; -*-
;;; ui/ophints/packages.el
(package! evil-goggles)
#+end_src
#+begin_src emacs-lisp :tangle modules/ui/ophints/config.el
;;; -*- lexical-binding: t; -*-
;;; ui/ophints/config.el
(use-package! evil-goggles
:hook (doom-first-input . evil-goggles-mode)
:init
(setq evil-goggles-duration 0.15
evil-goggles-blocking-duration 0.12
evil-goggles-async-duration 0.2)
:config
(pushnew! evil-goggles--commands
'(evil-magit-yank-whole-line
:face evil-goggles-yank-face
:switch evil-goggles-enable-yank
:advice evil-goggles--generic-async-advice)
'(+evil:yank-unindented
:face evil-goggles-yank-face
:switch evil-goggles-enable-yank
:advice evil-goggles--generic-async-advice)
'(+eval:region
:face evil-goggles-yank-face
:switch evil-goggles-enable-yank
:advice evil-goggles--generic-async-advice)
'(evil-fill
:face evil-goggles-fill-and-move-face
:switch evil-goggles-enable-fill-and-move
:advice evil-goggles--generic-async-advice)
'(evil-fill-and-move
:face evil-goggles-fill-and-move-face
:switch evil-goggles-enable-fill-and-move
:advice evil-goggles--generic-async-advice))
(custom-set-faces! '(evil-goggles-default-face :background "#2b3a7f")
'(evil-goggles-delete-face :inherit magit-diff-removed-highlight)
'(evil-goggles-paste-face :inherit magit-diff-added-highlight)
'(evil-goggles-change-face :inherit evil-goggles-delete-face)))
#+end_src
** YASnippet
#+call: confpkg()
Allow nested snippets:
#+begin_src emacs-lisp
(after! yasnippet
(setq yas-triggers-in-field t))
#+end_src
* Applications
** Calculator
#+call: confpkg("Calc")
Emacs Calc is the best calculator I've ever used, and given the fact that it's
an RPN calculator, that's saying something.
*** Leader Key Bindings
Typing =C-x *= every time I want to use Calc (very often) is annoying.
#+begin_src emacs-lisp
(map! :leader
:prefix ("#" . "calc")
:desc "Emacs Calc"
"#" #'calc
:desc "Emacs Calc"
"c" #'calc
:desc "Emacs Calc (full window)"
"C" #'full-calc
:desc "Quick Calc"
"q" #'quick-calc
:desc "Keypad"
"k" #'calc-keypad
:desc "Grab region into Calc"
"g" #'~/calc-grab-region
:desc "Paste from stack"
"y" #'calc-copy-to-buffer
:desc "Read keyboard macro"
"m" #'read-kbd-macro
(:prefix ("e" . "embedded")
:desc "Embedded mode"
"e" #'calc-embedded
:desc "Embedded mode (select)"
"s" #'calc-embedded-select
:desc "Embedded mode (word)"
"w" #'calc-embedded-word
:desc "Activate special operators"
"a" #'calc-embedded-activate
:desc "Duplicate formula at point"
"d" #'calc-embedded-duplicate
:desc "New formula"
"f" #'calc-embedded-new-formula
:desc "Next formula"
"j" #'calc-embedded-next
:desc "Previous formula"
"k" #'calc-embedded-previous
:desc "Refresh formula at point"
"r" #'calc-embedded-update-formula
:desc "Edit formula at point"
"`" #'calc-embedded-edit))
#+end_src
For the grab-region command, I think it makes sense to have it check whether
your selection is a rectangle (=C-v=):
#+begin_src emacs-lisp
(defun ~/calc-grab-region (top bot &optional arg)
"Perform either `calc-grab-region' or `calc-grab-rectangle' depending on
what type of visual state is currently active."
(interactive "r\nP")
(if (eq (evil-visual-type) 'block)
(calc-grab-rectangle top bot arg)
(calc-grab-region top bot arg)))
#+end_src
*** Evil Bindings
I want to have evil-esque keybindings in Calc, so let's enable the
=evil-collection= module for it. I haven't found a better way to do this than to
edit the relevant variable in ~init.el~:
#+begin_src emacs-lisp :tangle init.el :noweb-ref none
;; Enable evil-collection-calc
(setq +evil-collection-disabled-list
'(anaconda-mode
buff-menu
comint
company
custom
eldoc
elisp-mode
ert
free-keys
helm
help
indent
image
kotlin-mode
outline
replace
shortdoc
simple
slime
lispy))
#+end_src
Let's also rebind some keys. Preserving evil's =[= and =]= bindings doesn't make
sense to me, and =C-r= makes more sense as a redo binding than =D D=.
#+begin_src emacs-lisp
(defadvice! ~/evil-collection-calc-bindings ()
:after #'evil-collection-calc-setup
(map! :map calc-mode-map
:n "C-r" #'calc-redo
:n "[" #'calc-begin-vector
:n "]" #'calc-end-vector))
#+end_src
*** Appearance
Calc doesn't use faces to show selections by default, which I think is rather
strange.
#+begin_src emacs-lisp
(after! calc
(setq calc-highlight-selections-with-faces t
calc-show-selections nil)
(custom-set-faces!
`(calc-selected-face :weight extra-bold :foreground ,(doom-color 'highlight))
`(calc-nonselected-face :weight semi-light :foreground ,(doom-color 'comments))))
#+end_src
*** Other Defaults
#+begin_src emacs-lisp
(after! calc
(setq calc-window-height 13 ; Make window taller
calc-angle-mode 'rad ; Default to radians
calc-symbolic-mode t ; Symbolic evaluation
))
#+end_src
** TODO Mail
#+call: confpkg()
I use =isync=, =msmtp= and =mu= as Doom Emacs recommends.
#+begin_src emacs-lisp
(after! mu4e
(setq sendmail-program (executable-find "msmtp")
send-mail-function #'smtpmail-send-it
message-sendmail-f-is-evil t
message-sendmail-extra-arguments '("--read-envelope-from")
message-send-mail-function #'message-send-mail-with-sendmail))
#+end_src
*** Accounts
#+begin_src emacs-lisp
(set-email-account! "gmail"
'((mu4e-sent-folder . "/gmail/[Gmail]/Sent Mail")
(mu4e-drafts-folder . "/gmail/[Gmail]/Drafts")
(mu4e-trash-folder . "/gmail/[Gmail]/Trash")
(mu4e-refile-folder . "/gmail/[Gmail]/All Mail")
(smtpmail-smtp-user . "kiana.a.sheibani@gmail.com"))
t)
#+end_src
** Calendar
#+call: confpkg()
The calendar's main purpose for me is to give a better view of the [[*Agenda][Org agenda]].
#+begin_src emacs-lisp
(after! calfw
(setq calendar-week-start-day 1) ; Start week on Monday
(setq cfw:org-face-agenda-item-foreground-color (doom-color 'magenta)))
(map! :leader
:desc "Calendar"
"o c" #'cfw:open-org-calendar)
#+end_src
* Org
#+call: confpkg()
I love ~org-mode~. In fact, I love it so much that I'm willing to give it its own
top-level section in this config! Its power and flexibility are unmatched by any
other productivity/organization tool I've ever used. Much like Emacs itself, all
alternatives simply vanish.
Unfortunately, with that power comes a *lot* of configuration work up-front. It
was completely worth it for me when I made it out the other end, but that
doesn't mean everyone would have the time or patience to make it work.
** Basic Configuration
#+begin_src emacs-lisp
(setq org-directory "~/org/")
(after! org
(setq org-archive-location ; Global archive file
(concat org-directory ".org_archive::")
org-cycle-emulate-tab nil ; We don't need this with evil
org-attach-dir-relative t
org-log-into-drawer t ; Write logs into :LOGBOOK:
org-footnote-auto-label 'confirm ; Allow editing of footnote names
org-startup-with-inline-images t ; Do more stuff on startup
org-startup-with-latex-preview t
+org-startup-with-animated-gifs t
org-format-latex-options ; Make latex preview smaller
(plist-put org-format-latex-options :scale 0.55)
;; Todo Keywords
org-todo-keywords
'((sequence "TODO(t)" "STRT(s)" "WAIT(w)" "|" "DONE(d)")
(sequence "PROJ(p)" "NEXT(n)" "WORK(o!)" "HOLD(h@/!)" "|" "FIN(f!/@)")
(sequence "|" "KILL(k@)"))
org-todo-keyword-faces
'(("STRT" . +org-todo-active)
("WAIT" . +org-todo-onhold)
("KILL" . +org-todo-cancel)
("PROJ" . +org-todo-project)
("WORK" . +org-todo-active)
("HOLD" . +org-todo-onhold))
;; Customize appearance
org-hide-emphasis-markers t
org-hide-leading-stars nil
org-superstar-item-bullet-alist '((42 . 8226)
(43 . 8226)
(45 . 8226))))
#+end_src
*** Bindings
**** Convenience
There are a few useful functions Doom doesn't bind by default, so let's add them
for convenience.
#+begin_src emacs-lisp
(map! :after org
:map org-mode-map
:localleader
"N" #'org-num-mode
"C" #'org-columns
"p" #'org-priority ; Remove extraneous commands
"c D" #'org-clock-display
"m b f" #'org-table-eval-formula
"m b F" #'org-table-edit-formulas
;; Map babel commands into localleader
:desc "babel"
"v" (lookup-key org-mode-map (kbd "C-c C-v")))
#+end_src
*** Project Links
It's sometimes nice to be able to click a link in an Org file that takes me to
one of my projects.
#+begin_src emacs-lisp
(defun org-projectile-follow (path _)
"Open a projectile link to PATH."
(projectile-switch-project-by-name path))
(defun org-projectile-completion (&optional arg)
(let ((project (completing-read "Project: " projectile-known-projects nil 'confirm)))
(concat "projectile:" project)))
(after! org
(org-link-set-parameters "projectile"
:follow #'org-projectile-follow
:complete #'org-projectile-completion))
#+end_src
*** Export Directory
Org mode by default exports to the same directory the org-mode file is in. This
is inconvenient for me, as I use a lot of subdirectories. To fix this, we can
advise the function ~org-export-output-file-name~.
#+begin_src emacs-lisp
(defvar org-export-dir (expand-file-name "export/" org-directory)
"The directory to export Org mode files to.
If nil, then `default-directory' for the org buffer is used.")
(defadvice! ~/modify-org-export-dir (orig-fn extension &optional subtreep pub-dir)
:around #'org-export-output-file-name
(unless pub-dir
(setq pub-dir org-export-dir))
(unless (file-directory-p pub-dir)
(make-directory pub-dir t))
(funcall orig-fn extension subtreep pub-dir))
#+end_src
** Tags
Org mode offers a useful tag hierarchy system, configured via ~org-tag-alist~.
We'll be using ~org-tag-persistent-alist~ instead so that our tag hierarchy can't
be overwritten.
#+begin_src emacs-lisp
(defvar classes-mwf '()
"Classes that belong under the :MWF: tag.")
(defvar classes-tr '()
"Classes that belong under the :TR: tag.")
(defvar classes-online '()
"Classes that belong under the :Online: tag.")
(after! org
(setq classes-mwf '(("HIST1111" . ?1))
classes-tr '(("MATH2203" . ?2))
classes-online '(("HIST2111" . ?3)))
(setq org-tag-persistent-alist
`(("area" . ?A) ("goal" . ?G) ("project" . ?P) ("meta" . ?M)
(:newline)
;; Topics
("economics") ("polsci") ("math") ("history")
(:startgrouptag) ("math")
(:grouptags) ("calculus") ("algebra") (:endgrouptag)
;; Classes
(:startgrouptag) ("college")
(:grouptags) ("assign") ("notes") (:endgrouptag)
(:startgroup) ("college")
(:grouptags) ("TR") ("MWF") ("Online") (:endgroup)
(:startgroup) ("MWF")
(:grouptags) ,@classes-mwf (:endgroup)
(:startgroup) ("TR")
(:grouptags) ,@classes-tr (:endgroup)
(:startgroup) ("Online")
(:grouptags) ,@classes-online (:endgroup))))
#+end_src
** Org Roam
#+call: confpkg()
When I started out using Org mode, I just used vanilla Org files to manage my
notes. This worked, but as my notes grew more and more I've begun to
increasingly rely on [[https://www.orgroam.com/][Org-roam]] to more systematically manage my organization.
*** Concept
Org-roam is inspired by Roam Research, and like that tool it is based on the
Zettelkasten (slip-box) note-taking method. In the Zettelkasten method, notes
and concepts are separated into small chunks (called "nodes" in Org-roam
terminology). These notes can live freely in any file on your system, and are
linked to each other through ID links, leading to a freer note system that isn't
tied to any particular organizational structure or hierarchy.
*** Task Management
In my use of Org-roam for task management, I divide nodes into a few different
categories:
1. *Areas*, which represent continual areas of your life to organize and plan;
2. *Goals*, short- or long-term, things that can be completed;
3. *Tasks*, which are one-time and contribute to goals or areas.
Areas are stored as subnodes of the =Areas= file node, and likewise for goals.
They also have the =:area:= and =:goal:= tags respectively. A task is a node that is
a TODO entry that links to an area or a goal. We can thus check for if a node is
a task by checking if the node links to a =:area:= or =:goal:= tagged node.
#+begin_src emacs-lisp
(defun ~/org-roam-get-linked-nodes (node tag)
"Return the nodes that NODE links to that are tagged with TAG."
(let* ((response (org-roam-db-query [:select :distinct [dest]
:from links
:where (= source $s1)
:and (= type "id")
:group :by dest]
(org-roam-node-id node)))
(ids (mapcar #'car response)))
(--keep (let ((node (org-roam-node-from-id it)))
(when (-contains? (org-roam-node-tags node) tag)
node)) ids)))
#+end_src
*** Roam Buffer
The unlinked references section is turned off by default for performance
reasons, but I've never had any serious issues with it. Let's turn that on, and
also make sure backlinks are unique.
#+begin_src emacs-lisp
(after! org-roam
(setq +org-roam-auto-backlinks-buffer t
org-roam-mode-sections
'((org-roam-backlinks-section :unique t)
org-roam-reflinks-section
org-roam-unlinked-references-section)))
#+end_src
*** Roam Capture
Creating new nodes should be quick and easy, so we should stick to one template
to avoid the hassle of choosing.
#+begin_src emacs-lisp
(defun org-roam-node-file-maybe (node &optional dir)
"Get file name from NODE, or return a default filename in directory DIR."
(unless dir (setq dir org-roam-directory))
(or (org-roam-node-file node)
(expand-file-name (concat "%<%Y%m%d%H%M%S>-" (org-roam-node-slug node) ".org")
dir)))
(defun org-roam-node-file-maybe-pick-dir (node)
"Get file name from NODE, or ask for directory and return a default filename."
(or (org-roam-node-file node)
(expand-file-name (concat "%<%Y%m%d%H%M%S>-" (org-roam-node-slug node) ".org")
(read-directory-name "Directory: " org-roam-directory))))
(after! org-roam
(setq org-roam-capture-templates
'(("d" "Default" plain "%?"
:target (file+head "${file-maybe-pick-dir}"
"#+title: ${title}\n#+filetags:")
:unnarrowed t))
org-roam-dailies-capture-templates
'(("d" "Default" entry "* %?"
:target (file+head "%<%Y-%m-%d>.org"
"#+title: %<%Y-%m-%d>")))))
#+end_src
*** Roam Links
Making links to Roam nodes is a bit finicky. This helps fix some of that.
#+begin_src emacs-lisp
(defun org-roam-completion (&optional arg)
(let ((node (org-roam-node-read nil nil nil t)))
(concat "id:" (org-roam-node-id node))))
(defun org-roam-insert-description (idstr)
(org-roam-node-title (org-roam-node-from-id (substring idstr 3))))
(after! org
(org-link-set-parameters "roam"
:complete #'org-roam-completion
:insert-description #'org-roam-insert-description))
#+end_src
** TODO Capture Templates
#+begin_src emacs-lisp
(defun ~/org-project-find-heading ()
"Find heading in org project file."
(beginning-of-buffer)
;; (unless (string-match-p "\\`\\s-*$" (thing-at-point 'line))
;; (insert "\n")
;; (beginning-of-buffer))
(when (y-or-n-p "Insert project at heading? ")
(require 'consult-org)
;; Prevent consult from trying to recenter the window
;; after capture has already hidden the buffer
(let (consult-after-jump-hook)
(consult--read
(consult--slow-operation "Collecting headings..."
(or (consult-org--headings nil "-project" nil)
(user-error "No headings")))
:prompt "Heading: "
:category 'consult-org-heading
:sort nil
:require-match t
:history '(:input consult-org--history)
:narrow (consult-org--narrow)
:state (consult--jump-state)
:group nil
:lookup #'consult--lookup-candidate))))
(after! org
(setq org-capture-templates
'(("t" "Task")
("tt" "Task" entry (file+headline "events.org" "Tasks")
"* TODO %?" :empty-lines 1)
("td" "Task with Deadline" entry (file+headline "events.org" "Tasks")
"* TODO %?\nDEADLINE: %^{Deadline}T" :empty-lines 1)
("tD" "Task with Deadline (date only)" entry (file+headline "events.org" "Tasks")
"* TODO %?\nDEADLINE: %^{Deadline}t" :empty-lines 1)
("ts" "Scheduled Task" entry (file+headline "events.org" "Tasks")
"* TODO %?\nSCHEDULED: %^{Time}T" :empty-lines 1)
("tS" "Scheduled Task (date only)" entry (file+headline "events.org" "Tasks")
"* TODO %?\nSCHEDULED: %^{Date}t" :empty-lines 1)
("e" "Event" entry (file+headline "events.org" "Events")
"* %?\n%^T" :empty-lines 1)
("E" "Event (date only)" entry (file+headline "events.org" "Events")
"* %?\n$^t" :empty-lines 1)
("p" "Project" entry (file+function "projects.org" ~/org-project-find-heading)
"* PROJ %? :project:\n:PROPERTIES:\n:VISIBILITY: folded\n:END:
:LOGBOOK:\n- Created %U\n:END:"
:empty-lines 1))))
#+end_src
** Agenda
#+call: confpkg("Org Agenda")
*** Configuration
A full week-long agenda is usually too cluttered for me to read, so I'll narrow
it down to a single day. I also like the week to start on Monday.
#+begin_src emacs-lisp
(after! org
(setq org-agenda-span 'day
org-agenda-start-day nil
org-agenda-start-on-weekday 1 ; 1 = Monday
org-agenda-sorting-strategy
'((agenda habit-down time-up urgency-down category-up)
(todo urgency-down time-up category-up)
(tags urgency-down time-up category-up)
(search category-up))))
#+end_src
*** Agenda View
The Org agenda is a very nice feature, but by default it doesn't really provide
enough customization to fit my needs. I like to have nice categories to make
parsing my todos easier, so we'll use ~org-super-agenda~:
#+begin_src emacs-lisp :tangle packages.el :noweb-ref none
(package! org-super-agenda)
#+end_src
#+begin_src emacs-lisp
(use-package! org-super-agenda
:commands org-super-agenda-mode)
(after! org-agenda
(let ((inhibit-message t))
(org-super-agenda-mode))
;; This map is unnecessary and causes evil bindings to not work
;; while on super agenda headers
(setq org-super-agenda-header-map nil))
#+end_src
The ~org-agenda~ dispatcher is occasionally useful, but most of the time when I
want to open my agenda, it's to see my "preferred" view.
#+begin_src emacs-lisp
(defun ~/org-agenda-section-by-link (prefix tag item)
"Org super-agenda function to categorize agenda entries by linked node with TAG."
(when-let* ((marker (org-super-agenda--get-marker item))
(node (org-super-agenda--when-with-marker-buffer marker
(org-roam-node-at-point)))
(links (~/org-roam-get-linked-nodes node tag)))
(->> links
(mapcar #'org-roam-node-title)
(-interpose ", ")
(apply #'concat prefix))))
(after! org
(setq org-agenda-custom-commands
'(("o" "Overview"
((agenda "")
(alltodo ""
((org-super-agenda-groups
'((:discard (:todo "PROJ"))
(:name "Important"
:priority "A")
(:name "Assignments"
:tag "assign"
:order 1)
(:auto-map (lambda (item)
(~/org-agenda-section-by-link "Goal: " "goal" item))
:order 2)
(:auto-map (lambda (item)
(~/org-agenda-section-by-link "Area: " "area" item))
:order 3)
(:name "Notes to Intake"
:tag "notes"
:order 4)
(:name "Projects"
:and (:tag "project"
:todo ("WORK" "HOLD"))
:order 5))))))))
;; In case I ever use this
org-stuck-projects
'("project/!-TODO-STRT-WAIT-DONE"
("PROJ" "NEXT" "FIN" "KILL")
nil "")))
(defun ~/org-agenda (&optional arg)
"Wrapper around preferred agenda view."
(interactive "P")
(org-agenda arg "o"))
(map! :leader
:desc "Org agenda"
"o a" #'~/org-agenda
:desc "Org agenda dispatcher" ; Use shift to access full dispatcher
"o A" #'org-agenda)
#+end_src
*** Agenda Files
I have a lot of different subdirectories and groupings in my org directory, but
unfortunately directories listed in ~org-agenda-files~ aren't checked recursively!
I haven't yet found out how to solve this problem directly, so instead I'm going
to mitigate it somewhat by recursively adding every subdirectory of my org
directory to ~org-agenda-files~.
#+begin_src emacs-lisp
(defun directory-dirs (dirs)
"Recursively find all subdirectories of DIRS, ignoring dotfiles."
(when dirs
(let (result)
(dolist (dir dirs)
(let ((dir (directory-file-name dir))
(files (directory-files dir nil nil t)))
(dolist (file files)
(unless (= (aref file 0) ?.)
(let ((file (concat dir "/" file)))
(when (file-directory-p file)
(setq result (cons file result))))))))
(append dirs (directory-dirs result)))))
(defvar org-agenda-files-function #'org-agenda-files-function
"The function to determine the org agenda files.")
(defun org-agenda-files-function (get-dirs)
(funcall get-dirs (list org-directory)))
(defun ~/org-agenda-files-update (&optional fn)
"Populate `org-agenda-files' with the result of calling FN, or
`org-agenda-files-function' by default."
(interactive)
(unless fn
(setq fn org-agenda-files-function))
(setq org-agenda-files (funcall fn #'directory-dirs)))
(after! org (~/org-agenda-files-update))
#+end_src
** Citations
#+call: confpkg("Org Cite")
Org mode has a very robust system for specifying citations, one which is taken
advantage of by the package =citar=.
Let's start with some configuration. I use [[https://www.zotero.org/][Zotero]] to manage my citations, and
when I want to use them in Org mode I export them to a file =library.json= (CSL
JSON) in my org directory.
#+begin_src emacs-lisp
(after! org
(setq org-cite-csl-styles-dir "~/Zotero/styles"
org-cite-csl--fallback-style-file "/home/kiana/Zotero/styles/modern-language-styles.csl"
org-cite-global-bibliography (list (expand-file-name "library.json" org-directory))
citar-bibliography org-cite-global-bibliography))
#+end_src
And we should also make it look a little prettier:
#+begin_src emacs-lisp
;; Make faces conform to theme
(after! org
(custom-set-faces!
`(org-cite :foreground ,(doom-color 'green))
`(org-cite-key :slant italic :foreground ,(doom-color 'green))))
;; Citar icons
(after! citar
(setq citar-indicators
(list
(citar-indicator-create
:symbol (nerd-icons-mdicon "nf-md-link"
:face 'nerd-icons-lblue)
:padding " "
:function #'citar-has-links
:tag "has:links")
(citar-indicator-create
:symbol (nerd-icons-mdicon "nf-md-file"
:face 'nerd-icons-lred)
:padding " "
:function #'citar-has-files
:tag "has:files")
(citar-indicator-create
:symbol (nerd-icons-mdicon "nf-md-note_text"
:face 'nerd-icons-blue)
:padding " "
:function #'citar-has-notes
:tag "has:notes")
(citar-indicator-create
:symbol (nerd-icons-mdicon "nf-md-check"
:face 'nerd-icons-lgreen)
:padding " "
:function #'citar-is-cited
:tag "is:cited"))))
#+end_src
** Journal
#+call: confpkg("Org Journal")
I don't use ~org-journal~ anymore, but I'm keeping my old configuration for it in
case I want to go back.
#+begin_src emacs-lisp
(after! org-journal
;; One entry per day, no separation
(setq org-journal-file-format "%Y-%m-%d"
org-journal-hide-entries-p nil))
#+end_src
To make opening the journal more convenient, here's a command to open the latest
entry:
#+begin_src emacs-lisp
(defun +org/org-journal-open-latest ()
(interactive)
(require 'org-journal)
(funcall org-journal-find-file
(car (last (seq-filter #'file-regular-p
(directory-files org-journal-dir t))))))
(map! :leader
:desc "Journal"
"o j" #'+org/org-journal-open-latest)
#+end_src
* Languages and Modes
Despite Emacs being my editor of choice for programming, I don't actually have a
lot of configuration for programming languages. I suppose that this is because
language packages tend to not need much configuration, as the bounds of what a
language mode needs to do are typically defined by the language itself.
** Haskell
#+call: confpkg("!Mode haskell")
Operators being in italics looks ugly, so let's fix that.
#+begin_src emacs-lisp
(after! haskell-mode
(custom-set-faces! '(haskell-operator-face :slant normal)))
#+end_src
** Dired
#+call: confpkg("!Mode dired")
Dired by default spawns a new buffer for every directory, which clutters up your
buffer list very quickly.
#+begin_src emacs-lisp
(after! dired
(setq dired-kill-when-opening-new-dired-buffer t))
#+end_src
** Prose
#+call: confpkg("!Mode text")
I like having ~auto-fill-mode~ on while writing text:
#+begin_src emacs-lisp
(add-hook! text-mode #'auto-fill-mode)
#+end_src