doom-config/config.org

4254 lines
163 KiB
Org Mode
Raw Normal View History

2024-02-19 18:00:05 -05:00
#+title: Doom Emacs Config
#+author: tokinanpa
#+email: kiana.a.sheibani@gmail.com
2024-02-19 20:42:42 -05:00
#+property: header-args:elisp :results replace :exports code
2024-03-02 17:22:24 -05:00
#+property: header-args :tangle no :results silent :eval no-export :mkdirp yes
2024-02-19 18:00:05 -05:00
#+begin_quote
2024-03-05 03:03:24 -05:00
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.
2024-02-19 18:00:05 -05:00
-- Neil Stephenson, /In the Beginning was the Command Line/ (1998)
#+end_quote
2024-04-09 14:16:06 -04:00
* Introduction
2024-02-19 18:00:05 -05:00
*Hello!*
This is a literate configuration for [[https:github.com/doomemacs/doomemacs][Doom Emacs]].
** Background; or, My Emacs Story
2024-03-05 03:03:24 -05:00
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.
2024-02-19 18:00:05 -05:00
2024-03-05 03:03:24 -05:00
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.
2024-02-19 18:00:05 -05:00
*** In The Beginning
2024-03-05 03:03:24 -05:00
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.
2024-02-19 18:00:05 -05:00
2024-03-30 21:02:34 -04:00
#+caption: Look at this and tell me that it doesn't look at least a little awful.
2024-02-19 18:00:05 -05:00
#+name: vanilla-emacs
[[file:assets/vanilla_emacs.png]]
2024-03-05 03:03:24 -05:00
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.
2024-02-19 18:00:05 -05:00
2024-03-05 03:03:24 -05:00
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:
2024-02-19 18:00:05 -05:00
- Extreme flexibility
- Robust modular configuration system
- Sensible defaults
- Extensive ecosystem
2024-03-05 03:03:24 -05:00
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.
2024-02-19 18:00:05 -05:00
*** Literate Programming
2024-02-19 18:00:05 -05:00
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 around 1200 lines of code before I decided that something needed to be done.
2024-02-19 18:00:05 -05:00
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 Emacs code seemed like a good idea.
**** Org and Source Code
This file is written in Org, a plain-text markup language similar to Markdown in basic structure. Like Markdown, it has support for embedding blocks of source code in between paragraphs.
Unlike a standard markup language, however, Org mode has built-in capabilities for executing code and inserting the output into the document, somewhat similar to a Jupyter notebook.
#+begin_src emacs-lisp :eval yes :exports both :results replace
;; This is Emacs Lisp code in a src block!
(format "Result: %s" (+ 1 2))
#+end_src
#+RESULTS:
: Result: 3
The second block above was automatically generated by =org-mode=.
2024-04-01 23:33:27 -04:00
(This feature does not exclusively work with Emacs Lisp, but other languages require the corresponding build tools to be installed.)
**** Tangle, Weave, Export, Publish
2024-04-01 23:33:27 -04:00
Since Org's ability to execute code and process its output is so robust, it's only natural that one might consider using Org to annotate a codebase, storing the code inside source blocks in the document. This could be done by maintaining both the Org file and the actual source files separately, but that would require manually duplicating edits in both files, which is not ideal.
2024-04-01 23:33:27 -04:00
The solution is [[https://en.wikipedia.org/wiki/Literate_programming][literate programming]], the practice of embedding usable code into a markup document. Literate programming can be done in a few different ways; a handful of languages support directly processing these markup documents, such as Haskell and Julia. However, the way Org implements it is closer to the traditional method, which involves two processing systems:
2024-04-01 23:33:27 -04:00
1. /Tangling/, converting the file into raw source code. Tangling must be performed before the code is used; in Org, code is tangled into a new file, and this can be done to generate many source files at once.
2. /Weaving/, formatting the file into a human-readable document. Org conceptually divides this further into "exporting" and "publishing," where the latter is intended specifically for converting into web-ready HTML.
2024-04-01 23:33:27 -04:00
You are currently reading the published version of this literate program[fn:1]. If you were to download this repository and use it as your config, Emacs would be running the tangled version. These versions are generated in separate processes, but both are ultimately derived from the content and metadata inside of the Org file.
[fn:1] Unless you're reading the raw file on Github, in which case you are probably already decently familiar with =org-mode= to be able to read its markup.
**** From Code into Comprehension
The simultaneous handling of documentation and code inherent to literate programming is reminiscent of documentation generation (doc comments) in traditional programming. Both systems involve superimposing code and documentation into one file, but the literate style takes the concept one step further; the document isn't embedded in the code, the code is embedded in the document.
Instead of documentation having to be bent around the restrictions of source code, the source code can be written and organized with all the freedoms of prose. If written well, the literate program can be structured in a manner closer to how the human mind understands code, rather than how a computer processes it. This is assisted by features such as literate macros and tangling configuration, features intended to break one's code out of the restrictions of standard programming.
2024-02-19 18:00:05 -05:00
2024-03-20 02:46:32 -04:00
It's not the right tool for every codebase, but proper use of literate programming can make a program much, much easier to comprehend and maintain. This is especially true for configuration languages like Emacs Lisp, where much of the code is conceptually disconnected and can easily be split into categories.
2024-03-30 17:37:53 -04:00
** Current Issues
2024-04-23 15:13:43 -04:00
*** TODO Nix
Creating garbage collection roots currently doesn't work.
2024-04-01 23:18:46 -04:00
*** TODO Idris
The configuration for Idris is a bit light, and could use some touching up.
*** TODO Mail
2024-03-30 17:37:53 -04:00
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.
2024-04-09 14:16:06 -04:00
*** Org
**** TODO Configure Org popups
* Config Management with =confpkg=
2024-02-19 18:00:05 -05:00
2024-03-05 03:03:24 -05:00
As part of their literate config, Tecosaur implemented =confpkg=, an embedded Emacs Lisp library that manages multiple aspects of config tangling:
2024-02-19 18:00:05 -05:00
- Controlling what generated files each code block is tangled to
- Creating package files from templates
2024-02-19 18:00:05 -05:00
- 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 the source block mechanism.
2024-02-19 18:00:05 -05:00
2024-03-05 03:03:24 -05:00
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!
2024-02-19 18:00:05 -05:00
2024-03-05 03:03:24 -05:00
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:
2024-02-19 18:00:05 -05:00
- Change the package template to contain my information
2024-03-30 17:37:53 -04:00
- Reorganize to get rid of superfluous noweb references
- Prevent the code from being exported
- Allow package statements anywhere in subconfig files, rather than only at the beginning
2024-02-19 18:00:05 -05:00
2024-03-30 17:37:53 -04:00
** confpkg :noexport:
2024-02-19 18:00:05 -05:00
2024-03-30 17:37:53 -04:00
*** Preparation
2024-02-19 18:00:05 -05:00
#+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)
2024-03-07 01:41:44 -05:00
#+begin_src emacs-lisp :noweb no-export
2024-02-19 18:00:05 -05:00
<<confpkg-prepare()>>
#+end_src
2024-03-30 17:37:53 -04:00
*** Setup
2024-02-19 18:00:05 -05:00
#+name: confpkg-setup
2024-02-19 20:42:42 -05:00
#+begin_src emacs-lisp :results silent :noweb no-export
2024-02-19 18:00:05 -05:00
(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))
(re-search-forward "^;;; Code:\n")
2024-04-01 23:19:28 -04:00
(let ((comment-start ";")
(comment-padding " ")
(comment-end "")
statements)
(while (re-search-forward "(\\(package!\\|unpin!\\)" nil t)
(let* ((start (copy-marker (match-beginning 0)))
(end (progn (goto-char start)
(forward-sexp 1)
(if (looking-at "[\t ]*;.*")
(line-end-position)
(point))))
2024-04-01 23:19:28 -04:00
(contents (buffer-substring start end)))
(plist-put confpkg :package-statements
(nconc (plist-get confpkg :package-statements)
(list contents)))
(delete-region start (1+ end))
(re-search-backward "^;;; Code:")
(beginning-of-line)
2024-04-01 23:19:28 -04:00
(unless statements
(insert ";; Package statements:\n")
(setq statements t))
(insert contents)
2024-04-01 23:19:28 -04:00
(unless (string-suffix-p "\n" contents)
(insert "\n"))
(goto-char start)))
2024-04-01 23:19:28 -04:00
(when statements
2024-02-19 18:00:05 -05:00
(re-search-backward "^;;; Code:")
2024-04-01 23:19:28 -04:00
(comment-region
(save-excursion
(re-search-backward "^;; Package statements:")
(forward-line)
(point))
(point)
2)
(insert ";;\n")))
2024-02-19 18:00:05 -05:00
(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: © 2023-%s %s <%s>
2024-02-19 18:00:05 -05:00
;; 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"))
2024-02-19 18:00:05 -05:00
(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]()
2024-03-30 17:37:53 -04:00
*** Confpkg Dispatch
2024-02-19 18:00:05 -05:00
#+name: confpkg
2024-02-19 20:42:42 -05:00
#+begin_src elisp :var name="" needs="" after="" pre="" prefix="config-" via="copy" :results silent raw :noweb no-export
2024-02-19 18:00:05 -05:00
;; 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"
2024-02-19 20:42:42 -05:00
(format ":noweb no-export :tangle no :noweb-ref %s" confpkg-name))
2024-02-19 18:00:05 -05:00
(push (list :name name
:package confpkg-name
:file confpkg-file
:after after
:pre pre
:via (intern via)
:package-statements nil)
confpkg--list)
(format-spec
2024-03-02 17:22:24 -05:00
"#+begin_src emacs-lisp :tangle %f :noweb no-export :noweb-ref none :comments no
2024-02-19 18:00:05 -05:00
<<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
2024-03-30 17:37:53 -04:00
*** Quieter Output
2024-02-19 18:00:05 -05:00
#+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()
2024-03-30 17:37:53 -04:00
*** CLI
2024-02-19 20:42:42 -05:00
#+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
2024-03-30 17:37:53 -04:00
*** Timings
2024-02-19 18:00:05 -05:00
#+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
* Doom Modules
2024-03-05 03:03:24 -05:00
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 prewritten modules to enable.
2024-02-19 18:00:05 -05:00
2024-02-19 20:42:42 -05:00
#+begin_src emacs-lisp :tangle init.el :noweb no-export
2024-02-19 18:00:05 -05:00
;;; 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
2024-04-23 15:13:29 -04:00
Since this is a literate config, the corresponding ~:config literate~ module is necessary. We'll also turn on some of the default config options too.
2024-02-19 18:00:05 -05:00
#+name: doom-config
#+begin_src emacs-lisp
:config
literate
(default +bindings +smartparens)
#+end_src
** Completion
2024-03-05 03:03:24 -05:00
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.
2024-02-19 18:00:05 -05:00
#+name: doom-completion
#+begin_src emacs-lisp
:completion
(vertico +icons)
2024-06-13 14:30:06 -04:00
(corfu +icons +orderless)
2024-02-19 18:00:05 -05:00
#+end_src
** Checkers
2024-03-05 03:03:24 -05:00
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.
2024-02-19 18:00:05 -05:00
2024-03-05 03:03:24 -05:00
We'll also enable a dedicated spell checking module using ~aspell~, as that seems to be the recommended option.
2024-02-19 18:00:05 -05:00
#+name: doom-checkers
#+begin_src emacs-lisp
:checkers
(syntax +flymake +childframe)
(spell +aspell)
;;grammar
#+end_src
** UI
2024-03-05 03:03:24 -05:00
Most of these are either defaults that come with Doom Emacs or just recommended, but here are the highlights:
2024-02-19 18:00:05 -05:00
- ~vi-tilde-fringe~ because I like how it looks
2024-03-05 03:28:09 -05:00
- ~(window-select +numbers)~ because multiple windows are too inconvenient without an easy way to switch between them
2024-02-19 18:00:05 -05:00
- ~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
2024-03-02 18:28:37 -05:00
(popup +defaults)
2024-02-19 18:00:05 -05:00
;;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
2024-03-05 03:03:24 -05:00
word-wrap
2024-02-19 18:00:05 -05:00
#+end_src
#+name: doom-tools
#+begin_src emacs-lisp
:tools
;;ansible
biblio
2024-03-02 17:27:17 -05:00
;;collab
2024-02-19 18:00:05 -05:00
;;debugger
direnv
;;docker
;;editorconfig
;;ein
(eval +overlay)
;;gist
(lookup +docsets)
lsp
2024-06-13 14:31:37 -04:00
(magit +forge)
2024-02-19 18:00:05 -05:00
make
2024-03-02 18:28:31 -05:00
pass
2024-02-19 18:00:05 -05:00
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
2024-03-05 03:03:24 -05:00
Doom Emacs provides a large collection of modules for different languages. Which is good, because setting up language mode packages is kind of annoying.
2024-02-19 18:00:05 -05:00
#+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
2024-03-30 21:02:01 -04:00
(org +roam2 +present
2024-02-19 18:00:05 -05:00
+gnuplot +jupyter
2024-03-30 21:02:01 -04:00
+pandoc +journal)
2024-02-19 18:00:05 -05:00
;;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
2024-03-05 03:03:24 -05:00
This is mostly config settings that don't belong to any particular package and aren't important enough to get their own major section.
2024-02-19 18:00:05 -05:00
** Sensible Settings
#+call: confpkg("Settings")
2024-03-05 03:03:24 -05:00
It wouldn't be Emacs if there wasn't an endless list of config variables to change every aspect of its function!
2024-02-19 18:00:05 -05:00
#+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
)
2024-03-05 02:51:04 -05:00
(global-subword-mode 1)
2024-02-19 18:00:05 -05:00
#+end_src
Thanks once again to Tecosaur for some of these settings.
** Personal Information
#+call: confpkg()
2024-03-05 03:03:24 -05:00
Emacs uses this basic personal information for a few different things, mostly applications.
2024-02-19 18:00:05 -05:00
#+begin_src emacs-lisp
(setq user-full-name "Kiana Sheibani"
user-mail-address "kiana.a.sheibani@gmail.com")
#+end_src
2024-02-20 03:31:12 -05:00
** Authentication
2024-02-25 00:06:37 -05:00
#+call: confpkg("Auth")
2024-03-05 03:03:24 -05:00
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.
2024-02-20 03:31:12 -05:00
#+begin_src emacs-lisp
2024-03-05 02:51:29 -05:00
(require 'auth-source-pass)
2024-03-02 18:28:31 -05:00
(setq auth-sources '(password-store "~/.authinfo.gpg")
2024-02-20 03:31:12 -05:00
auth-source-cache-expiry nil)
#+end_src
2024-02-19 18:00:05 -05:00
** Bindings
#+call: confpkg()
*** Windows & Workspaces
2024-04-09 14:16:06 -04:00
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 33%, and also throw in a convenient binding for switching to =treemacs=.
2024-02-19 18:00:05 -05:00
#+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!
2024-03-05 03:03:24 -05:00
I like to reorganize my workspaces, so we can also add bindings to change the workspace order.
2024-02-19 18:00:05 -05:00
#+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
2024-03-05 03:03:24 -05:00
It's sometimes useful to have a ~universal-argument~ binding that doesn't go through the leader key.
2024-02-19 18:00:05 -05:00
#+begin_src emacs-lisp
(map! :map global-map
"M-u" #'universal-argument)
#+end_src
2024-03-05 03:03:24 -05:00
It's also sometimes useful to have an ~evil-ex~ binding that /does/ go through the leader key.
2024-02-19 18:00:05 -05:00
#+begin_src emacs-lisp
(map! :leader
"w :" nil
":" #'evil-ex)
#+end_src
*** Evil Macros
2024-03-05 03:03:24 -05:00
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.
2024-02-19 18:00:05 -05:00
#+begin_src emacs-lisp
(map! :map evil-normal-state-map
"q" nil
"C-q" #'evil-record-macro)
#+end_src
*** Creating New Projects
2024-03-05 03:03:24 -05:00
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.
2024-02-19 18:00:05 -05:00
#+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.
2024-02-19 18:00:05 -05:00
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))
2024-02-19 18:00:05 -05:00
(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
2024-03-05 03:03:24 -05:00
I'm not even going to bother explaining this one. Emacs is just janky sometimes lol
2024-02-19 18:00:05 -05:00
#+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
2024-02-27 15:22:15 -05:00
** Automated Nix Builds
#+call: confpkg("Nix")
2024-03-05 03:03:24 -05:00
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:
2024-02-27 15:22:15 -05:00
2024-03-05 03:03:24 -05:00
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.
2024-02-27 15:22:15 -05:00
This was my approach before coming up with the third option:
2024-03-05 03:03:24 -05:00
3. Build the tool and point Emacs directly to the store path. This is the simplest solution, but requires the most complex Emacs configuration.
2024-02-27 15:22:15 -05:00
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. Return
nil if the build fails.
2024-02-27 15:22:15 -05:00
If IMPURE is t, then allow impure builds."
(require 'nix) (require 's)
2024-03-22 00:54:41 -04:00
(with-temp-message (format "Building \"%s\" ..." out)
(with-temp-buffer
(let* ((args `("build" "--no-link" "--print-out-paths"
,@(if impure "--impure") ,out))
(status (apply #'call-process nix-executable nil
(list (current-buffer) nil) nil args)))
(when (eql status 0)
2024-03-22 00:54:41 -04:00
(s-trim (buffer-string)))))))
#+end_src
2024-02-27 15:22:15 -05:00
2024-03-05 03:03:24 -05:00
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.
2024-02-27 15:22:15 -05:00
#+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.
Return nil if the build fails.
2024-02-27 15:22:15 -05:00
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."
(when-let* ((path (nix-build-out-path out impure))
(gcdir "/nix/var/nix/gcroots/emacs")
(sym (expand-file-name name gcdir)))
2024-03-02 17:23:00 -05:00
(unless (equal path (file-symlink-p sym))
2024-03-22 00:54:41 -04:00
(require 'tramp)
2024-03-02 17:23:00 -05:00
(make-directory (concat "/sudo::" gcdir) t)
(make-symbolic-link path (concat "/sudo::" sym) t))
2024-02-27 15:22:15 -05:00
path))
#+end_src
2024-03-22 00:52:59 -04:00
* Aesthetics
#+call: confpkg("Visual")
If you're going to be staring at your screen for hours a day, you might as well make the thing you're staring at look nice.
** Theme
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
** Fonts
2024-04-09 14:16:06 -04:00
Victor Mono is my preferred coding font. I also use Source Sans Pro as my sans-serif font, though that is more an arbitrary pick than actually liking how it looks.
2024-03-22 00:52:59 -04:00
#+begin_src emacs-lisp
2024-03-31 02:49:08 -04:00
(setq doom-font (font-spec :family "VictorMono" :size 13)
2024-03-22 00:52:59 -04:00
doom-variable-pitch-font (font-spec :family "Source Sans Pro" :size 16))
#+end_src
2024-04-23 15:12:23 -04:00
I'm a very big fan of how italics look in Victor Mono, so let's make more things italicized! While we're here, we'll also set doom's modified buffer font to be orange instead of yellow (I like how it looks better).
2024-03-22 00:52:59 -04:00
#+begin_src emacs-lisp
(custom-set-faces!
'(font-lock-comment-face :slant italic)
'(font-lock-variable-name-face :slant italic)
2024-04-23 15:12:23 -04:00
`(doom-modeline-buffer-modified
:foreground ,(doom-color 'orange)
:weight bold
:inherit doom-modeline))
2024-03-22 00:52:59 -04:00
#+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
2024-04-09 14:16:06 -04:00
** Line Wrapping
#+call: confpkg("Line Wrapping")
I have rather specific tastes when it comes to line wrapping. I like soft line wrapping (~visual-line-mode~), but I want it to be as seamless as possible.
#+begin_src emacs-lisp
(setq +word-wrap-fill-style 'soft ; Soft line wrapping
evil-respect-visual-line-mode t ; Respect visual line mode
)
(setq-default fill-column 90) ; More space before wrap
#+end_src
*** Hacks
**** Evil
For some reason, telling Evil to respect soft line wrapping doesn't change the behavior of =j= and =k=, so I'll do that myself.
#+begin_src emacs-lisp
(after! evil
(evil-define-motion evil-next-line (count)
"Move the cursor COUNT lines down."
:type line
(let ((line-move-visual evil-respect-visual-line-mode))
(evil-line-move (or count 1))))
(evil-define-motion evil-previous-line (count)
"Move the cursor COUNT lines up."
:type line
(let ((line-move-visual evil-respect-visual-line-mode))
(evil-line-move (- (or count 1))))))
#+end_src
**** Modeline
The =visual-fill-column= package works by expanding the window's right margin. This causes the right edge of the modeline to follow the margin as well, which looks a bit strange. As a hacky fix, I've found that configuring the the modeline to align itself to the right fringe instead of the right window edge seems to fix the issue.
#+begin_src emacs-lisp
(setq mode-line-right-align-edge 'right-fringe)
#+end_src
**** Line Numbers
When a buffer has line numbers, they can interfere with the margins and make the line smaller than it should be. We can mitigate this issue by adding extra columns to the window.
#+begin_src emacs-lisp
(add-hook! display-line-numbers-mode
2024-04-23 15:11:52 -04:00
(require 'visual-fill-column)
(when visual-fill-column-mode
(setq-local visual-fill-column-extra-text-width '(0 . 6))
(visual-fill-column--adjust-window)))
2024-04-09 14:16:06 -04:00
#+end_src
2024-03-22 00:52:59 -04:00
** Dashboard
#+call: confpkg("Dashboard")
There's a lot of reasons why I don't like Spacemacs and why I left it for Doom Emacs (mainly the fact that it's slow and often opaque to the user), but there's one thing that Spacemacs undoubtedly has Doom beat in:
[[https://user-images.githubusercontent.com/33982951/39624821-a4abccee-4f92-11e8-9e91-3d5b542bbb85.png][Spacemacs's dashboard has /impeccable/ style.]]
2024-04-01 23:33:27 -04:00
Doom Emacs tends to favor practicality over aesthetics, and its dashboard is no exception. If we want something that looks visually appealing, we're going to need a serious overhaul.
2024-03-22 00:52:59 -04:00
*** Splash Banner
The Doom dashboard allows the use of an image for its banner, which supports any image type Emacs can display, including SVG. I have procured an SVG image to use for my dashboard, the classic Emacs E:
#+attr_html: :width 150px
[[file:assets/splash.svg]]
The obvious choice for the fill color of the image would have been purple, the standard highlight color of my theme, but I wanted the banner to pop out a bit more.
The image can be set like thus:
#+begin_src emacs-lisp
(setq fancy-splash-image
(expand-file-name "assets/splash.svg" doom-private-dir))
#+end_src
*** Title
Since our banner no longer includes a title, we should add one after the splash image. This title format is inspired by Spacemacs!
#+begin_src emacs-lisp
(defface doom-dashboard-title
'((t (:weight bold :inherit warning)))
"Face used for the Doom Emacs title on the dashboard."
:group 'doom-dashboard)
(setq +doom-dashboard-banner-padding '(0 . 3))
(defvar +doom-dashboard-title-padding 3)
(defun doom-dashboard-widget-title ()
(when (display-graphic-p)
(insert (propertize
(+doom-dashboard--center
+doom-dashboard--width
"[D O O M E M A C S]")
'face 'doom-dashboard-title)
(make-string +doom-dashboard-title-padding ?\n))))
#+end_src
To add the title to the dashboard, we create a new widget that inserts the title string with some padding. We only do this on graphical displays, as non-graphical ones fall back on the default ASCII banner, which includes a title.
*** Other Tweaks
We'll put our title widget into the ~+doom-dashboard-functions~ hook, and while we're at it we'll also get rid of the footer widget, which I don't see much use for.
#+begin_src emacs-lisp
(setq +doom-dashboard-functions
'(doom-dashboard-widget-banner
doom-dashboard-widget-title
doom-dashboard-widget-shortmenu
doom-dashboard-widget-loaded))
#+end_src
We should also declutter some other aspects of the dashboard. Since the dashboard has load information built into it, I don't see much purpose in printing it to the minibuffer on startup.
#+begin_src emacs-lisp
(remove-hook 'doom-after-init-hook #'doom-display-benchmark-h)
#+end_src
2024-02-19 18:00:05 -05:00
* Packages
2024-03-05 03:03:24 -05:00
Now that we've enabled our preferred modules and done some basic configuration, we can install and configure our packages.
2024-02-19 18:00:05 -05:00
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
2024-03-05 03:03:24 -05:00
Everything else goes in ~config.el~, which is managed by [[*=confpkg=][confpkg]] as outlined earlier.
2024-02-19 18:00:05 -05:00
2024-06-13 14:30:06 -04:00
** Corfu
2024-02-19 18:00:05 -05:00
2024-06-13 14:30:06 -04:00
#+call: confpkg("Pkg: corfu")
2024-02-19 18:00:05 -05:00
#+begin_src emacs-lisp
2024-06-13 14:30:06 -04:00
(after! corfu
;; I don't really use TAB very often, so prefer other actions
(setq +corfu-want-tab-prefer-navigating-snippets t
+corfu-want-tab-prefer-expand-snippets t
+corfu-want-tab-prefer-navigating-org-tables t))
2024-02-19 18:00:05 -05:00
#+end_src
** Eldoc
#+call: confpkg("Pkg: eldoc")
2024-02-19 18:00:05 -05:00
2024-03-05 03:03:24 -05:00
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.
2024-02-19 18:00:05 -05:00
#+begin_src emacs-lisp
(after! eldoc
(setq eldoc-documentation-strategy 'eldoc-documentation-compose))
#+end_src
** Embark
#+call: confpkg("Pkg: embark")
2024-02-19 18:00:05 -05:00
2024-03-05 03:03:24 -05:00
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.
2024-02-19 18:00:05 -05:00
*** Targets
2024-03-05 03:03:24 -05:00
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.
2024-02-19 18:00:05 -05:00
#+begin_src emacs-lisp
2024-04-01 23:33:27 -04:00
(defadvice! ~/embark-target-prog-mode (old-fn)
2024-02-19 18:00:05 -05:00
"Advise an embark target to only activate in `prog-mode'."
2024-04-01 23:33:27 -04:00
:around #'embark-target-expression-at-point
(when (derived-mode-p 'prog-mode)
(funcall old-fn)))
2024-02-19 18:00:05 -05:00
2024-04-01 23:33:27 -04:00
(defadvice! ~/embark-target-identifier (old-fn)
2024-02-19 18:00:05 -05:00
"Advise an embark target to only activate in `prog-mode' and not in `lsp-mode'."
2024-04-01 23:33:27 -04:00
:around #'embark-target-identifier-at-point
(when (and (derived-mode-p 'prog-mode)
(not (bound-and-true-p lsp-mode)))
(funcall old-fn)))
2024-02-19 18:00:05 -05:00
(after! embark
2024-02-27 15:22:51 -05:00
(embark-define-thingatpt-target defun emacs-lisp-mode))
#+end_src
2024-03-05 03:03:24 -05:00
We'll also define a word targeter, since that case was previously handled by the identifier one.
2024-02-27 15:22:51 -05:00
#+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)))))
2024-02-19 18:00:05 -05:00
2024-02-27 15:22:51 -05:00
(after! embark
2024-02-19 18:00:05 -05:00
(pushnew! embark-target-finders #'embark-target-word-at-point))
#+end_src
*** LSP Integration
2024-03-05 03:03:24 -05:00
The provided action types related to programming only apply to Emacs Lisp code, so we'll add a new one that integrates with LSP.
2024-02-19 18:00:05 -05:00
#+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
2024-03-05 03:03:24 -05:00
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.
2024-02-19 18:00:05 -05:00
#+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
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))
2024-02-19 18:00:05 -05:00
(cl-pushnew #'embark--mark-target
(alist-get #'+eval/region-and-replace embark-around-action-hooks))
2024-02-19 18:00:05 -05:00
(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
2024-02-19 18:00:05 -05:00
"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
2024-02-19 18:00:05 -05:00
"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")
2024-02-19 18:00:05 -05:00
#+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")
2024-02-19 18:00:05 -05:00
2024-03-05 03:03:24 -05:00
I really like Flycheck's double-arrow fringe indicator, so let's quickly steal that:
2024-02-19 18:00:05 -05:00
#+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
2024-03-05 03:03:24 -05:00
Flymake normally uses italics for warnings, but my italics font being cursive makes that a bit too visually noisy.
2024-02-19 18:00:05 -05:00
#+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
2024-02-19 18:00:05 -05:00
(package! flycheck :disable t)
(package! flyspell :disable t)
#+end_src
2024-03-02 17:27:57 -05:00
*** Bindings
#+begin_src emacs-lisp
(map! :leader
:desc "Open errors buffer"
"c X" #'flymake-show-project-diagnostics)
#+end_src
2024-02-19 18:00:05 -05:00
*** 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
2024-03-02 17:27:57 -05:00
*** Popups
#+begin_src emacs-lisp
(after! flymake
(set-popup-rule! "^\\*Flymake" :vslot 1 :side 'bottom))
#+end_src
2024-06-13 15:18:55 -04:00
** Git
#+call: confpkg("Pkg: magit")
I use GPG signing for commits, which means that committing often takes longer than the single second timeout. Eight seconds seems like a reasonable amount of time to type in a password.
#+begin_src emacs-lisp
(after! git-commit
(setq git-commit-post-finish-hook-timeout 8))
#+end_src
*** Magit Syntax Highlighting
Magit already looks 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
2024-02-19 18:00:05 -05:00
** Indent Guides
#+call: confpkg("Pkg: highlight-indent-guides")
2024-02-19 18:00:05 -05:00
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")
2024-02-19 18:00:05 -05:00
~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
2024-03-02 17:27:57 -05:00
Here's a convenient leader key binding as well:
2024-02-19 18:00:05 -05:00
#+begin_src emacs-lisp
(map! :leader
:desc "Select LSP code lens"
2024-03-02 17:27:57 -05:00
"c L" #'lsp-avy-lens)
2024-02-19 18:00:05 -05:00
#+end_src
2024-03-07 01:44:46 -05:00
** Marginalia
#+call: confpkg("Pkg: marginalia")
2024-03-07 01:44:46 -05:00
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.
** Operation Hints
2024-04-01 23:33:27 -04:00
I like having ophints for vim editing so that I don't get lost when making large edits, 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.
2024-03-07 01:44:46 -05:00
#+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
** Snippets
#+call: confpkg("Pkg: yasnippet")
2024-03-07 01:44:46 -05:00
2024-04-01 23:33:27 -04:00
Snippets are a sophisticated method of warding off the scourge of unnecessary keystrokes. They were a bit hard to get used to, but I've warmed up to them over time.
2024-03-07 01:44:46 -05:00
*** Tweaks
Allow nested snippets:
#+begin_src emacs-lisp
(after! yasnippet
(setq yas-triggers-in-field t))
#+end_src
*** Editing Snippets
Snippets are edited by visiting their file in ~snippet-mode~, which defines some useful keybindings for working with the snippet.
**** Trailing Newlines
If there are any trailing newlines in the snippet file, they will be inserted when the snippet is expanded. This may not always be desirable, so we should prevent Emacs from automatically inserting trailing newlines in these buffers.
#+begin_src emacs-lisp
(add-hook! snippet-mode
(setq-local require-final-newline nil))
#+end_src
**** Testing
When editing a snippet, the binding =C-c C-t= can be used to test it in a fresh buffer. This is very useful, but with Evil it has the issue of leaving the editor in normal state, when snippets are designed to be expanded in insert state.
#+begin_src emacs-lisp
2024-04-01 23:33:27 -04:00
(defadvice! ~/yas-tryout-insert-state (&rest _)
2024-03-07 01:44:46 -05:00
"Switch to Insert state when trying out a snippet."
:after #'yas-tryout-snippet
(evil-insert-state))
#+end_src
*** Creating New Snippets
2024-04-01 23:33:27 -04:00
Doom's command to create a new snippet, ~+snippets/new~, defines a template inside of itself purely for when creating a snippet through this command. This doesn't make much sense to me when file templates already exist as a standard system in Doom, and snippets are stored inside files!
2024-03-07 01:44:46 -05:00
#+begin_src emacs-lisp
(defadvice! ~/snippets-new (&optional all-modes)
2024-04-01 23:33:27 -04:00
"Use standard Doom Emacs file template system when creating a new snippet."
2024-03-07 01:44:46 -05:00
:override #'+snippets/new
(let* ((mode (+snippets--snippet-mode-name-completing-read all-modes))
(default-directory (+snippet--ensure-dir (expand-file-name mode +snippets-dir)))
(snippet-key (read-string "Enter a key for the snippet: "))
(snippet-file-name (expand-file-name snippet-key)))
(when (+snippets--use-snippet-file-name-p snippet-file-name)
2024-04-01 23:33:27 -04:00
(find-file snippet-file-name))))
2024-03-07 01:44:46 -05:00
#+end_src
2024-06-13 15:18:55 -04:00
** Spell Checking
#+call: confpkg("Pkg: ispell")
Doom Emacs sets up spell-checking with ~ispell~ (Emacs-internal tool) and =aspell= (external tool) for us, which is nice. We just need to set which dictionary to use:
#+begin_src emacs-lisp
(after! ispell
(setq ispell-dictionary "en_US"))
#+end_src
We also need to generate a plain-text dictionary for some ~ispell~ functionality, which is annoying, but I haven't figured out a way around it. I could use my automated nix-build system for this, but I want to have access to my =aspell= config file, so it's easier to just put it in the usual location.
#+begin_src emacs-lisp
(defvar ~/plaintext-dict (expand-file-name "ispell/dict.plain" doom-data-dir)
"File location of a plaintext wordlist for spellchecking.")
(unless (file-readable-p ~/plaintext-dict)
(shell-command-to-string
(concat
"aspell -l en_US dump master > " ~/plaintext-dict ";"
"aspell -d en-computers.rws dump master >> " ~/plaintext-dict ";"
"aspell -d en_US-science.rws dump master >> " ~/plaintext-dict ";")))
(after! ispell
(setq ispell-alternate-dictionary ~/plaintext-dict))
#+end_src
Now that we have this word list, we can also plug it into ~cape-dict~ and get proper spelling completion!
#+begin_src emacs-lisp
(after! cape
(setq cape-dict-file ~/plaintext-dict))
(add-hook! text-mode
(add-hook! 'completion-at-point-functions :local :depth 40 #'cape-dict))
#+end_src
2024-02-19 18:00:05 -05:00
** Treemacs
#+call: confpkg("Pkg: treemacs")
2024-02-19 18:00:05 -05:00
2024-03-05 03:03:24 -05:00
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:
2024-02-19 18:00:05 -05:00
#+begin_src emacs-lisp
(use-package! treemacs
2024-02-27 15:22:15 -05:00
:defer t
2024-02-19 18:00:05 -05:00
:init
; More accurate git status
(setq +treemacs-git-mode 'deferred
2024-02-27 15:22:15 -05:00
treemacs-python-executable
(if-let ((path (nix-build-out-path-gcroot
"treemacs-python" "nixpkgs#python3")))
(concat path "/bin/python")
(error "Building python for treemacs failed")))
2024-02-19 18:00:05 -05:00
: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
2024-04-23 15:13:29 -04:00
(treemacs-resize-icons 12) ; Make icons smaller
2024-02-19 18:00:05 -05:00
)
#+end_src
*** Project Integration
2024-03-05 03:03:24 -05:00
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:
2024-02-19 18:00:05 -05:00
#+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
2024-03-05 03:03:24 -05:00
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.
2024-02-19 18:00:05 -05:00
#+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)))
2024-02-19 18:00:05 -05:00
;; 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)
2024-02-19 18:00:05 -05:00
treemacs--workspaces)
(list workspace))))
(treemacs-do-switch-workspace workspace)
2024-02-19 18:00:05 -05:00
(treemacs--invalidate-buffer-project-cache)
(treemacs--rerender-after-workspace-change))))
#+end_src
2024-03-07 01:43:45 -05:00
** VTerm
2024-02-19 18:00:05 -05:00
#+call: confpkg("Pkg: vterm")
2024-02-19 18:00:05 -05:00
2024-03-22 00:54:59 -04:00
I've set my default Emacs shell to =bash=, since pointing Emacs to a non-POSIX shell like =fish= (my usual default) can cause incompatibility issues. I still want to use =fish= for my own purposes, though, so we'll set it as the shell in ~vterm~:
2024-02-19 18:00:05 -05:00
#+begin_src emacs-lisp
(after! vterm
(setq-default vterm-shell (executable-find "fish")))
#+end_src
* Applications
** Calculator
#+call: confpkg("Calc")
2024-03-05 03:03:24 -05:00
Emacs Calc is the best calculator I've ever used, and given the fact that it's an RPN calculator, that's saying something.
2024-02-19 18:00:05 -05:00
*** 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
2024-03-05 03:03:24 -05:00
For the grab-region command, I think it makes sense to have it check whether your selection is a rectangle (=C-v=):
2024-02-19 18:00:05 -05:00
#+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
2024-04-23 15:13:29 -04:00
I want to have vim keybindings in Calc, so let's enable the =evil-collection= module for it by removing it from ~+evil-collection-disabled-list~.
2024-02-19 18:00:05 -05:00
2024-03-22 00:54:41 -04:00
#+begin_src emacs-lisp
2024-02-19 18:00:05 -05:00
;; Enable evil-collection-calc
2024-03-22 00:54:41 -04:00
(delq 'calc +evil-collection-disabled-list)
2024-02-19 18:00:05 -05:00
#+end_src
2024-04-23 15:13:29 -04:00
Let's also rebind some keys. We'll set =[= and =]= to directly begin and end vectors like it did originally, and =C-r= makes more sense as a redo binding than =D D=.
2024-02-19 18:00:05 -05:00
#+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
2024-03-05 03:03:24 -05:00
Calc doesn't use faces to show selections by default, which I think is rather strange.
2024-02-19 18:00:05 -05:00
#+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
** Calendar
2024-02-25 00:06:37 -05:00
#+call: confpkg()
2024-02-19 18:00:05 -05:00
The calendar's main purpose for me is to give a better view of the [[*Agenda][Org agenda]].
#+begin_src emacs-lisp
2024-03-05 02:52:44 -05:00
(after! calendar
;; Start week on Monday
2024-03-08 18:43:11 -05:00
(setq calendar-week-start-day 1)
;; ISO date style
(calendar-set-date-style 'iso))
2024-03-05 02:52:44 -05:00
2024-02-19 18:00:05 -05:00
(after! calfw
2024-03-05 02:52:44 -05:00
(setq cfw:org-face-agenda-item-foreground-color (doom-color 'yellow)))
2024-02-19 18:00:05 -05:00
(map! :leader
:desc "Calendar"
"o c" #'cfw:open-org-calendar)
#+end_src
2024-03-05 02:52:01 -05:00
** Emacs Everywhere
#+call: confpkg("Emacs Everywhere")
2024-03-05 03:03:24 -05:00
Emacs Everywhere is a great idea. Unfortunately, the default package on MELPA uses X-based window commands, while I use Hyprland, which is Wayland-based. To fix this issue, we need to override some of the package's variables and functions.
2024-03-05 02:52:01 -05:00
#+begin_src emacs-lisp
(after! emacs-everywhere
;; Shell commands for interacting with window system
(setq emacs-everywhere-paste-command
2024-03-19 03:28:34 -04:00
'("wtype" "-M" "shift" "-P" "Insert")
2024-03-05 02:52:01 -05:00
emacs-everywhere-copy-command
'("sh" "-c" "wl-copy < %f")
emacs-everywhere-window-focus-command
'("hyprctl" "dispatch" "focuswindow" "address:%w")))
;; Function for accessing current window
(defadvice! ~/emacs-everywhere-app-info-hyprland ()
"Return information on the active window, in Hyprland."
:override #'emacs-everywhere--app-info-linux
(pcase-let*
((`(,window-id ,window-class ,window-title . ,window-dims-)
(split-string (shell-command-to-string
"hyprctl activewindow -j | jaq -r \
'.address, .class, .title, .at[], .size[]'")
"\n"))
(window-dims (mapcar #'string-to-number (butlast window-dims-))))
(make-emacs-everywhere-app
:id window-id
:class window-class
:title window-title
:geometry window-dims)))
#+end_src
2024-04-01 23:18:46 -04:00
** Mail
2024-03-07 01:44:46 -05:00
#+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
** Password Management
#+call: confpkg("Pass")
I use the standard Unix-style password management system, [[https://www.passwordstore.org/][pass]].
#+begin_src emacs-lisp
(map! :leader
:desc "Password Store"
"o s" #'pass)
(after! password-store
(setq pass-show-keybindings nil ; Keybindings take up too much space
pass-suppress-confirmations t ; Quit shouldn't need a confirm step
)
;; Move to right side
(set-popup-rule! "^\\*Password-Store" :side 'right :size 0.25 :quit nil))
#+end_src
2024-02-19 18:00:05 -05:00
* Org
#+call: confpkg()
2024-03-05 03:03:24 -05:00
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.
2024-02-19 18:00:05 -05:00
2024-03-05 03:03:24 -05:00
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.
2024-02-19 18:00:05 -05:00
** Basic Configuration
#+begin_src emacs-lisp
;; Org Directory - location of main org repo
2024-02-27 15:19:47 -05:00
(setq org-directory "~/org/")
2024-02-19 18:00:05 -05:00
(after! org
2024-02-27 15:19:47 -05:00
(setq org-archive-location ; Global archive file
(concat org-directory ".org_archive::")
2024-02-19 18:00:05 -05:00
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
2024-03-30 21:02:01 -04:00
;; Startup
2024-02-19 18:00:05 -05:00
org-startup-with-inline-images t ; Do more stuff on startup
org-startup-with-latex-preview t
+org-startup-with-animated-gifs t
;; Customize appearance
org-indent-indentation-per-level 0 ; No heading indentation
org-cycle-separator-lines 1 ; Keep 1-line padding between folded headings
org-hide-emphasis-markers t ; Hide *emphasis*
2024-03-22 00:54:41 -04:00
org-image-actual-width '(550) ; Default images to 550px
2024-02-19 18:00:05 -05:00
org-format-latex-options ; Make latex preview smaller
(plist-put org-format-latex-options :scale 0.55)
2024-02-19 18:00:05 -05:00
;; Todo Keywords
org-todo-keywords
'((sequence
"TODO(t)" "PROJ(p)"
"NEXT(n)"
"STRT(s!)"
"WAIT(w@/!)" "HOLD(h@/!)"
"|"
"DONE(d!/@)" "PART(r@/@)"
"KILL(k@/@)"))
2024-02-19 18:00:05 -05:00
org-todo-keyword-faces
'(("PROJ" . +org-todo-project)
("STRT" . +org-todo-active)
2024-02-19 18:00:05 -05:00
("WAIT" . +org-todo-onhold)
("HOLD" . +org-todo-onhold)
("KILL" . +org-todo-cancel))
))
2024-03-02 17:26:54 -05:00
#+end_src
2024-02-19 18:00:05 -05:00
2024-04-04 23:33:57 -04:00
*** 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 subject-alist '()
"An alist where the car is the name of a class subject (typically
four capitalized letters), and the cdr is a list of sub-tags.")
(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))
2024-04-09 14:05:27 -04:00
subject-alist '(("MATH" ("calculus") ("algebra"))
("HIST")
("POLS")
("ECON")))
2024-04-04 23:33:57 -04:00
(setq org-tag-persistent-alist
`(("area" . ?A) ("goal" . ?G) ("project" . ?P) ("meta" . ?M)
(:startgrouptag) ("college")
(:grouptags) ("assign") ("notes") (:endgrouptag)
2024-04-09 14:05:27 -04:00
,@(-mapcat
2024-04-04 23:33:57 -04:00
(lambda (subject)
`((:startgrouptag) (,(car subject))
(:grouptags)
(,(format "{%s[[:digit:]]+}" (car subject)))
,@(cdr subject)
(:endgrouptag)))
2024-04-09 14:05:27 -04:00
subject-alist)
2024-04-04 23:33:57 -04:00
(: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
2024-03-02 17:26:54 -05:00
*** Bindings
**** Convenience
2024-03-05 03:03:24 -05:00
There are a few useful functions Doom doesn't bind by default, so let's add them for convenience.
2024-03-02 17:26:54 -05:00
#+begin_src emacs-lisp
(map! :after org
:map org-mode-map
2024-02-19 18:00:05 -05:00
:localleader
"N" #'org-num-mode
"C" #'org-columns
"p" #'org-priority ; Remove extraneous commands
2024-03-22 00:54:41 -04:00
"g j" #'org-goto
2024-02-19 18:00:05 -05:00
"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
2024-03-02 17:29:23 -05:00
**** YASnippet
2024-03-05 03:03:24 -05:00
By default, snippet expansion doesn't work in Org mode; when =TAB= is pressed to move to the next placeholder, headline visibility cycling is triggered instead. This is because in ~org-mode-map~ =TAB= is unconditionally bound to ~org-cycle~, and for some reason this has a higher precedence than YAS's keymaps.
2024-03-02 17:29:23 -05:00
2024-03-05 03:03:24 -05:00
While this is a complex problem, the solution is actually rather simple: just remove the ~org-mode-map~ binding. The ~org-cycle~ command will still be triggered on =TAB= when in normal mode, as it is bound in ~evil-org-mode-map~, but when in insert mode (as one generally is during snippet expansion) the binding will fall through both maps and be handled by YASnippet.
2024-03-02 17:29:23 -05:00
#+begin_src emacs-lisp
(map! :after org
:map org-mode-map
"TAB" nil
"<tab>" nil)
#+end_src
This also means we don't need ~org-cycle~ to emulate indentation, which is nice. Unfortunately, this breaks something else: ~org-cycle~ not only handles visibility cycling, but also navigating inside tables. This means that we need a little extra binding configuration so that we can make this properly work:
2024-03-02 17:29:23 -05:00
2024-03-05 02:52:11 -05:00
#+begin_src emacs-lisp
(let ((item
`(menu-item nil org-table-next-field
:filter ,(lambda (cmd)
(when (org-table-p)
cmd)))))
(map! :after org
:map evil-org-mode-map
:i "TAB" item
:i "<tab>" item))
2024-03-05 02:52:11 -05:00
#+end_src
This makes it so that we only let tab keypresses fall through if we aren't in a table. (We'll just assume that we aren't going to be expanding any snippets while we're in a table.)
2024-02-19 18:00:05 -05:00
*** Project Links
2024-03-05 03:03:24 -05:00
It's sometimes nice to be able to click a link in an Org file that takes me to one of my projects.
2024-02-19 18:00:05 -05:00
#+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
2024-03-30 21:02:01 -04:00
** Appearance
*** Modern
Doom Emacs's =+pretty= flag by default uses the package =org-superstar= to prettify Org files. This package is decently nice looking, but it has a much nicer and more comprehensive alternative in the form of =org-modern=.
#+begin_src emacs-lisp :tangle packages.el
(package! org-modern)
#+end_src
#+begin_src emacs-lisp
(use-package! org-modern
:hook (org-mode . org-modern-mode)
:config
2024-06-13 14:29:32 -04:00
(setq org-modern-star 'replace
org-modern-replace-stars '("◉" "○" "✸" "✿" "✤" "✜" "◆" "▶")
2024-03-30 21:02:01 -04:00
org-modern-label-border 0.3
org-modern-table-vertical 1
org-modern-table-horizontal 0.2
org-modern-list '((?- . "•")
(?+ . "•")
(?* . "•"))
org-modern-footnote
(cons nil (cadr org-script-display))
2024-04-01 23:32:15 -04:00
org-modern-todo nil
2024-03-30 21:02:01 -04:00
org-modern-todo-faces
'(("TODO" :inverse-video t :inherit org-todo)
("PROJ" :inverse-video t :inherit +org-todo-project)
("STRT" :inverse-video t :inherit +org-todo-active)
("WAIT" :inverse-video t :inherit +org-todo-onhold)
("HOLD" :inverse-video t :inherit +org-todo-onhold)
("KILL" :inverse-video t :inherit +org-todo-cancel))
org-modern-block-fringe nil
org-modern-block-name
'((t . t)
("src" "»" "«")
("example" "»–" "–«")
("quote" "❝" "❞")
("export" "⏩" "⏪"))
org-modern-priority nil
org-modern-horizontal-rule (make-string 36 ?─)
org-modern-keyword
2024-03-31 02:49:38 -04:00
'(("title" . "󰛼")
("subtitle" . "")
2024-03-30 21:02:01 -04:00
("author" . "")
("email" . "󰇰")
("date" . "󰃮")
2024-04-01 23:32:15 -04:00
("property" . "󰆧")
2024-03-31 02:49:38 -04:00
("filetags" . "󰓼")
2024-03-30 21:02:01 -04:00
("bind" . "󰌷")
("bibliography" . "")
("print_bibliography" . #("" 0 1 (display (raise -0.1))))
("cite_export" . "")
("print_glossary" . #("ᴬᶻ" 0 1 (display (raise -0.1))))
("glossary_sources" . #("" 0 1 (display (raise -0.14))))
("include" . "⇤")
("setupfile" . "⇚")
("html_head" . "🅷")
("html" . "🅗")
("latex_class" . "🄻")
("latex_class_options" . #("🄻" 1 2 (display (raise -0.14))))
("latex_header" . "🅻")
("latex_header_extra" . "🅻⁺")
("latex" . "🅛")
("beamer_theme" . "🄱")
("beamer_color_theme" . #("🄱" 1 2 (display (raise -0.12))))
("beamer_font_theme" . "🄱𝐀")
("beamer_header" . "🅱")
("beamer" . "🅑")
("attr_latex" . "🄛")
("attr_html" . "🄗")
("attr_org" . "⒪")
2024-03-31 02:49:38 -04:00
("call" . "󰐍")
2024-03-30 21:02:01 -04:00
("name" . "󱍶")
("header" . "")
("caption" . "󰦨")
2024-03-31 02:49:38 -04:00
("results" . " result")
(t . " "))
2024-03-30 21:02:01 -04:00
org-modern-checkbox
'((88 . "")
2024-03-31 02:49:38 -04:00
(45 . #("" 0 2 (composition ((2)))))
2024-03-30 21:02:01 -04:00
(32 . ""))))
#+end_src
The default colors for various elements of =org-modern= don't match with our theme colors, so we need to modify them ourselves. We'll also modify a few aspects of label appearance here.
#+begin_src emacs-lisp
(after! org-modern
(set-face-attribute 'org-modern-label nil :height 0.85)
(custom-set-faces!
'(org-checkbox :weight normal :inherit org-todo)
`(org-modern-tag
:foreground ,(doom-color 'fg-alt)
:background ,(doom-color 'base0)
:inherit (secondary-selection org-modern-label))
`(org-modern-horizontal-rule
:strike-through ,(doom-color 'grey) :inherit org-hide)
`(org-modern-done
2024-04-09 14:06:36 -04:00
:height 1.1
2024-03-30 21:02:01 -04:00
:foreground ,(doom-color 'fg-alt)
2024-04-01 23:32:15 -04:00
:background ,(doom-color 'modeline-bg)
2024-03-30 21:02:01 -04:00
:inherit org-modern-label)
2024-04-01 23:32:15 -04:00
`(org-modern-date-active
:foreground ,(doom-color 'brown)
:inherit org-modern-done)
2024-03-30 21:02:01 -04:00
`(org-modern-date-inactive
2024-03-31 02:49:38 -04:00
:foreground ,(doom-color 'doc-comments)
2024-03-30 21:02:01 -04:00
:inherit org-modern-date-active)
`(org-modern-time-active
2024-04-01 23:32:15 -04:00
:foreground ,(doom-color 'fg-alt)
:background ,(doom-color 'base0)
2024-04-09 14:06:36 -04:00
:inherit org-modern-done)
2024-03-30 21:02:01 -04:00
`(org-modern-time-inactive
2024-04-01 23:32:15 -04:00
:foreground ,(doom-color 'grey)
:background ,(doom-color 'modeline-bg-l)
2024-04-09 14:06:36 -04:00
:inherit org-modern-time-active)
`(org-modern-statistics
:foreground ,(doom-color 'yellow)
:inherit org-checkbox-statistics-todo)))
2024-03-30 21:02:01 -04:00
#+end_src
*** Appear
Since we've disabled =+pretty=, we need to add the packages we do want from it, namely =org-appear= to automatically handle emphasis markers and other nice things like that.
#+begin_src emacs-lisp :tangle packages.el
(package! org-appear)
#+end_src
#+begin_src emacs-lisp
(use-package! org-appear
:hook (org-mode . org-appear-mode)
:config
2024-04-09 14:06:36 -04:00
(setq org-appear-autoentities t
org-appear-inside-latex t))
2024-03-30 21:02:01 -04:00
(after! org
(setq org-highlight-latex-and-related '(native script entities)))
#+end_src
*** Emphasis
When marking text for =*emphasis*=, Org mode normally only allows emphasized sections to span 2 lines. This strikes me as needlessly limited, so let's bump up that number to 20 lines.
#+begin_src emacs-lisp
(after! org
(setf (nth 4 org-emphasis-regexp-components) 20))
#+end_src
2024-03-05 02:52:11 -05:00
** Enhancements
2024-03-20 02:31:21 -04:00
While Org-mode provides a very comprehensive set of tools and systems, there are a few small things that are missing or that would make the overall UX smoother. Luckily, Org-mode being implemented as an Emacs package gives us more than enough control over its function to crowbar in a few new features!
2024-03-30 16:16:46 -04:00
*** Archive Restore
Org offers a wonderfully useful trash system called archiving, which lets you move subtrees into a file where they can be saved without permanently deleting them. However, there's no system for automatically restoring these subtrees once they're archived!
Thankfully, Org already stores context data about archived subtrees, so we can use that. First, let's create a function for restoring an archived subtree at point.
#+begin_src emacs-lisp
(defun org-restore-from-archive ()
"Restore an archived subtree to its original file and location.
This function works on any subtree with the ARCHIVE_FILE and optionally
the ARCHIVE_OLPATH and ARCHIVE_TODO properties, returning the subtree
to the specified state. It will also remove any other ARCHIVE_* properties.
WARNING: If this subtree had an attachment that was deleted during archive,
the attachment cannot be restored!"
(interactive)
(let* ((archived-p
(lambda (element)
(and (org-element-type-p element 'headline)
(org-element-property :ARCHIVE_FILE element))))
(lineage (org-element-lineage (org-element-at-point) nil t))
(heading (or (-first archived-p lineage)
(user-error "Archived heading could not be found")))
(title (org-element-property :title heading))
(file (org-element-property :ARCHIVE_FILE heading))
(olp (-some--> (org-element-property :ARCHIVE_OLPATH heading)
(s-split "/" it t)))
(todo (org-element-property :ARCHIVE_TODO heading))
(target (condition-case nil
(org-find-olp (cons file olp))
((t (error "Invalid outline path %S" olp))))))
(goto-char (org-element-begin heading))
;; Restore todo state
(when todo (org-todo todo))
;; Delete properties (code hacked from `org-entry-delete')
(save-excursion
(pcase (org-get-property-block)
(`(,begin . ,origin)
(let ((end (copy-marker origin))
(re (org-re-property "ARCHIVE_[^:]+" t t)))
(goto-char begin)
(while (re-search-forward re end t)
(delete-region (match-beginning 0)
(line-beginning-position 2)))
(when (= begin end)
(delete-region (line-beginning-position 0)
(line-beginning-position 2)))))))
;; Move back to original location
(org-refile nil nil (list "" file nil target))
(org-refile-goto-last-stored)
(message "Restored \"%s\" to file %s" title file)))
#+end_src
2024-03-05 02:52:11 -05:00
*** Todo Date Overriding
2024-03-20 02:31:21 -04:00
My attention span being what it is, I often forget to update TODO entries in my Org files until long after the task has been completed. I rely heavily on tracking TODOs through timestamps, so it would be nice to have a command to specify the time to log the task as being completed at. To do this, we can create a new variable ~org-todo-time~ that will override the time when set.
2024-03-05 02:52:11 -05:00
#+begin_src emacs-lisp
(defvar org-todo-time nil
"The time to use when updating TODO entries.
If nil, then use the current time.")
(defadvice! ~/org-override-time (old-fn)
"Use `org-todo-time' as the current time if it is specified."
2024-03-30 21:02:34 -04:00
:around #'org-current-time
2024-03-05 02:52:11 -05:00
(or org-todo-time (funcall old-fn)))
#+end_src
2024-03-05 03:03:24 -05:00
We can then define and bind alternate versions of ~org-todo~ and ~org-agenda-todo~ that allow us to pick the time to set.
2024-03-05 02:52:11 -05:00
#+begin_src emacs-lisp
(defmacro ~/org-wrap-todo (fn)
"Wrap a command to set `org-todo-time'."
(let ((new-fn (intern (format "%s-date" fn))))
`(defun ,new-fn (&optional arg)
,(format "Call `%s', but allow the user to pick a time first." fn)
(interactive "P")
(let ((org-todo-time (org-read-date t t)))
(,fn arg)))))
(after! org
(~/org-wrap-todo org-todo)
(~/org-wrap-todo org-agenda-todo))
(map! :mode org-mode
:after org
:localleader
"T" #'org-todo-date)
(map! :mode org-agenda-mode
:after org
2024-03-06 01:24:33 -05:00
"T" #'org-agenda-todo-date
:localleader
2024-03-05 02:52:11 -05:00
"T" #'org-agenda-todo-date)
2024-03-06 01:24:33 -05:00
(map! :mode evil-org-agenda-mode
:after org
:m "T" #'org-agenda-todo-date)
2024-02-19 18:00:05 -05:00
#+end_src
2024-03-07 01:46:11 -05:00
*** Header Argument Snippets
2024-03-20 02:31:21 -04:00
Writing header arguments for source code blocks is confusing, so let's automate the process somewhat by adding snippets to fill in keywords. To do this, we'll need to write some utility functions for checking if we're in a source block header.
2024-03-07 01:46:11 -05:00
2024-03-20 02:31:21 -04:00
(This is based on the similar code in [[https://tecosaur.github.io/emacs-config/config.html#snippet-helpers][Tecosaur's config]], though I've removed some of the features I don't care much for.)
2024-03-07 01:46:11 -05:00
#+begin_src emacs-lisp
(defun ~/yas-org-src-header-p ()
"Determine whether `point' is within a src-block header or header-args."
(let ((context (org-element-context)))
(pcase (org-element-type context)
('src-block (< (point) ; before code part of the src-block
(save-excursion
(goto-char (org-element-property :begin context))
(forward-line 1)
(point))))
('inline-src-block (< (point) ; before code part of the inline-src-block
(save-excursion
(goto-char (org-element-property :begin context))
(search-forward "]{")
(point))))
('keyword (string-match-p "^header-args"
(org-element-property :value context))))))
(defun ~/yas-org-src-lang ()
"Try to find the current language of the src/header at `point'.
Return nil otherwise."
(let ((context (org-element-context)))
(pcase (org-element-type context)
('src-block (org-element-property :language context))
('inline-src-block (org-element-property :language context))
('keyword
(when (string-match "^header-args:\\([^ ]+\\)"
(org-element-property :value context))
(match-string 1 (org-element-property :value context)))))))
(defun ~/yas-org-prompt-header-arg (arg question values)
"Prompt the user to set ARG header property to one of VALUES with QUESTION.
The default value is identified and indicated. If either default is selected,
or no selection is made: nil is returned."
(let* ((src-block-p (not (looking-back
"^#\\+property:[ \t]+header-args:.*"
(line-beginning-position))))
(default
(or
(cdr (assoc arg
(if src-block-p
(nth 2 (org-babel-get-src-block-info t))
(org-babel-merge-params
org-babel-default-header-args
(let ((lang-headers
(intern (concat "org-babel-default-header-args:"
(~/yas-org-src-lang)))))
(when (boundp lang-headers) (eval lang-headers t)))))))
""))
default-value)
(setq values (mapcar
(lambda (value)
(if (string-match-p (concat "^" (regexp-quote value) "$")
default)
(setq default-value
(concat value " "
(propertize "(default)" 'face
'font-lock-doc-face)))
value))
values))
(let ((selection
(consult--read values :prompt question :default default-value)))
(unless (or (string-match-p "(default)$" selection)
(string= "" selection))
selection))))
#+end_src
*** Org Checklist
The simple package =org-checklist= from =org-contrib= makes it so that checkboxes inside of cyclic tasks are reset when the task is repeated. Sounds good to me!
#+begin_src emacs-lisp
(when (modulep! :lang org)
(use-package org-checklist
:commands (org-reset-checkbox-state-maybe
org-make-checklist-export
org-checklist)
:init
(add-hook 'org-after-todo-state-change-hook #'org-checklist)
:config
(remove-hook 'org-after-todo-state-change-hook #'org-checklist)
(add-hook 'org-after-todo-state-change-hook #'org-checklist)))
#+end_src
The ~org-checklist~ function will reset the checkboxes on any task, but I only want them reset when the task repeats.
#+begin_src emacs-lisp
(defadvice! ~/org-checklist-only-on-repeating (old-fn)
"Only reset checkboxes when marking repeater tasks as DONE."
:around #'org-checklist
(when (org-get-repeat)
(funcall old-fn)))
#+end_src
I don't want to have to specify the =RESET_CHECK_BOXES= property for every TODO I write, though. I would much prefer if it was on by default, and the system allowed me to turn it off if I wanted to. Luckily, the fine control Org gives you over property inheritance nicely fixes this problem.
#+begin_src emacs-lisp
(after! org-checklist
(push '("RESET_CHECK_BOXES" . "t") org-global-properties))
(defadvice! ~/org-checklist-reset-inherit ()
"Override checkbox resetting to use property inheritance."
:override #'org-reset-checkbox-state-maybe
(if (org-entry-get (point) "RESET_CHECK_BOXES" t)
(org-reset-checkbox-state-subtree)))
#+end_src
2024-03-08 18:42:28 -05:00
** Bug Fixes and Tweaks
2024-04-09 14:05:56 -04:00
*** Improved Tag Selection
The facilities for selecting and adding tags do not play very nicely with complex tag hierarchies, especially fast tag selection. The main problems are:
- Fast tag selection interprets regex tags as actual tags
- Fast tag selection can assign multiple keys to the same tag
- Regular tag selection interprets special tag entries such as ~:startgroup~ as actual tags
To fix this, we'll need some liberal use of override advising.
#+begin_src emacs-lisp
(defadvice! ~/org-assign-fast-keys (alist)
:override #'org-assign-fast-keys
(let (new e (alt ?0))
(while (setq e (pop alist))
(if (or (memq (car e) '(:newline :grouptags :endgroup :startgroup :startgrouptag :endgrouptag))
(and (string-prefix-p "{" (car e)) (string-suffix-p "}" (car e)))
(cdr e)) ;; Key already assigned.
(push e new)
(let ((clist (string-to-list (downcase (car e))))
(used (append new alist)))
(when (= (car clist) ?@)
(pop clist))
(while (and clist (rassoc (car clist) used))
(pop clist))
(unless clist
(while (rassoc alt used)
(cl-incf alt)))
(push (cons (car e) (or (car clist) alt)) new))))
(nreverse new)))
(defadvice! ~/org-fast-tag-selection (current-tags inherited-tags tag-table &optional todo-table)
:override #'org-fast-tag-selection
(let* (;; Combined alist of all the tags and todo keywords.
(tag-alist (append tag-table todo-table))
;; Max width occupied by a single tag record in the completion buffer.
(field-width
(+ 3 ; keep space for "[c]" binding.
1 ; ensure that there is at least one space between adjacent tag fields.
3 ; keep space for group tag " : " delimiter.
;; The longest tag.
(if (null tag-alist) 0
(apply #'max
(mapcar (lambda (x)
(if (stringp (car x)) (string-width (car x))
0))
tag-alist)))))
(origin-buffer (current-buffer))
(expert-interface (eq org-fast-tag-selection-single-key 'expert))
;; Tag completion table, for normal completion (<TAB>).
(tab-tags nil)
(inherited-face 'org-done)
(current-face 'org-todo)
;; Characters available for auto-assignment.
(tag-binding-char-list org--fast-tag-selection-keys)
(tag-binding-chars-left org-fast-tag-selection-maximum-tags)
field-number ; current tag column in the completion buffer.
tag-binding-spec ; Alist element.
current-tag current-tag-char auto-tag-char
tag-table-local ; table holding all the displayed tags together with auto-assigned bindings.
input-char rtn
ov-start ov-end ov-prefix
(exit-after-next org-fast-tag-selection-single-key)
(done-keywords org-done-keywords)
groups ingroup intaggroup char-tags)
;; Calculate the number of tags with explicit user bindings + tags in groups.
;; These tags will be displayed unconditionally. Other tags will
;; be displayed only when there are free bindings left according
;; to `org-fast-tag-selection-maximum-tags'.
(dolist (tag-binding-spec tag-alist)
(pcase tag-binding-spec
(`((or :startgroup :startgrouptag) . _)
(setq ingroup t))
(`((or :endgroup :endgrouptag) . _)
(setq ingroup nil))
((guard (cdr tag-binding-spec))
(cl-decf tag-binding-chars-left))
(`((or :newline :grouptags))) ; pass
((guard ingroup)
(cl-decf tag-binding-chars-left))))
(setq ingroup nil) ; It t, it means malformed tag alist. Reset just in case.
;; Move global `org-tags-overlay' overlay to current heading.
;; Calls to `org-set-current-tags-overlay' will take care about
;; updating the overlay text.
;; FIXME: What if we are setting file tags?
(save-excursion
(forward-line 0)
(if (looking-at org-tag-line-re)
(setq ov-start (match-beginning 1)
ov-end (match-end 1)
ov-prefix "")
(setq ov-start (1- (line-end-position))
ov-end (1+ ov-start))
(skip-chars-forward "^\n\r")
(setq ov-prefix
(concat
(buffer-substring (1- (point)) (point))
(if (> (current-column) org-tags-column)
" "
(make-string (- org-tags-column (current-column)) ?\ ))))))
(move-overlay org-tags-overlay ov-start ov-end)
;; Highlight tags overlay in Org buffer.
(org-set-current-tags-overlay current-tags ov-prefix)
;; Display tag selection dialogue, read the user input, and return.
(save-excursion
(save-window-excursion
;; Select tag list buffer, and display it unless EXPERT-INTERFACE.
(if expert-interface
(set-buffer (get-buffer-create " *Org tags*"))
(delete-other-windows)
(set-window-buffer (split-window-vertically) (get-buffer-create " *Org tags*"))
(switch-to-buffer-other-window " *Org tags*"))
;; Fill text in *Org tags* buffer.
(erase-buffer)
(setq-local org-done-keywords done-keywords)
;; Insert current tags.
(org-fast-tag-insert "Inherited" inherited-tags inherited-face "\n")
(org-fast-tag-insert "Current" current-tags current-face "\n\n")
;; Display whether next change exits selection dialogue.
(org-fast-tag-show-exit exit-after-next)
;; Show tags, tag groups, and bindings in a grid.
;; Each tag in the grid occupies FIELD-WIDTH characters.
;; The tags are filled up to `window-width'.
(setq field-number 0)
(while (setq tag-binding-spec (pop tag-alist))
(pcase tag-binding-spec
;; Display tag groups on starting from a new line.
(`(:startgroup . ,group-name)
(push '() groups) (setq ingroup t)
(unless (zerop field-number)
(setq field-number 0)
(insert "\n"))
(insert (if group-name (format "%s: " group-name) "") "{ "))
;; Tag group end is followed by newline.
(`(:endgroup . ,group-name)
(setq ingroup nil field-number 0)
(insert "}" (if group-name (format " (%s) " group-name) "") "\n"))
;; Group tags start at newline.
(`(:startgrouptag)
(setq intaggroup t)
(unless (zerop field-number)
(setq field-number 0)
(insert "\n"))
(insert "[ "))
;; Group tags end with a newline.
(`(:endgrouptag)
(setq intaggroup nil field-number 0)
(insert "]\n"))
(`(:newline)
(unless (zerop field-number)
(setq field-number 0)
(insert "\n")
(setq tag-binding-spec (car tag-alist))
(while (equal (car tag-alist) '(:newline))
(insert "\n")
(setq tag-alist (cdr tag-alist)))))
(`(:grouptags)
;; Previous tag is the tag representing the following group.
;; It was inserted as "[c] TAG " with spaces filling up
;; to the field width. Replace the trailing spaces with
;; " : ", keeping to total field width unchanged.
(delete-char -3)
(insert " : "))
(_
(setq current-tag (copy-sequence (car tag-binding-spec))) ; will be modified by side effect
(if (or (member current-tag char-tags)
(and (string-prefix-p "{" current-tag)
(string-suffix-p "}" current-tag)))
(setq current-tag-char nil)
;; Compute tag binding.
(if (cdr tag-binding-spec)
;; Custom binding.
(setq current-tag-char (cdr tag-binding-spec))
;; No auto-binding. Update `tag-binding-chars-left'.
(unless (or ingroup intaggroup) ; groups are always displayed.
(cl-decf tag-binding-chars-left))
;; Automatically assign a character according to the tag string.
(setq auto-tag-char
(string-to-char
(downcase (substring
current-tag (if (= (string-to-char current-tag) ?@) 1 0)))))
(if (or (rassoc auto-tag-char tag-table-local)
(rassoc auto-tag-char tag-table))
;; Already bound. Assign first unbound char instead.
(progn
(while (and tag-binding-char-list
(or (rassoc (car tag-binding-char-list) tag-table-local)
(rassoc (car tag-binding-char-list) tag-table)))
(pop tag-binding-char-list))
(setq current-tag-char (or (car tag-binding-char-list)
;; Fall back to display "[ ]".
?\s)))
;; Can safely use binding derived from the tag string.
(setq current-tag-char auto-tag-char)))
(push current-tag char-tags))
;; Record all the tags in the group. `:startgroup'
;; clause earlier added '() to `groups'.
;; `(car groups)' now contains the tag list for the
;; current group.
(when ingroup (push current-tag (car groups)))
;; Compute tag face.
(setq current-tag (org-add-props current-tag nil 'face
(cond
((not (assoc current-tag tag-table))
;; The tag is from TODO-TABLE.
(org-get-todo-face current-tag))
((member current-tag current-tags) current-face)
((member current-tag inherited-tags) inherited-face))))
(when (equal (caar tag-alist) :grouptags)
(org-add-props current-tag nil 'face 'org-tag-group))
;; Respect `org-fast-tag-selection-maximum-tags'.
(when (or ingroup intaggroup (cdr tag-binding-spec) (> tag-binding-chars-left 0))
;; Insert the tag.
(when (and (zerop field-number) (not ingroup) (not intaggroup)) (insert " "))
(if current-tag-char
(insert "[" current-tag-char "] ")
(insert " "))
(insert current-tag
;; Fill spaces up to FIELD-WIDTH.
(make-string
(- field-width 4 (length current-tag)) ?\ ))
;; Record tag and the binding/auto-binding.
(push (cons current-tag current-tag-char) tag-table-local)
;; Last column in the row.
(when (= (cl-incf field-number) (/ (- (window-width) 4) field-width))
(unless (memq (caar tag-alist) '(:endgroup :endgrouptag))
(insert "\n")
(when (or ingroup intaggroup) (insert " ")))
(setq field-number 0))))))
(insert "\n")
;; Keep the tags in order displayed. Will be used later for sorting.
(setq tag-table-local (nreverse tag-table-local))
(goto-char (point-min))
(unless expert-interface (org-fit-window-to-buffer))
;; Read user input.
(setq rtn
(catch 'exit
(while t
(message "[a-z..]:toggle [SPC]:clear [RET]:accept [TAB]:edit [!] %sgroups%s"
(if (not groups) "no " "")
(if expert-interface " [C-c]:window" (if exit-after-next " [C-c]:single" " [C-c]:multi")))
(setq input-char
(let ((inhibit-quit t)) ; intercept C-g.
(read-char-exclusive)))
;; FIXME: Global variable used by `org-beamer-select-environment'.
;; Should factor it out.
(setq org-last-tag-selection-key input-char)
(pcase input-char
;; <RET>
(?\r (throw 'exit t))
;; Toggle tag groups.
(?!
(setq groups (not groups))
(goto-char (point-min))
(while (re-search-forward "[{}]" nil t) (replace-match " ")))
;; Toggle expert interface.
(?\C-c
(if (not expert-interface)
(org-fast-tag-show-exit
(setq exit-after-next (not exit-after-next)))
(setq expert-interface nil)
(delete-other-windows)
(set-window-buffer (split-window-vertically) " *Org tags*")
(switch-to-buffer-other-window " *Org tags*")
(org-fit-window-to-buffer)))
;; Quit.
2024-04-23 15:13:29 -04:00
((or ?\C-g ?q)
2024-04-09 14:05:56 -04:00
(delete-overlay org-tags-overlay)
;; Quit as C-g does.
(keyboard-quit))
;; Clear tags.
(?\s
(setq current-tags nil)
(when exit-after-next (setq exit-after-next 'now)))
;; Use normal completion.
(?\t
;; Compute completion table, unless already computed.
(unless tab-tags
(setq tab-tags
(delq nil
(mapcar (lambda (x)
(let ((item (car-safe x)))
(and (stringp item)
(list item))))
;; Complete using all tags; tags from current buffer first.
(org--tag-add-to-alist
(with-current-buffer origin-buffer
(org-get-buffer-tags))
tag-table)))))
(setq current-tag (completing-read "Tag: " tab-tags))
(when (string-match "\\S-" current-tag)
(cl-pushnew (list current-tag) tab-tags :test #'equal)
(setq current-tags (org--add-or-remove-tag current-tag current-tags groups)))
(when exit-after-next (setq exit-after-next 'now)))
;; INPUT-CHAR is for a todo keyword.
((let (and todo-keyword (guard todo-keyword))
(car (rassoc input-char todo-table)))
(with-current-buffer origin-buffer
(save-excursion (org-todo todo-keyword)))
(when exit-after-next (setq exit-after-next 'now)))
;; INPUT-CHAR is for a tag.
((let (and tag (guard tag))
(car (rassoc input-char tag-table-local)))
(setq current-tags (org--add-or-remove-tag tag current-tags groups))
(when exit-after-next (setq exit-after-next 'now))))
;; Create a sorted tag list.
(setq current-tags
(sort current-tags
(lambda (a b)
;; b is after a.
;; `memq' returns tail of the list after the match + the match.
(assoc b (cdr (memq (assoc a tag-table-local) tag-table-local))))))
;; Exit when we are set to exit immediately.
(when (eq exit-after-next 'now) (throw 'exit t))
;; Continue setting tags in the loop.
;; Update the currently active tags indication in the completion buffer.
(goto-char (point-min))
(forward-line 1)
(delete-region (point) (line-end-position))
(org-fast-tag-insert "Current" current-tags current-face)
;; Update the active tags displayed in the overlay in Org buffer.
(org-set-current-tags-overlay current-tags ov-prefix)
;; Update tag faces in the displayed tag grid.
(let ((tag-re (concat "\\[.\\] \\(" org-tag-re "\\)")))
(while (re-search-forward tag-re nil t)
(let ((tag (match-string 1)))
(add-text-properties
(match-beginning 1) (match-end 1)
(list 'face
(cond
((member tag current-tags) current-face)
((member tag inherited-tags) inherited-face)
(t 'default)))))))
(goto-char (point-min)))))
;; Clear the tag overlay in Org buffer.
(delete-overlay org-tags-overlay)
;; Return the new tag list.
(if rtn
(mapconcat 'identity current-tags ":")
nil)))))
#+end_src
The last problem is that Org automatically assigns keys alphabetically if not specified, which means keys can often be difficult to reach. To fix this, we can simply configure some variables.
#+begin_src emacs-lisp
(after! org
(setq org--fast-tag-selection-keys
(string-to-list "asdfghjklrueitywovnASDFGHJKLRUEITYWOVN")
org-fast-tag-selection-maximum-tags 40))
#+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
2024-03-08 18:42:28 -05:00
*** Attachment Inline Previews
2024-03-22 00:54:59 -04:00
Doom enhances Org mode's attachment system to show inline previews of attached images. However, this appears to be broken due to using outdated APIs, so we have to patch the link parameter responsible.
2024-03-08 18:42:28 -05:00
#+begin_src emacs-lisp
(defadvice! ~/org-image-file-data-fn (protocol link _description)
"Properly handle attachment links."
:override #'+org-image-file-data-fn
(setq link
(pcase protocol
("download"
(expand-file-name
link
(or (if (require 'org-download nil t) org-download-image-dir)
(if (require 'org-attach) org-attach-id-dir)
default-directory)))
("attachment"
(require 'org-attach)
(org-attach-expand link))
(_ (expand-file-name link default-directory))))
(when (and (file-exists-p link)
(image-type-from-file-name link))
(with-temp-buffer
(set-buffer-multibyte nil)
(setq buffer-file-coding-system 'binary)
(insert-file-contents-literally link)
(buffer-substring-no-properties (point-min) (point-max)))))
(defadvice! ~/org-init-attach-link ()
"Properly set attachment link's parameters."
:after #'+org-init-attachments-h
(org-link-set-parameters "attachment" :image-data-fun #'+org-image-file-data-fn))
#+end_src
2024-03-22 00:53:45 -04:00
*** Inline Image Previews
2024-03-08 18:42:28 -05:00
2024-03-22 00:53:45 -04:00
Inline images in Org are file links pointing to images without a description. User-defined inline images can have a description, however, which makes for a disparity between file links and other links that is very counter-intuitive. We can advise the image data provider functions to fix this, and I'll add a variable to restore the default behavior if I ever want to.
#+begin_src emacs-lisp
(defvar +org-inline-image-desc nil
"Whether to allow inline images to contain descriptions.")
(defadvice! +org-inline-desc (old-fn protocol link description)
"Disable inline images with descriptions when `+org-inline-image-desc'
is non-nil."
:around '(org-yt-image-data-fun
+org-inline-image-data-fn
+org-http-image-data-fn)
(when (or +org-inline-image-desc (null description))
(funcall old-fn protocol link description)))
#+end_src
This works fine for explicitly displaying inline images. However, toggling using =RET= is now broken, as the command does not properly test for inline images. This will be fixed in the next section.
*** DWIM Command
The command ~+org/dwim-at-point~ will toggle all overlays in a subtree even if there are other actions that are more likely to be what the user meant (such as marking as DONE). We also need to change the interaction with links to properly account for whether the link has a description.
Annoyingly, the only good way to fix these issues is to completely override the extremely long function.
2024-03-08 18:42:28 -05:00
#+begin_src emacs-lisp
(defadvice! ~/org-dwim (old-fn &optional arg)
"Various tweaks to the function of the DWIM command."
2024-03-08 18:42:28 -05:00
:override #'+org/dwim-at-point
(if (button-at (point))
(call-interactively #'push-button)
(let* ((context (org-element-context))
(type (org-element-type context)))
(while (and context (memq type '(verbatim code bold italic underline strike-through subscript superscript)))
(setq context (org-element-property :parent context)
type (org-element-type context)))
(pcase type
((or `citation `citation-reference)
(org-cite-follow context arg))
(`headline
(cond ((memq (bound-and-true-p org-goto-map)
(current-active-maps))
(org-goto-ret))
((and (fboundp 'toc-org-insert-toc)
(member "TOC" (org-get-tags)))
(toc-org-insert-toc)
(message "Updating table of contents"))
((string= "ARCHIVE" (car-safe (org-get-tags)))
(org-force-cycle-archived))
((or (org-element-property :todo-type context)
(org-element-property :scheduled context))
(org-todo
(if (eq (org-element-property :todo-type context) 'done)
(or (car (+org-get-todo-keywords-for (org-element-property :todo-keyword context)))
'todo)
'done)))
(t
(let* ((beg (if (org-before-first-heading-p)
(line-beginning-position)
(save-excursion (org-back-to-heading) (point))))
(end (if (org-before-first-heading-p)
(line-end-position)
(save-excursion (org-end-of-subtree) (point))))
(overlays (ignore-errors (overlays-in beg end)))
(latex-overlays
(cl-find-if (lambda (o) (eq (overlay-get o 'org-overlay-type) 'org-latex-overlay))
overlays))
(image-overlays
(cl-find-if (lambda (o) (overlay-get o 'org-image-overlay))
overlays)))
(+org--toggle-inline-images-in-subtree beg end)
(if (or image-overlays latex-overlays)
(org-clear-latex-preview beg end)
(org--latex-preview-region beg end)))))
(org-update-checkbox-count)
(org-update-parent-todo-statistics)
(when (and (fboundp 'toc-org-insert-toc)
(member "TOC" (org-get-tags)))
(toc-org-insert-toc)
(message "Updating table of contents"))
)
(`clock (org-clock-update-time-maybe))
(`footnote-reference
(org-footnote-goto-definition
(org-element-property :label context)))
(`footnote-definition
(org-footnote-goto-previous-reference
(org-element-property :label context)))
((or `planning `timestamp)
(org-follow-timestamp-link))
((or `table `table-row)
(if (org-at-TBLFM-p)
(org-table-calc-current-TBLFM)
(ignore-errors
(save-excursion
(goto-char (org-element-property :contents-begin context))
(org-call-with-arg 'org-table-recalculate (or arg t))))))
(`table-cell
(org-table-blank-field)
(org-table-recalculate arg)
(when (and (string-empty-p (string-trim (org-table-get-field)))
(bound-and-true-p evil-local-mode))
(evil-change-state 'insert)))
(`babel-call
(org-babel-lob-execute-maybe))
(`statistics-cookie
(save-excursion (org-update-statistics-cookies arg)))
((or `src-block `inline-src-block)
(org-babel-execute-src-block arg))
((or `latex-fragment `latex-environment)
(org-latex-preview arg))
(`link
(let* ((lineage (org-element-lineage context '(link) t))
(path (org-element-property :path lineage)))
(if (or (equal (org-element-property :type lineage) "img")
2024-03-22 00:53:45 -04:00
(and path (image-supported-file-p path)
(or +org-inline-image-desc
(not (org-element-property :contents-begin lineage)))))
2024-03-08 18:42:28 -05:00
(+org--toggle-inline-images-in-subtree
(org-element-property :begin lineage)
(org-element-property :end lineage))
(org-open-at-point arg))))
((guard (org-element-property :checkbox (org-element-lineage context '(item) t)))
(org-toggle-checkbox)
(unless arg
(org-next-item)
(beginning-of-line)
(re-search-forward "\\[.\\] ")))
2024-03-08 18:42:28 -05:00
(`paragraph
(+org--toggle-inline-images-in-subtree))
(_
(if (or (org-in-regexp org-ts-regexp-both nil t)
(org-in-regexp org-tsr-regexp-both nil t)
(org-in-regexp org-link-any-re nil t))
(call-interactively #'org-open-at-point)
(+org--toggle-inline-images-in-subtree
(org-element-property :begin context)
(org-element-property :end context))))))))
#+end_src
2024-03-19 03:26:48 -04:00
*** Default Categories
2024-04-01 23:33:27 -04:00
When an explicit category is not specified, Org mode typically defaults to the filename (sans extension). This ... sort of makes sense? I guess? It doesn't really, because filename conventions don't make for good agenda category names. I want my category names to be in title case, whereas a file name is typically going to be all lowercase and without spaces. This is especially bad for Org-roam, where filenames are automatically generated and way too long to be a UI element.
2024-03-19 03:26:48 -04:00
To fix this issue, it is thankfully rather simple to patch Org-mode's category system[fn:2]. The following code sets things up so that the file's =#+title= metadata is used as the default category, falling back on the default behavior if a title is not given.
2024-03-19 03:26:48 -04:00
#+begin_src emacs-lisp
(defadvice! ~/org-default-category (old-fn)
"Modify how Org resolves the default category through the
`org-category' variable."
:around '(org-refresh-category-properties org-element--get-category)
(let ((org-category (or org-category (org-get-title))))
(funcall old-fn)))
#+end_src
[fn:2] Where by "simple" I mean that it took me multiple hours of combing through Org's source code in order to find the multiple places(???) where this behavior was implemented and to figure out how to modify it. At least the final code is short!
2024-03-19 03:26:48 -04:00
2024-02-28 12:04:54 -05:00
** Org Roam
#+call: confpkg("Org: Roam")
2024-02-28 12:04:54 -05:00
2024-03-05 03:03:24 -05:00
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.
2024-02-28 12:04:54 -05:00
*** Concept
2024-03-05 03:03:24 -05:00
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.
2024-02-28 12:04:54 -05:00
*** Task Management
2024-03-05 03:03:24 -05:00
In my use of Org-roam for task management, I divide nodes into a few different categories:
2024-02-28 12:04:54 -05:00
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.
2024-03-05 03:03:24 -05:00
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.
2024-02-28 12:04:54 -05:00
#+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
:inner-join tags :on (= dest node_id)
:where (= source $s1)
:and (= type "id")
2024-03-19 15:13:52 -04:00
:and (= tag $s2)]
(org-roam-node-id node) tag)))
(mapcar (lambda (id) (org-roam-populate
(org-roam-node-create :id (car id))))
response)))
2024-02-28 12:04:54 -05:00
#+end_src
2024-04-01 23:18:46 -04:00
*** Bindings
Here's a handful of bindings that streamline common operations I use:
2024-03-22 00:54:41 -04:00
#+begin_src emacs-lisp
(map! :mode org-mode
:after org
:localleader
"m l" #'org-roam-link-replace-all
"m R" #'org-roam-extract-subtree)
#+end_src
2024-04-09 14:05:01 -04:00
*** Roam File Names
By default, Roam generates file names containing both a timestamp and the node title. This makes it annoying if I want to change the title of that node later, so I'll change it to just the timestamp.
#+begin_src emacs-lisp
(after! org-roam
(setq org-roam-extract-new-file-path "%<%Y-%m-%d_%H-%M-%S>.org"))
#+end_src
2024-02-28 12:04:54 -05:00
*** Roam Buffer
2024-05-11 20:54:11 -04:00
The unlinked references section is turned off by default for performance reasons, but sometimes I want to see that information when writing notes. I also don't want it to show up for every node, because some of my nodes have quite a large reference list. Let's add a predicate to control when unlinked references are shown.
#+begin_src emacs-lisp
(defadvice! ~/org-roam-unlinked-references-p (node)
"A predicate to control when unlinked references are shown in the `org-roam' buffer."
:before-while #'org-roam-unlinked-references-section
(assoc "UNLINKED_REFS" (org-roam-node-properties node)))
#+end_src
2024-02-28 12:04:54 -05:00
#+begin_src emacs-lisp
(after! org-roam
2024-03-02 17:29:38 -05:00
(setq org-roam-mode-sections
2024-02-28 12:04:54 -05:00
'((org-roam-backlinks-section :unique t)
org-roam-reflinks-section
org-roam-unlinked-references-section)))
#+end_src
2024-03-30 16:14:33 -04:00
*** 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
** Capture
Org capture is a very useful mechanism for shortening the time it takes to "capture" information in your notes. The default method for configuring capture is the variable ~org-capture-templates~, which specifies templates to generate new entries with.
*** YASnippet Integration
These templates are all well and good, but there's a crucial issue with them: we already have a perfectly good template system! [[*Snippets][Snippets]] are highly versatile and useful, and it would be nice if we could leverage their features in Org capture templates.
The goal is to hook into Org's capturing system to get it to feed its template into YASnippet, instead of printing it directly.
#+begin_src emacs-lisp
(defadvice! org-capture-yasnippet (old-fn &optional arg)
"Modify Org's capture system to expand a YASnippet template."
:around #'org-capture-place-template
(let* ((template (org-capture-get :template))
(org-capture-plist (plist-put org-capture-plist :template "")))
(cl-letf (((symbol-function 'org-kill-is-subtree-p) #'always))
(funcall old-fn arg)
2024-04-23 15:06:41 -04:00
(yas-expand-snippet template))))
2024-03-30 16:14:33 -04:00
#+end_src
This advice overrides ~org-capture-place-template~, the function that:
1. Moves to the location of capture inside the file
2. Modifies the template to fit its context
3. Places the template in the file
2024-04-23 15:13:29 -04:00
The advice temporarily sets Org's capture template to an empty string, lets Org do its thing, and then expands the template itself. This does mean that Org no longer performs point 2, but YASnippet is powerful enough that we can simply do that ourselves in the template.
2024-03-30 16:14:33 -04:00
2024-02-28 12:04:54 -05:00
*** Roam Capture
2024-03-30 16:14:33 -04:00
Since Org-roam is currently my primary method of using Org, I only use its templating system. We'll start with defining some useful node accessor functions.
2024-02-28 12:04:54 -05:00
#+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."
2024-04-23 15:10:07 -04:00
(setq dir (expand-file-name (or dir "") org-roam-directory))
2024-02-28 12:04:54 -05:00
(or (org-roam-node-file node)
2024-04-23 15:10:07 -04:00
(expand-file-name "%<%Y-%m-%d_%H-%M-%S>.org" dir)))
(defun org-roam-node-file-maybe-cite (node)
(org-roam-node-file-maybe node "cite"))
2024-02-28 12:04:54 -05:00
2024-03-30 16:14:33 -04:00
(defun org-roam-node-file-maybe-dir (node)
2024-02-28 12:04:54 -05:00
"Get file name from NODE, or ask for directory and return a default filename."
(or (org-roam-node-file node)
2024-03-30 16:14:33 -04:00
(expand-file-name
2024-04-23 15:10:07 -04:00
"%<%Y-%m-%d_%H-%M-%S>.org"
2024-03-30 16:14:33 -04:00
(read-directory-name "Directory: " org-roam-directory))))
2024-02-28 12:04:54 -05:00
#+end_src
2024-03-30 16:14:33 -04:00
I want there to only be one capture template when I'm making new links, so that I don't get distracted. However, when I'm explicitly telling Roam to make a new node, it would be a shame if there was only one option. To fix this, I'll add new variables representing the /default/ templates to use in situations where I don't want to have to choose.
2024-02-28 12:04:54 -05:00
#+begin_src emacs-lisp
2024-03-30 16:14:33 -04:00
(defvar org-roam-capture-default-template nil
"The default capture template to use when not explicitly capturing.")
2024-02-28 12:04:54 -05:00
2024-03-30 16:14:33 -04:00
(defvar org-roam-dailies-capture-default-template nil
"The default daily capture template to use when not explicitly capturing.")
2024-02-28 12:04:54 -05:00
#+end_src
2024-03-30 16:14:33 -04:00
With those variables created, we can define our templates.
2024-02-19 18:00:05 -05:00
2024-03-30 16:14:33 -04:00
#+begin_src emacs-lisp
(after! org-roam
;; Default templates
(setq org-roam-capture-default-template
'("d" "Default" plain ""
:target (file+head "${file-maybe-dir}"
"#+title: ${title}")
:unnarrowed t
:immediate-finish t)
org-roam-dailies-capture-default-template
'("d" "Default" plain ""
:target (file+head "%<%Y-%m-%d>.org"
"#+title: %<%Y-%m-%d>")
:unnarrowed t
:immediate-finish t))
;; Active templates
(setq org-roam-capture-templates
'(("f" "Standalone file" plain ""
:target (file+head "${file-maybe-dir}"
"#+title: ${title}")
:unnarrowed t)
2024-04-23 15:10:07 -04:00
("n" "Citation Note" plain ""
:target (file+head "${file-maybe-cite}"
"#+title: ${note-title}")
:immediate-finish t
:unnarrowed t)
2024-03-30 16:14:33 -04:00
("h" "Heading" entry
2024-04-23 15:06:41 -04:00
"`(make-string (org-outline-level) ?*)`* ${title}
2024-03-30 16:14:33 -04:00
:RELATED:
Subnote of `(let ((node (org-roam-node-at-point)))
(format \"[[id:%s][%s]]\"
(org-roam-node-id node)
(org-roam-node-title node)))`$1
:END:\n\n$0"
:target (file "20240329114914-a.org")))
org-roam-dailies-capture-templates
2024-04-23 15:09:02 -04:00
'(("e" "Event" entry "* $1\n%^t\n\n$0"
:target (file+head "%<%Y-%m-%d>.org"
"#+title: %<%Y-%m-%d>")
:empty-lines 1)
("t" "Task" entry "* TODO $1\n\n$0"
:target (file+head "%<%Y-%m-%d>.org"
"#+title: %<%Y-%m-%d>")
2024-03-30 16:14:33 -04:00
:empty-lines 1)
2024-04-23 15:06:41 -04:00
("n" "Notes" entry "** $0"
2024-03-30 16:14:33 -04:00
:target (file+head+olp "%<%Y-%m-%d>.org"
"#+title: %<%Y-%m-%d>"
("Course Notes :notes:"))
:unnarrowed t))))
#+end_src
Now we just have to advise Org-roam with the proper logic!
#+begin_src emacs-lisp
(defadvice! ~/org-roam-default-capture (old-fn &rest args)
"Use default capture template when not explicitly capturing."
:around '(org-roam-node-find org-roam-node-insert)
(let ((org-roam-capture-templates (list org-roam-capture-default-template)))
(apply old-fn args)))
(defadvice! ~/org-roam-dailies-default-capture (old-fn time &optional goto keys)
"Use default capture template when not explicitly capturing."
:around #'org-roam-dailies--capture
(let ((org-roam-dailies-capture-templates
(if goto (list org-roam-dailies-capture-default-template)
org-roam-dailies-capture-templates)))
(funcall old-fn time goto keys)))
2024-02-19 18:00:05 -05:00
#+end_src
** Agenda
#+call: confpkg("Org: Agenda")
2024-02-19 18:00:05 -05:00
2024-02-25 00:06:21 -05:00
*** Configuration
2024-03-05 03:03:24 -05:00
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.
2024-02-25 00:06:21 -05:00
#+begin_src emacs-lisp
(after! org
(setq org-agenda-span 'day
org-agenda-start-day nil
2024-02-27 15:31:31 -05:00
org-agenda-start-on-weekday 1 ; 1 = Monday
2024-02-25 00:06:21 -05:00
2024-02-27 15:31:31 -05:00
org-agenda-sorting-strategy
2024-06-13 14:29:32 -04:00
'((agenda time-up habit-down prority-down category-up)
(todo habit-down priority-down time-up category-up)
(tags habit-down priority-down time-up category-up)
2024-03-08 18:43:39 -05:00
(search category-up))
;; Make sure agenda is the only window
org-agenda-window-setup 'only-window
org-agenda-restore-windows-after-quit t
;; Agenda prefix
org-agenda-prefix-format
'((agenda . " %i %-18:c%?-12t% s")
(todo . " %i %-18:c")
(tags . " %i %-18:c")
(search . " %i %-18:c"))))
2024-02-25 00:06:21 -05:00
#+end_src
2024-02-27 15:31:31 -05:00
*** Agenda View
2024-04-23 15:13:29 -04:00
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 reading my todos easier, so we'll use ~org-super-agenda~:
2024-02-27 15:31:31 -05:00
#+begin_src emacs-lisp :tangle packages.el
2024-02-27 15:31:31 -05:00
(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))
2024-02-28 21:59:54 -05:00
;; This map is unnecessary and causes evil bindings to not work
;; while on super agenda headers
2024-02-27 15:31:31 -05:00
(setq org-super-agenda-header-map nil))
#+end_src
2024-02-19 18:00:05 -05:00
2024-03-05 03:03:24 -05:00
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.
2024-04-01 23:33:27 -04:00
By customizing ~org-super-agenda-groups~ via a let-binding in my custom agenda view, I can set up a sophisticated multi-category agenda system.
2024-02-19 18:00:05 -05:00
#+begin_src emacs-lisp
2024-02-27 15:31:31 -05:00
(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)))
2024-02-28 21:59:13 -05:00
(links (~/org-roam-get-linked-nodes node tag)))
(->> links
(mapcar #'org-roam-node-title)
(-interpose ", ")
(apply #'concat prefix))))
2024-02-27 15:31:31 -05:00
2024-02-19 18:00:05 -05:00
(after! org
(setq org-agenda-custom-commands
2024-02-27 15:31:31 -05:00
'(("o" "Overview"
2024-02-19 18:00:05 -05:00
((agenda "")
2024-02-27 15:31:31 -05:00
(alltodo ""
((org-super-agenda-groups
2024-03-06 01:27:38 -05:00
'((:name "Next/In Progress"
:todo ("NEXT" "STRT" "WAIT" "HOLD"))
2024-02-27 15:31:31 -05:00
(:name "Important"
2024-03-06 01:27:38 -05:00
:priority "A"
:order 1)
(:name "Notes to Intake"
:tag "notes"
:order 10)
2024-02-27 15:31:31 -05:00
(:name "Assignments"
:tag "assign"
:order 2)
(:auto-map (lambda (item)
2024-03-06 01:27:38 -05:00
(~/org-agenda-section-by-link
"Goal: " "goal" item))
2024-02-27 15:31:31 -05:00
:order 3)
2024-03-06 01:27:38 -05:00
(:auto-map (lambda (item)
(~/org-agenda-section-by-link
"Area: " "area" item))
:order 4))))))))))
2024-04-01 23:33:27 -04:00
#+end_src
2024-04-01 23:33:27 -04:00
This "overview" agenda command is very nice. It's so nice, in fact, that it's almost always the only agenda view I want to use! Having to go through the dispatcher every time I want to access it is annoying and unnecessary.
2024-02-19 18:00:05 -05:00
2024-04-01 23:33:27 -04:00
#+begin_src emacs-lisp
2024-02-19 18:00:05 -05:00
(defun ~/org-agenda (&optional arg)
"Wrapper around preferred agenda view."
(interactive "P")
2024-02-27 15:31:31 -05:00
(org-agenda arg "o"))
2024-02-19 18:00:05 -05:00
(map! :leader
:desc "Org agenda"
"o a" #'~/org-agenda
2024-04-01 23:33:27 -04:00
;; Use shift to access full dispatcher
:desc "Org agenda dispatcher"
2024-02-19 18:00:05 -05:00
"o A" #'org-agenda)
#+end_src
*** Agenda Files
2024-03-07 01:43:29 -05:00
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! We can solve this by creating a function ~org-agenda-files-function~ to return the agenda files, then advising Org to call that function before getting the agenda files.
2024-02-19 18:00:05 -05:00
2024-03-07 01:43:29 -05:00
#+begin_src emacs-lisp
(defun org-agenda-files-function ()
(require 'find-lisp)
(find-lisp-find-files org-directory "\.org$"))
2024-02-19 18:00:05 -05:00
2024-03-07 01:43:29 -05:00
(defvar org-agenda-files-function #'org-agenda-files-function
"The function to determine the org agenda files.")
2024-02-19 18:00:05 -05:00
2024-03-07 01:43:29 -05:00
(defadvice! ~/org-agenda-files (&rest _)
"Set `org-agenda-files' before Org fetches them."
:before #'org-agenda-files
(setq org-agenda-files (funcall org-agenda-files-function)))
2024-02-19 18:00:05 -05:00
#+end_src
** Citations
#+call: confpkg("Org: Cite")
2024-02-19 18:00:05 -05:00
2024-03-19 03:28:46 -04:00
Org mode has a very nice notation for specifying citations in square brackets, and the package =citar= uses this to implement a useful bibliography system.
2024-02-19 18:00:05 -05:00
2024-03-05 03:03:24 -05:00
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.
2024-02-19 18:00:05 -05:00
#+begin_src emacs-lisp
(after! org
(setq org-cite-csl-styles-dir "~/Zotero/styles"
2024-04-23 15:10:07 -04:00
;; MLA style by default
org-cite-csl--fallback-style-file
"/home/kiana/Zotero/styles/modern-language-association.csl"
org-cite-global-bibliography
(list (expand-file-name "library.json" org-directory))
2024-02-19 18:00:05 -05:00
citar-bibliography org-cite-global-bibliography))
#+end_src
2024-04-23 15:10:07 -04:00
*** Citation Settings
I primarily use the CSL export processor to create MLA-style citation, so let's configure that to make its citations more standard.
#+begin_src emacs-lisp
(after! org
(setq org-cite-csl-link-cites nil ; This is not recommended by MLA
))
#+end_src
*** Roam Citation Notes
We can use the package =citar-org-roam= (bundled with Doom module =:tools biblio=) to create citation notes in Roam.
#+begin_src emacs-lisp
(after! citar-org-roam
(setq citar-org-roam-subdir "cite"
citar-org-roam-capture-template-key "n"))
#+end_src
*** Aesthetics
We should also make Org citations look a little prettier:
2024-02-19 18:00:05 -05:00
#+begin_src emacs-lisp
;; Make faces conform to theme
(after! org
(custom-set-faces!
`(org-cite :foreground ,(doom-color 'green))
2024-04-23 15:10:07 -04:00
'(org-cite-key :foreground nil :slant italic :underline t :inherit org-cite)))
2024-02-19 18:00:05 -05:00
;; 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")
2024-02-19 18:00:05 -05:00
2024-03-05 03:03:24 -05:00
I don't use ~org-journal~ anymore, but I'm keeping my old configuration for it in case I want to go back.
2024-02-19 18:00:05 -05:00
#+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
2024-03-05 03:03:24 -05:00
To make opening the journal more convenient, here's a command to open the latest entry:
2024-02-19 18:00:05 -05:00
#+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
2024-03-22 00:54:59 -04:00
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 personal configuration, as the bounds of what a language mode needs to do are typically defined by the language itself.
2024-02-19 18:00:05 -05:00
2024-05-11 20:54:11 -04:00
** Indentation
#+call: confpkg("Indent")
I prefer 2-space indentation in all circumstances. Unfortunately, Emacs's indentation is mode-specific, so I have to configure every mode's indentation separately.
#+begin_src emacs-lisp
(after! css-mode
(setq css-indent-offset 2))
#+end_src
2024-02-19 18:00:05 -05:00
** Dired
#+call: confpkg("Mode: Dired")
2024-02-19 18:00:05 -05:00
2024-03-05 03:03:24 -05:00
Dired by default spawns a new buffer for every directory, which clutters up your buffer list very quickly.
2024-02-19 18:00:05 -05:00
#+begin_src emacs-lisp
(after! dired
(setq dired-kill-when-opening-new-dired-buffer t))
#+end_src
2024-03-02 17:28:30 -05:00
** Haskell
#+call: confpkg("Mode: Haskell")
2024-03-02 17:28:30 -05:00
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
2024-04-01 23:18:46 -04:00
** Idris
2024-03-02 17:28:30 -05:00
:PROPERTIES:
:header-args:emacs-lisp: :noweb-ref idris-config
:END:
2024-03-05 03:03:24 -05:00
Idris's support for Emacs is still experimental and has a few kinks to work out, especially since I'm using Idris 2, which the recommended =idris-mode= needs to be configured to use.
2024-03-02 17:28:30 -05:00
The Doom module is very outdated, so I'll be overriding it.
#+begin_src emacs-lisp :tangle modules/lang/idris/packages.el :noweb-ref none
;; -*- no-byte-compile: t; -*-
;;; lang/idris/packages.el
(package! idris-mode)
#+end_src
#+begin_src emacs-lisp :tangle modules/lang/idris/config.el :noweb yes :noweb-ref none :exports none
;;; -*- lexical-binding: t; -*-
;;; lang/idris/config.el
<<idris-config>>
#+end_src
*** Config
#+begin_src emacs-lisp
(after! idris-mode
(setq idris-interpreter-path "idris2"
idris-repl-banner-functions
2024-03-05 02:51:04 -05:00
'(idris-repl-text-banner) ; No fun allowed!!
2024-03-02 17:28:30 -05:00
idris-repl-show-repl-on-startup nil ; Don't show repl on startup
)
(add-hook! idris-mode :append #'lsp!)
(set-repl-handler! 'idris-mode (cmd! (idris-repl-buffer)))
(set-lookup-handlers! 'idris-mode
:documentation #'idris-docs-at-point)
(map! :localleader
:mode idris-mode
"q" #'idris-quit
"l" #'idris-load-file
"t" #'idris-type-at-point
"a" #'idris-add-clause
"e" #'idris-make-lemma
"c" #'idris-case-dwim
"w" #'idris-make-with-block
"m" #'idris-add-missing
"p" #'idris-proof-search
"h" #'idris-docs-at-point
(:prefix ("i" . "ipkg")
"f" #'idris-open-package-file
"b" #'idris-ipkg-build
"c" #'idris-ipkg-clean
"i" #'idris-ipkg-install))
(map! :localleader
:mode idris-ipkg-mode
"b" #'idris-ipkg-build
"c" #'idris-ipkg-clean
"i" #'idris-ipkg-install
"f" #'idris-ipkg-insert-field))
2024-03-31 02:49:38 -04:00
#+end_src
2024-03-02 17:28:30 -05:00
*** Appearance
2024-03-05 03:03:24 -05:00
Operators being in italics looks ugly in this mode too, but unfortunately due to Idris's more complicated syntax highlighting system we have to do a bit more work than for Haskell. There's also the issue of the semantic highlighting faces, which don't match our theme colors.
2024-03-02 17:28:30 -05:00
#+begin_src emacs-lisp
(after! idris-mode
(custom-set-faces!
'((idris-operator-face idris-colon-face idris-equals-face) :slant normal)
;; Semantic highlighting
`(idris-semantic-type-face :foreground ,(doom-color 'blue))
`(idris-semantic-data-face :foreground ,(doom-color 'red))
`(idris-semantic-bound-face :foreground ,(doom-color 'magenta) :slant italic)
`(idris-semantic-function-face :foreground ,(doom-color 'dark-green))
`(idris-semantic-postulate-face :foreground ,(doom-color 'yellow))))
#+end_src
2024-03-08 18:54:04 -05:00
2024-05-11 20:54:11 -04:00
** Yuck
#+call: confpkg("Mode: Yuck")
Since I've started using [[https://github.com/elkowar/eww/tree/master?tab=readme-ov-file][EWW (Elkowar's Wacky Widgets)]] for my linux setup, it would be nice to have an editing mode for its configuration language.
#+begin_src emacs-lisp :tangle packages.el
(package! yuck-mode)
#+end_src
2024-03-08 18:54:04 -05:00
* Scratch
#+call: confpkg()
This section is for code with little or no associated documentation. This could be because the code is:
1. Temporary
2. Unimportant
3. Self-explanatory
4. Just not really worth the time it takes to write these explanations
** Org
*** Automate Problem List
#+begin_src emacs-lisp
(defvar ~/org-problem-spec-alist nil
"An alist of regexps matching problem specs.")
(setq ~/org-problem-spec-alist
2024-03-22 00:54:00 -04:00
`((,(rx (group (+ digit))
(* space) "-" (* space)
(group (+ digit))
(* space) "odd")
. ,(lambda (beg end)
(when (cl-evenp beg) (cl-incf beg))
(number-sequence beg end 2)))
(,(rx (group (+ digit))
(* space) "-" (* space)
(group (+ digit))
(* space) "even")
. ,(lambda (beg end)
(when (cl-oddp beg) (cl-incf beg))
(number-sequence beg end 2)))
(,(rx (group (+ digit))
(* space) "-" (* space)
(group (+ digit))) . number-sequence)
(,(rx (group (+ digit))) . list)))
2024-03-08 18:54:04 -05:00
(defun ~/org-generate-problem-list (&rest specs)
(interactive (s-split "," (read-from-minibuffer "Problems: ")))
(let* ((alist ~/org-problem-spec-alist)
(problems
(mapcan
(lambda (spec)
2024-03-22 00:54:00 -04:00
(let* ((match
(or (--some (-some-> (s-match (car it) spec)
cdr (cons (cdr it)))
alist)
(user-error "Invalid problem spec \"%s\"" spec))))
2024-03-08 18:54:04 -05:00
(apply (cdr match) (mapcar #'string-to-number (car match)))))
specs)))
2024-03-22 00:54:00 -04:00
(move-to-left-margin)
2024-03-08 18:54:04 -05:00
(dolist (num problems)
(insert (format "- [ ] %d\n" num)))))
#+end_src