Compare commits

..

No commits in common. "3e46106e9015e7048a23a4e2c25fcf8560845676" and "17bd8557db1c28d2edb4f43156c6fe7bac7df707" have entirely different histories.

3 changed files with 232 additions and 240 deletions

View file

@ -39,13 +39,13 @@ It was at this point that I started thinking about Emacs again. By chance, I hap
- Sensible defaults - Sensible defaults
- Extensive ecosystem - Extensive ecosystem
As I became more comfortable with configuration via scripting, I immersed myself into the many utilities that make up the Emacs ecosystem, especially =org-mode=, the gold standard of Emacs tools. I started putting more and more time into tweaking this environment 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. 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.
*** Literate Programming *** Literate Programming
My first Doom Emacs config was hacked together directly from the generated example config: no comments, no organization, nothing. ~after!~ and ~use-package!~ blocks were scattered about the file without rhyme or reason, making it very difficult to remember what any particular line of code was actually doing. I was able to mitigate some of this issue by categorizing my config into multiple files, but at the end of the day it was a losing battle. The config directory was at around 1200 lines of code before I decided that something needed to be done. 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.
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 was very comfortable with =org-mode= at this point, so it seemed like a good solution and a natural next step. 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 **** Org and Source Code
@ -113,7 +113,7 @@ As part of their literate config, Tecosaur implemented =confpkg=, an embedded Em
- Automatically detecting cross-section dependencies - Automatically detecting cross-section dependencies
- Reporting profiling information on config load times - Reporting profiling information on config load times
It's an incredibly impressive utility, and I highly recommend reading [[https://tecosaur.github.io/emacs-config/config.html#rudimentary-configuration-confpkg][the section in their config]] on its design. I tried to read through it myself, but I don't understand half of it; it's a bizarre mixture of exploits to hook into =org-mode='s tangling process, self-modifying buffer shenanigans, and abuse of various features of =org-babel=, the package that manages source blocks. 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.
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! 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!
@ -122,7 +122,7 @@ If you're reading the raw org file instead of the published version, the code fo
- Prevent =confpkg='s code from being exported - Prevent =confpkg='s code from being exported
- Change the package template to contain my information - Change the package template to contain my information
- Reorganize to get rid of superfluous noweb references - Reorganize to get rid of superfluous noweb references
- Hook only into babel calls that contain =confpkg= as a substring - Hook into only babel calls that contain =confpkg= as a substring
- Allow package statements anywhere in subconfig files, rather than only at the beginning - Allow package statements anywhere in subconfig files, rather than only at the beginning
** confpkg :noexport: ** confpkg :noexport:
@ -761,19 +761,11 @@ NODE defaults to the root node."
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. 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.
#+begin_src emacs-lisp :eval no #+begin_src emacs-lisp :tangle init.el :noweb no-export
;;; init.el -*- lexical-binding: t; -*- ;;; init.el -*- lexical-binding: t; -*-
;; This file controls what Doom modules are enabled and what order they load in. ;; This file controls what Doom modules are enabled and what order they load in.
(doom!
...
)
#+end_src
#+begin_src emacs-lisp :tangle init.el :exports none
;;; init.el -*- lexical-binding: t; -*-
(doom! <<doom-input>> (doom! <<doom-input>>
<<doom-completion>> <<doom-completion>>
@ -815,7 +807,7 @@ literate
** Completion ** Completion
I'm a big fan of the Vertico ecosystem, as it's lightweight and easy to use. Let's turn on that module, along with Corfu for a nice in-buffer completion popup and some icons because why not. I'm a big fan of the Vertico ecosystem, as it's lightweight and easy to use. Let's turn on that module, along with the icons flag because why not.
#+name: doom-completion #+name: doom-completion
#+begin_src emacs-lisp #+begin_src emacs-lisp
@ -870,7 +862,7 @@ ophints
;;tabs ;;tabs
(treemacs +lsp) (treemacs +lsp)
unicode unicode
(vc-gutter +pretty) (vc-gutter +diff-hl +pretty)
vi-tilde-fringe vi-tilde-fringe
(window-select +numbers) (window-select +numbers)
workspaces workspaces
@ -1000,7 +992,7 @@ emacs-lisp
;;hy ;;hy
idris idris
;;json ;;json
(java +lsp +tree-sitter) ;;(java +lsp)
;;javascript ;;javascript
;;julia ;;julia
;;kotlin ;;kotlin
@ -1049,7 +1041,8 @@ This is mostly config settings that don't belong to any particular package and a
It wouldn't be Emacs if there wasn't an endless list of config variables to change every aspect of its function! It wouldn't be Emacs if there wasn't an endless list of config variables to change every aspect of its function!
#+begin_src emacs-lisp #+begin_src emacs-lisp
(setq-default delete-by-moving-to-trash t ; Delete files to trash (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 window-combination-resize t ; Resize windows more evenly
) )
@ -1064,6 +1057,8 @@ It wouldn't be Emacs if there wasn't an endless list of config variables to chan
(global-subword-mode 1) (global-subword-mode 1)
#+end_src #+end_src
Thanks once again to Tecosaur for some of these settings.
** Personal Information ** Personal Information
#+call: confpkg() #+call: confpkg()
@ -1079,7 +1074,7 @@ Emacs uses this basic personal information for a few different things, mostly ap
#+call: confpkg("Auth") #+call: confpkg("Auth")
The =auth-source-pass= module lets you use [[*Password Management][pass]] as a source for authentication. Let's turn that on. I don't want my cache files to get deleted whenever I mess up my Doom install, so let's move them to somewhere more safe.
#+begin_src emacs-lisp #+begin_src emacs-lisp
(require 'auth-source-pass) (require 'auth-source-pass)
@ -1198,9 +1193,25 @@ If PARENTS is non-nil, the parents of the specified directory will also be creat
:desc "Undo Tree" :desc "Undo Tree"
"o u" #'undo-tree-visualize "o u" #'undo-tree-visualize
:desc "Open URL" :desc "Open URL"
"s u" #'goto-address-at-point "s u" #'goto-address-at-point)
:desc "Nerd Icons" #+end_src
"i i" #'nerd-icons-insert)
*** ... This is Also Here
I'm not even going to bother explaining this one. Emacs is just janky sometimes lol
#+begin_src emacs-lisp
(defadvice! ~/projectile-find-file (invalidate-cache &optional ff-variant)
:override #'projectile--find-file
(projectile-maybe-invalidate-cache invalidate-cache)
(let* ((project-root (projectile-acquire-root))
(file (read-file-name "Find file: " project-root project-root
(confirm-nonexistent-file-or-buffer) nil
))
(ff (or ff-variant #'find-file)))
(when file
(funcall ff (expand-file-name file project-root))
(run-hooks 'projectile-find-file-hook))))
#+end_src #+end_src
** Automated Nix Builds ** Automated Nix Builds
@ -1212,7 +1223,7 @@ Some packages in this config such as =treemacs=, =org-roam=, etc. require certai
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. 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. 2. Build the tool and put a symlink to the output somewhere, e.g. in the HOME directory. This avoids polluting the environment, but you still have to deal with an unwieldy symlink that breaks Emacs if you accidentally delete it.
This was my approach before coming up with the third option: This was my approach before coming up with the third option:
3. Build the tool and point Emacs directly to the store path. This is the most automatic solution, but requires the most complex Emacs configuration. 3. Build the tool and point Emacs directly to the store path. This is the simplest solution, but requires the most complex Emacs configuration.
This section is an implementation of that third solution. This section is an implementation of that third solution.
@ -1228,14 +1239,14 @@ If IMPURE is t, then allow impure builds."
(with-temp-message (format "Building \"%s\" ..." out) (with-temp-message (format "Building \"%s\" ..." out)
(with-temp-buffer (with-temp-buffer
(let* ((args `("build" "--no-link" "--print-out-paths" (let* ((args `("build" "--no-link" "--print-out-paths"
,@(when impure '("--impure")) ,out)) ,@(if impure "--impure") ,out))
(status (apply #'call-process nix-executable nil (status (apply #'call-process nix-executable nil
(list (current-buffer) nil) nil args))) (list (current-buffer) nil) nil args)))
(when (eql status 0) (when (eql status 0)
(s-trim (buffer-string))))))) (s-trim (buffer-string)))))))
#+end_src #+end_src
This works if we just want to start a build, but there's a problem: we haven't indicated to Nix that we're using this output for something, 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. This works well enough if we just want to build something, but there's a problem: we haven't indicated to Nix that we want this output to stick around, so it will be deleted the next time we garbage collect. To fix this, we can write a wrapper function that also makes the output path a garbage collection root.
#+begin_src emacs-lisp #+begin_src emacs-lisp
(defun nix-build-out-path-gcroot (name out &optional impure) (defun nix-build-out-path-gcroot (name out &optional impure)
@ -2197,8 +2208,7 @@ Doom Emacs sets up spell-checking with ~ispell~ (Emacs-internal tool) and =aspel
(setq ispell-dictionary "en_US")) (setq ispell-dictionary "en_US"))
#+end_src #+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 in this case it's easier to just put it in the usual location. 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.
Now that we have our new-and-improved template registry system, we can add new file templates as we please. Now that we have our new-and-improved template registry system, we can add new file templates as we please.
#+begin_src emacs-lisp #+begin_src emacs-lisp
@ -2286,7 +2296,18 @@ A natural thing to do when browsing through the project tree is create a new fil
*** Project Integration *** Project Integration
When I have a project open, Treemacs is flexible and allows me 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 completely messing up the existing project structure. This convenience function ensures that only the project directory is open. I often accidentally open the project tree before I've even selected a project, which I don't want because it messes up =treemacs-projectile=. Let's fix that problem:
#+begin_src emacs-lisp
(defun ~/treemacs-restrict (&rest _)
(unless (doom-project-p)
(user-error "Must be in a project to open project tree")))
(advice-add #'treemacs-select-window :before #'~/treemacs-restrict)
(advice-add #'+treemacs/toggle :before #'~/treemacs-restrict)
#+end_src
When I do have a project open, Treemacs is flexible and allows you to open directories other than that project. This /would/ be great and convenient if it weren't for the fact that it doesn't do so very well, often opening the wrong directories entirely. This convenience function ensures that only the project directory is open.
#+begin_src emacs-lisp #+begin_src emacs-lisp
(defun ~/treemacs-fix-project () (defun ~/treemacs-fix-project ()
@ -2535,34 +2556,6 @@ I use the standard Unix-style password management system, [[https://www.password
(set-popup-rule! "^\\*Password-Store" :side 'right :size 0.25 :quit nil)) (set-popup-rule! "^\\*Password-Store" :side 'right :size 0.25 :quit nil))
#+end_src #+end_src
*** Tweaks
For some unknown reason, the creators of the original =pass= package decided that when showing the pass buffer, the main dispatch function would call ~pop-to-buffer~ when it needs to be created, but ~switch-to-buffer~ when it already exists. These are different functions! Let's fix that.
#+begin_src emacs-lisp
(defadvice! ~/pass ()
"Fix inconsistent opening of `pass-mode' buffer."
:override #'pass
(if-let ((window (get-buffer-window pass-buffer-name)))
(progn
(select-window window)
(pass-update-buffer))
(pop-to-buffer pass-buffer-name)
(pass-setup-buffer)))
#+end_src
When visiting a password file, the file's buffer replaces the pass buffer, which isn't very good UX. To fix this, we can advise it to use ~pop-to-buffer~, folding it into the popup system.
#+begin_src emacs-lisp
(defadvice! ~/pass-view ()
"Use `find-file-other-window' instead of `find-file'."
:override #'pass-view
(pass--with-closest-entry entry
(pop-to-buffer
(find-file-noselect (concat (f-join (password-store-dir) entry) ".gpg"))
(cdr (+popup-make-rule nil '(:side bottom :size 8 :quit t :modeline t))))))
#+end_src
* Org * Org
#+call: confpkg() #+call: confpkg()
@ -2632,11 +2625,13 @@ four capitalized letters), and the cdr is a list of sub-tags.")
"Classes that belong under the :Online: tag.") "Classes that belong under the :Online: tag.")
(after! org (after! org
(setq classes-mwf '(("PHIL2010" . ?1) ("CSE1322" . ?2)) (setq classes-mwf '()
classes-tr '(("MATH2345" . ?3)) classes-tr '()
classes-online '() classes-online '()
subject-alist '(("MATH" ("calculus") ("algebra")) subject-alist '(("MATH" ("calculus") ("algebra"))
("HIST") ("POLS") ("ECON") ("PHIL"))) ("HIST")
("POLS")
("ECON")))
(setq org-tag-persistent-alist (setq org-tag-persistent-alist
`(("area" . ?A) ("goal" . ?G) ("project" . ?P) ("meta" . ?M) `(("area" . ?A) ("goal" . ?G) ("project" . ?P) ("meta" . ?M)
@ -2648,7 +2643,7 @@ four capitalized letters), and the cdr is a list of sub-tags.")
(lambda (subject) (lambda (subject)
`((:startgrouptag) (,(car subject)) `((:startgrouptag) (,(car subject))
(:grouptags) (:grouptags)
(,(format "{%s[[:digit:]]+L?}" (car subject))) (,(format "{%s[[:digit:]]+}" (car subject)))
,@(cdr subject) ,@(cdr subject)
(:endgrouptag))) (:endgrouptag)))
subject-alist) subject-alist)
@ -2676,17 +2671,14 @@ There are a few useful functions Doom doesn't bind by default, so let's add them
(map! :after org (map! :after org
:map org-mode-map :map org-mode-map
:localleader :localleader
"n" #'org-num-mode "N" #'org-num-mode
"C" #'org-columns "C" #'org-columns
"p" #'org-priority ; Remove extraneous commands
"g j" #'org-goto "g j" #'org-goto
"c D" #'org-clock-display "c D" #'org-clock-display
"m b f" #'org-table-eval-formula "m b f" #'org-table-eval-formula
"m b F" #'org-table-edit-formulas "m b F" #'org-table-edit-formulas
;; Remove extraneous commands
"a" #'org-attach
"p" #'org-priority
;; Map babel commands into localleader ;; Map babel commands into localleader
:desc "babel" :desc "babel"
"v" (lookup-key org-mode-map (kbd "C-c C-v"))) "v" (lookup-key org-mode-map (kbd "C-c C-v")))
@ -2702,9 +2694,7 @@ While this is a complex problem, the solution is actually rather simple: just re
(map! :after org (map! :after org
:map org-mode-map :map org-mode-map
"TAB" nil "TAB" nil
"<tab>" nil "<tab>" nil)
:i "TAB" nil
:i "<tab>" nil)
#+end_src #+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. If ~org-cycle~ isn't being called when in insert mode, then we can't use =TAB= to move fields. This means that we need a little extra binding configuration: 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. If ~org-cycle~ isn't being called when in insert mode, then we can't use =TAB= to move fields. This means that we need a little extra binding configuration:
@ -2744,16 +2734,6 @@ It's sometimes nice to be able to click a link in an Org file that takes me to o
** Appearance ** Appearance
*** Entities
#+begin_src emacs-lisp
(after! org
(setq org-pretty-entities t)
(pushnew! org-entities-user
'("top" "\\top" t "&top;" "" "22A4" "")
'("bot" "\\bot" t "&perp;" "" "22A5" "⊥")))
#+end_src
*** Modern *** 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=. 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=.
@ -2798,7 +2778,6 @@ Doom Emacs's =+pretty= flag by default uses the package =org-superstar= to prett
("email" . "󰇰") ("email" . "󰇰")
("date" . "󰃮") ("date" . "󰃮")
("property" . "󰆧") ("property" . "󰆧")
("startup" . "󰜉")
("filetags" . "󰓼") ("filetags" . "󰓼")
("bind" . "󰌷") ("bind" . "󰌷")
("bibliography" . "") ("bibliography" . "")
@ -2888,10 +2867,7 @@ Since we've disabled =+pretty=, we need to add the packages we do want from it,
:hook (org-mode . org-appear-mode) :hook (org-mode . org-appear-mode)
:config :config
(setq org-appear-autoentities t (setq org-appear-autoentities t
org-appear-autokeywords t org-appear-inside-latex t))
org-appear-autosubmarkers t
org-appear-inside-latex nil))
(after! org (after! org
(setq org-highlight-latex-and-related '(native script entities))) (setq org-highlight-latex-and-related '(native script entities)))
@ -3006,25 +2982,10 @@ My attention span being what it is, I often forget to update TODO entries in my
If nil, then use the current time.") If nil, then use the current time.")
;; HACK: Why does Emacs have so many independently defined time functions (defadvice! ~/org-override-time (old-fn)
(defadvice! ~/org-override-time (old-fn &rest args)
"Use `org-todo-time' as the current time if it is specified." "Use `org-todo-time' as the current time if it is specified."
:around (list #'org-current-time #'org-auto-repeat-maybe) :around #'org-current-time
(if org-todo-time (or org-todo-time (funcall old-fn)))
(cl-letf (((symbol-function #'current-time)
(lambda (&rest _) org-todo-time))
(float-time-old (symbol-function #'float-time))
((symbol-function #'float-time)
(lambda (&optional time) (funcall float-time-old (or time org-todo-time))))
(time-subtract-old (symbol-function #'time-subtract))
((symbol-function #'time-subtract)
(lambda (a b) (funcall time-subtract-old (or a org-todo-time) (or b org-todo-time))))
(decode-time-old (symbol-function #'decode-time))
((symbol-function #'decode-time)
(lambda (&optional time &rest args)
(apply decode-time-old (or time org-todo-time) args))))
(apply old-fn args))
(apply old-fn args)))
#+end_src #+end_src
We can then define and bind alternate versions of ~org-todo~ and ~org-agenda-todo~ that allow us to pick the time to set. We can then define and bind alternate versions of ~org-todo~ and ~org-agenda-todo~ that allow us to pick the time to set.
@ -3187,6 +3148,40 @@ If nil, then `default-directory' for the org buffer is used.")
(funcall orig-fn extension subtreep pub-dir)) (funcall orig-fn extension subtreep pub-dir))
#+end_src #+end_src
*** Attachment Inline Previews
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.
#+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
*** Inline Image Previews *** Inline Image Previews
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. 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.
@ -3198,8 +3193,9 @@ Inline images in Org are file links pointing to images without a description. Us
(defadvice! +org-inline-desc (old-fn protocol link description) (defadvice! +org-inline-desc (old-fn protocol link description)
"Disable inline images with descriptions when `+org-inline-image-desc' "Disable inline images with descriptions when `+org-inline-image-desc'
is non-nil." is non-nil."
:around (list #'+org-inline-image-data-fn :around '(org-yt-image-data-fun
#'+org-http-image-data-fn) +org-inline-image-data-fn
+org-http-image-data-fn)
(when (or +org-inline-image-desc (null description)) (when (or +org-inline-image-desc (null description))
(funcall old-fn protocol link description))) (funcall old-fn protocol link description)))
#+end_src #+end_src
@ -3331,17 +3327,19 @@ Annoyingly, the only good way to fix these issues is to completely override the
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. 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.
To fix this issue, it's... "easy" to patch Org-mode's category system. 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. To fix this issue, it's... "easy" 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.
#+begin_src emacs-lisp #+begin_src emacs-lisp
(defadvice! ~/org-default-category (old-fn) (defadvice! ~/org-default-category (old-fn)
"Modify how Org resolves the default category through the "Modify how Org resolves the default category through the
`org-category' variable." `org-category' variable."
:around #'org-element--get-category :around '(org-refresh-category-properties org-element-org-data-parser)
(let ((org-category (or org-category (org-get-title)))) (let ((org-category (or org-category (org-get-title))))
(funcall old-fn))) (funcall old-fn)))
#+end_src #+end_src
[fn:2] 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!
** Org Roam ** Org Roam
#+call: confpkg("Org: Roam") #+call: confpkg("Org: Roam")
@ -3357,23 +3355,21 @@ Org-roam is inspired by Roam Research, and like that tool it is based on the Zet
In my use of Org-roam for task management, I divide nodes into a few different categories: In my use of Org-roam for task management, I divide nodes into a few different categories:
1. *Areas*, which represent continual areas of your life to organize and plan; 1. *Areas*, which represent continual areas of your life to organize and plan;
2. *Goals*, short- or long-term that can be completed; 2. *Goals*, short- or long-term, things that can be completed;
3. *Tasks*, which are small, one-time and contribute to goals or areas. 3. *Tasks*, which are one-time and contribute to goals or areas.
Areas are stored as subnodes of the =Areas= file node, and likewise for goals. They also have the =:area:= and =:goal:= tags respectively. A task is a node that is a TODO entry that links to an area or a goal. We can thus check for if a node is a task by checking if the node links to a =:area:= or =:goal:= tagged node. Areas are stored as subnodes of the =Areas= file node, and likewise for goals. They also have the =:area:= and =:goal:= tags respectively. A task is a node that is a TODO entry that links to an area or a goal. We can thus check for if a node is a task by checking if the node links to a =:area:= or =:goal:= tagged node.
#+begin_src emacs-lisp #+begin_src emacs-lisp
(defun ~/org-roam-get-linked-nodes (node tag) (defun ~/org-roam-get-linked-nodes (node tag)
"Return the nodes that NODE links to that are tagged with TAG." "Return the nodes that NODE links to that are tagged with TAG."
(unless (stringp node)
(setq node (org-roam-node-id node)))
(let ((response (org-roam-db-query [:select :distinct [dest] (let ((response (org-roam-db-query [:select :distinct [dest]
:from links :from links
:inner-join tags :on (= dest node_id) :inner-join tags :on (= dest node_id)
:where (= source $s1) :where (= source $s1)
:and (= type "id") :and (= type "id")
:and (= tag $s2)] :and (= tag $s2)]
node tag))) (org-roam-node-id node) tag)))
(mapcar (lambda (id) (org-roam-populate (mapcar (lambda (id) (org-roam-populate
(org-roam-node-create :id (car id)))) (org-roam-node-create :id (car id))))
response))) response)))
@ -3453,8 +3449,7 @@ The goal is to hook into Org's capturing system to get it to feed its template i
:around #'org-capture-place-template :around #'org-capture-place-template
(let* ((template (org-capture-get :template)) (let* ((template (org-capture-get :template))
(org-capture-plist (plist-put org-capture-plist :template ""))) (org-capture-plist (plist-put org-capture-plist :template "")))
(letf! ((#'org-paste-subtree #'ignore) (cl-letf (((symbol-function 'org-kill-is-subtree-p) #'always))
(#'org-kill-is-subtree-p #'always))
(funcall old-fn arg) (funcall old-fn arg)
(yas-expand-snippet template)))) (yas-expand-snippet template))))
#+end_src #+end_src
@ -3465,7 +3460,7 @@ This advice overrides ~org-capture-place-template~, the function that:
2. Modifies the template to fit its context 2. Modifies the template to fit its context
3. Places the template in the file 3. Places the template in the file
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 can no longer perform point 2, but YASnippet is powerful enough that we can simply do that ourselves in the template. 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.
*** Roam Capture *** Roam Capture
@ -3489,22 +3484,17 @@ Since Org-roam is currently my primary method of using Org, I only use its templ
(read-directory-name "Directory: " org-roam-directory)))) (read-directory-name "Directory: " org-roam-directory))))
#+end_src #+end_src
I want there to only be one capture template when I'm making new links, so that I don't get distracted by a prompt. However, when I'm explicitly telling Roam to use node capture, it would be a shame if there was only one option. To fix this, I can split my capture templates into three contexts: a default template for when not explicitly capturing, a list of templates for capturing on a node that already exists, and the regular list for when the node is new. 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.
#+begin_src emacs-lisp #+begin_src emacs-lisp
(defvar org-roam-capture-default-template nil (defvar org-roam-capture-default-template nil
"The default capture template to use when not explicitly capturing.") "The default capture template to use when not explicitly capturing.")
(defvar org-roam-capture-existing-templates nil
"Capture templates to use when capturing on an existing node.")
(defvar org-roam-dailies-capture-default-template nil (defvar org-roam-dailies-capture-default-template nil
"The default daily capture template to use when not explicitly capturing.") "The default daily capture template to use when not explicitly capturing.")
#+end_src #+end_src
(We don't distinguish between new and existing dailies, since dailies are meant to follow a single format, which can just be covered by the default template.)
With those variables created, we can define our templates. With those variables created, we can define our templates.
#+begin_src emacs-lisp #+begin_src emacs-lisp
@ -3522,12 +3512,26 @@ With those variables created, we can define our templates.
"#+title: %<%Y-%m-%d>") "#+title: %<%Y-%m-%d>")
:unnarrowed t :unnarrowed t
:immediate-finish t)) :immediate-finish t))
;; new-file templates ;; Active templates
(setq org-roam-capture-templates (setq org-roam-capture-templates
'(("f" "Standalone file" plain "" '(("f" "Standalone file" plain ""
:target (file+head "${file-maybe-dir}" :target (file+head "${file-maybe-dir}"
"#+title: ${title}") "#+title: ${title}")
:unnarrowed t)) :unnarrowed t)
("n" "Citation Note" plain ""
:target (file+head "${file-maybe-cite}"
"#+title: ${note-title}")
:immediate-finish t
:unnarrowed t)
("h" "Heading" entry
"`(make-string (org-outline-level) ?*)`* ${title}
: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 org-roam-dailies-capture-templates
'(("e" "Event" entry "* $1\n%^t\n\n$0" '(("e" "Event" entry "* $1\n%^t\n\n$0"
:target (file+head "%<%Y-%m-%d>.org" :target (file+head "%<%Y-%m-%d>.org"
@ -3549,22 +3553,10 @@ Now we just have to advise Org-roam with the proper logic!
#+begin_src emacs-lisp #+begin_src emacs-lisp
(defadvice! ~/org-roam-default-capture (old-fn &rest args) (defadvice! ~/org-roam-default-capture (old-fn &rest args)
"Use default capture template when not explicitly capturing." "Use default capture template when not explicitly capturing."
:around #'org-roam-node-insert :around '(org-roam-node-find org-roam-node-insert)
(let ((org-roam-capture-templates (list org-roam-capture-default-template))) (let ((org-roam-capture-templates (list org-roam-capture-default-template)))
(apply old-fn args))) (apply old-fn args)))
(cl-defun ~/org-roam-capture (&optional goto keys &key filter-fn templates info)
(let ((node (org-roam-node-read nil filter-fn)))
(org-roam-capture- :goto goto
:info info
:keys keys
:templates (or templates
(when (org-roam-node-file node)
org-roam-capture-existing-templates))
:node node
:props '(:immediate-finish nil))))
(advice-add #'org-roam-capture :override #'~/org-roam-capture)
(defadvice! ~/org-roam-dailies-default-capture (old-fn time &optional goto keys) (defadvice! ~/org-roam-dailies-default-capture (old-fn time &optional goto keys)
"Use default capture template when not explicitly capturing." "Use default capture template when not explicitly capturing."
:around #'org-roam-dailies--capture :around #'org-roam-dailies--capture
@ -3584,26 +3576,16 @@ A full week-long agenda is usually too cluttered for me to read, so I'll narrow
#+begin_src emacs-lisp #+begin_src emacs-lisp
(after! org (after! org
(require 'org-roam-dailies) ; Load along with `org' to make this code work
(setq org-agenda-span 'day (setq org-agenda-span 'day
org-agenda-start-day nil org-agenda-start-day nil
org-agenda-start-on-weekday 1 ; 1 = Monday org-agenda-start-on-weekday 1 ; 1 = Monday
org-deadline-warning-days 7
org-agenda-sorting-strategy org-agenda-sorting-strategy
'((agenda time-up habit-down priority-down category-up) '((agenda time-up habit-down priority-down category-up)
(todo habit-down priority-down time-up category-up) (todo habit-down priority-down time-up category-up)
(tags habit-down priority-down time-up category-up) (tags habit-down priority-down time-up category-up)
(search category-up)) (search category-up))
;; Agenda files
org-agenda-files
(list org-directory
org-roam-directory
(f-join org-roam-directory "school/courses")
(f-join org-roam-directory org-roam-dailies-directory))
;; Make sure agenda is the only window ;; Make sure agenda is the only window
org-agenda-window-setup 'only-window org-agenda-window-setup 'only-window
org-agenda-restore-windows-after-quit t org-agenda-restore-windows-after-quit t
@ -3643,55 +3625,46 @@ By customizing ~org-super-agenda-groups~ via a let-binding in my custom agenda v
(defun ~/org-agenda-section-by-link (prefix tag item) (defun ~/org-agenda-section-by-link (prefix tag item)
"Org super-agenda function to categorize agenda entries by linked node with TAG." "Org super-agenda function to categorize agenda entries by linked node with TAG."
(when-let* ((marker (org-super-agenda--get-marker item)) (when-let* ((marker (org-super-agenda--get-marker item))
(nodes (org-super-agenda--when-with-marker-buffer marker (node (org-super-agenda--when-with-marker-buffer marker
(--keep (org-element-property :ID it) (org-roam-node-at-point)))
(org-element-lineage (org-element-context) nil t)))) (links (~/org-roam-get-linked-nodes node tag)))
(-compare-fn (lambda (a b)
(equal (org-roam-node-id a) (org-roam-node-id b))))
(links (-distinct (--mapcat (~/org-roam-get-linked-nodes it tag) nodes))))
(->> links (->> links
(mapcar #'org-roam-node-title) (mapcar #'org-roam-node-title)
(-interpose ", ") (-interpose ", ")
(apply #'concat prefix)))) (apply #'concat prefix))))
(defvar ~/org-todo-groups nil
"`org-super-agenda' groups for TODO entries of agenda.")
(after! org (after! org
(setq ~/org-todo-groups (setq org-agenda-custom-commands
'((:name "Next/In Progress"
:todo ("NEXT" "STRT" "WAIT" "HOLD"))
(:name "Important"
:priority "A"
:order 1)
(:name "Goals"
:tag "goal"
:order 9)
(:name "Notes to Intake"
:tag "notes"
:order 10)
(:name "Assignments"
:tag "assign"
:order 2)
(:auto-map (lambda (item)
(~/org-agenda-section-by-link
"Goal: " "goal" item))
:order 3)
(:auto-map (lambda (item)
(~/org-agenda-section-by-link
"Area: " "area" item))
:order 4))
org-agenda-custom-commands
'(("o" "Overview" '(("o" "Overview"
((agenda "") ((agenda "")
(alltodo "" ((org-super-agenda-groups (alltodo ""
(-insert-at 1 '(:discard ((org-super-agenda-groups
(:date t '((:name "Next/In Progress"
:deadline t :todo ("NEXT" "STRT" "WAIT" "HOLD"))
:scheduled t)) (:discard
~/org-todo-groups)))))) (:date t
("g" "Grouped list of TODO entries" :deadline t
alltodo "" ((org-super-agenda-groups ~/org-todo-groups)))))) :scheduled t))
(:name "Important"
:priority "A"
:order 1)
(:name "Goals"
:tag "goal"
:order 9)
(:name "Notes to Intake"
:tag "notes"
:order 10)
(:name "Assignments"
:tag "assign"
:order 2)
(:order-multi
(3
(:auto-map (lambda (item)
(~/org-agenda-section-by-link
"Goal: " "goal" item)))
(:auto-map (lambda (item)
(~/org-agenda-section-by-link
"Area: " "area" item))))))))))))))
#+end_src #+end_src
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. 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.
@ -3710,22 +3683,22 @@ This "overview" agenda command is very nice. It's so nice, in fact, that it's al
"o A" #'org-agenda) "o A" #'org-agenda)
#+end_src #+end_src
*** TODO Tweaks *** Agenda Files
When finding a node with ~org-roam-node-find~, the universal argument to open it in another window doesn't work when the node is new. We can fix this with an override: 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.
#+begin_src emacs-lisp #+begin_src emacs-lisp
(cl-defun ~/org-roam-node-find (&optional other-window initial-input filter-fn pred &key templates) (defun org-agenda-files-function ()
(let ((node (org-roam-node-read initial-input filter-fn pred))) (require 'find-lisp)
(if (org-roam-node-file node) (find-lisp-find-files org-directory "\.org$"))
(org-roam-node-visit node other-window)
(org-roam-capture- (defvar org-agenda-files-function #'org-agenda-files-function
:node node "The function to determine the org agenda files.")
:templates (or templates (list org-roam-capture-default-template))
:props (if other-window (defadvice! ~/org-agenda-files (&rest _)
'(:after-finalize find-file-other-window) "Set `org-agenda-files' before Org fetches them."
'(:finalize find-file)))))) :before #'org-agenda-files
(advice-add #'org-roam-node-find :override #'~/org-roam-node-find) (setq org-agenda-files (funcall org-agenda-files-function)))
#+end_src #+end_src
** Citations ** Citations
@ -3840,6 +3813,17 @@ To make opening the journal more convenient, here's a command to open the latest
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. 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.
** 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
** Dired ** Dired
#+call: confpkg("Mode: Dired") #+call: confpkg("Mode: Dired")
@ -3930,7 +3914,7 @@ The Doom module is very outdated, so I'll be overriding it.
*** Appearance *** Appearance
Operators being in italics looks ugly in this mode too, but 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. 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.
#+begin_src emacs-lisp #+begin_src emacs-lisp
(after! idris-mode (after! idris-mode
@ -3945,45 +3929,6 @@ Operators being in italics looks ugly in this mode too, but due to Idris's more
`(idris-semantic-postulate-face :foreground ,(doom-color 'yellow)))) `(idris-semantic-postulate-face :foreground ,(doom-color 'yellow))))
#+end_src #+end_src
** 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
** Java
The =lsp-java= package provides LSP support using the standard language server, the Eclipse-based ~jdtls~. Unfortunately, this package isn't designed for Nix, so it fails to find the server script in our Nix store. We need to do a bit of legwork to patch in this support, as well as some extra considerations, such as per-project config directories.
#+begin_src emacs-lisp
(defun +lsp-java-server-store-path ()
"Return the Nix store derivation path to the Java language server."
(string-remove-suffix
(concat "/bin/" lsp-java-jdt-ls-command)
(or
(executable-find lsp-java-jdt-ls-command)
(user-error "Could not find Java language server"))))
(after! java-mode
(setq lsp-java-jdt-ls-prefer-native-command t))
(add-hook! java-mode
(setq-local lsp-java-server-install-dir
(concat (+lsp-java-server-store-path) "/share/java/jdtls/")))
(defadvice! ~/lsp-java-ls-command ()
"Configure `lsp-java' to work with Nix."
:override #'lsp-java--ls-command
(let ((root (or (doom-project-root) (f-join (getenv "XDG_DATA_HOME") "jdtls"))))
(list lsp-java-jdt-ls-command
"-configuration" (f-join root "config-linux")
"-data" (f-join root "java-workspace"))))
#+end_src
** Yuck ** Yuck
#+call: confpkg("Mode: Yuck") #+call: confpkg("Mode: Yuck")
@ -4004,3 +3949,50 @@ This section is for code with little or no associated documentation. This could
2. Self-explanatory 2. Self-explanatory
3. Hard to categorize 3. Hard to categorize
4. Just not really worth the time it takes to write commentary 4. Just not really worth the time it takes to write commentary
** 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
`((,(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)))
(defun ~/org-generate-problem-list (&rest specs)
(interactive (s-split "," (read-from-minibuffer "Problems: ")))
(let* ((alist ~/org-problem-spec-alist)
(problems
(mapcan
(lambda (spec)
(let* ((match
(or (--some (-some-> (s-match (car it) spec)
cdr (cons (cdr it)))
alist)
(user-error "Invalid problem spec \"%s\"" spec))))
(apply (cdr match) (mapcar #'string-to-number (car match)))))
specs)))
(move-to-left-margin)
(dolist (num problems)
(insert (format "- [ ] %d\n" num)))))
#+end_src

View file

@ -12,6 +12,6 @@
}$4 }$4
}; };
outputs = { $5 }: outputs = { self$5, ... }:
$0 $0
} }

View file

@ -2,6 +2,6 @@
# key: begin # key: begin
# name: latex-begin # name: latex-begin
# -- # --
\begin{${1:displaymath}} \begin{${1:equation}*}
$0 $0
\end{$1} \end{$1*}