Skip to content

Doom Emacs Config

This is my personal config.el for Doom Emacs. It controls font settings, theme, Org-mode behavior, tangling and exporting, Copilot and AI tooling, and various Doom-specific enhancements.

We set lexical binding for the file.

;;; $DOOMDIR/config.el -*- lexical-binding: t; -*-

We begin by setting the primary font for Doom. I’m using Maple Mono at size 20, with increased spacing to give it a clean and open feel.

(setq doom-font (font-spec :family "Maple Mono" :size 20 :spacing 90))

The theme I use is catppuccin — soft, readable, and comfortable on the eyes.

(let ((eink (getenv "EINK_MODE")))
(if (string= eink "1")
(setq doom-theme 'doom-one-light)
(setq doom-theme 'catppuccin)))

I enable relative line numbers across the board, which helps with navigation.

(setq display-line-numbers-type 'relative)

My Org files live in the root of my repository — this ensures that tangling and exporting behave predictably from anywhere.

(setq org-directory "~/nickseagull.dev/")
(setq user-full-name "Nikita Tchayka")

Now we define a helper that determines whether an Org file contains any tangleable code blocks.

(defun ns/org-file-has-tangle-blocks-p (file)
"Return non-nil if FILE contains any Babel src block with a :tangle path or yes."
(with-temp-buffer
(insert-file-contents file)
(goto-char (point-min))
(re-search-forward "#\\+BEGIN_SRC.*:tangle\\s-+\\([^ \t\n]+\\)" nil t)))

We now define a function that automatically tangles an Org file when it is saved, but only if it includes tangleable blocks.

(defun ns/org-babel-auto-tangle ()
"Auto-tangle current Org file on save if it contains any tangleable blocks."
(when (and (buffer-file-name)
(string= (file-name-extension (buffer-file-name)) "org")
(ns/org-file-has-tangle-blocks-p (buffer-file-name)))
(org-babel-tangle)))

This behavior is attached to org-mode buffers only.

(after! org-mode
(add-hook! org-mode
(lambda ()
(add-hook! after-save-hook #'ns/org-babel-auto-tangle nil t))))

Now we define a recursive tangler for all Org files in a directory.

(defun ns/tangle-org-files-in-dir (directory)
"Recursively tangle Org files in DIRECTORY that contain Babel blocks with
:tangle."
(interactive "DDirectory: ")
(let ((org-files (directory-files-recursively directory "\\.org$")))
(dolist (file org-files)
(when (ns/org-file-has-tangle-blocks-p file)
(with-current-buffer (find-file-noselect file)
(org-babel-tangle))))))

To complement tangling, we now define a function that exports all Org files under a directory to Hugo-compatible Markdown files, preserving their relative structure.

(defun ns/hugo-export-org-files-in-dir (org-root-dir hugo-root-dir)
"Recursively export Org files under ORG-ROOT-DIR using ox-hugo.
Place the resulting .md files in HUGO-ROOT-DIR/content/docs/,
preserving the same subdirectory structure.
Skips killing the current buffer if it's one of the exported files."
(interactive "DOrg root directory: \nDHugo root directory: ")
(require 'ox-hugo)
(let ((org-files (directory-files-recursively org-root-dir "\\.org$"))
(content-root (expand-file-name "content/docs/" hugo-root-dir))
(current (current-buffer)))
(dolist (org-file org-files)
(let* ((relative-path (file-relative-name org-file org-root-dir))
(md-filename (concat (file-name-sans-extension relative-path) ".md"))
(final-md-path (expand-file-name md-filename content-root))
(target-dir (file-name-directory final-md-path)))
(make-directory target-dir t)
(with-current-buffer (find-file-noselect org-file)
(setq-local org-hugo-base-dir hugo-root-dir)
(org-hugo-export-wim-to-md)
(unless (eq (current-buffer) current)
(kill-buffer))))))
(message "Finished exporting Org files from %s to %s/content/docs"
(abbreviate-file-name org-root-dir)
(abbreviate-file-name hugo-root-dir)))

This next function ties it all together: tangle, then export everything. This is the one I call when building the site.

(defun ns/tangle-and-export-all-org-files ()
"Tangle and export all Org files in `org-directory` to Hugo markdown.
Hugo project is assumed to be at `org-directory/website/src/`."
(interactive)
(let* ((org-root-dir (expand-file-name org-directory))
(hugo-root-dir (expand-file-name ".webgen/src/" org-root-dir)))
(ns/tangle-org-files-in-dir org-root-dir)
(ns/hugo-export-org-files-in-dir org-root-dir hugo-root-dir)))

We also hook this function to saving files, but only if they belong to the Org source tree.

(defun ns/auto-tangle-and-export-on-save ()
"Auto tangle and export if the saved Org file is under `org-directory`."
(when (and (eq major-mode 'org-mode)
(string-prefix-p (expand-file-name org-directory)
(buffer-file-name)))
(run-with-idle-timer 0.5 nil #'ns/tangle-and-export-all-org-files)))
(add-hook 'after-save-hook #'ns/auto-tangle-and-export-on-save)

I use GitHub Copilot for completions, and fall back to Company if needed.

(use-package! copilot
:hook (prog-mode . copilot-mode)
:bind (:map copilot-completion-map
("<tab>" . 'copilot-accept-completion)
("TAB" . 'copilot-accept-completion)
("C-TAB" . 'copilot-accept-completion-by-word)
("C-<tab>" . 'copilot-accept-completion-by-word)))

I also reposition Treemacs to the right of the screen.

(after! treemacs
(setq treemacs-position 'right))

The aidermacs package gives me AI-driven tools and workflows. I bind its main menu to C-c a.

(use-package! aidermacs
:bind
(("C-c a" . aidermacs-transient-menu))
:custom
(aidermacs-use-architect-mode t)
(aidermacs-default-model "gpt-4o"))

I keep using GitHub Copilot:

(use-package! copilot
:hook (prog-mode . copilot-mode)
:bind (:map copilot-completion-map
("<tab>" . 'copilot-accept-completion)
("TAB" . 'copilot-accept-completion)
("C-TAB" . 'copilot-accept-completion-by-word)
("C-<tab>" . 'copilot-accept-completion-by-word)
("C-n" . 'copilot-next-completion)
("C-p" . 'copilot-previous-completion))
:config
(setq copilot-max-char -1) ;; sometimes it gives me warnings about the fact that the source files I edit are too large. Just removing the limit
;; I get a bunch of warnings regarding indentation in Go, so I just make it explicit
(add-to-list 'copilot-indentation-alist '(prog-mode 2))
(add-to-list 'copilot-indentation-alist '(org-mode 2))
(add-to-list 'copilot-indentation-alist '(text-mode 2))
(add-to-list 'copilot-indentation-alist '(closure-mode 2))
(add-to-list 'copilot-indentation-alist '(go-mode 4))
(add-to-list 'copilot-indentation-alist '(emacs-lisp-mode 2)))

For Go development, I bind the DAP (debugging) hydra to SPC m D.

(add-hook! go-mode
(map! :localleader
:map go-mode-map
"D" #'dap-hydra))

Also, I prefer customizing the Go compilation command to this one:

(after! projectile
(projectile-register-project-type 'go '("go.mod")
:compile "go build -gcflags='-e' ./..."
:test "go test ./pkg/..."
:run "go run main.go"))

Lastly, I enable clipetty so that Emacs can use the system clipboard over SSH.

(setq dap-auto-configure-features '(sessions locals controls tooltip))
(use-package! clipetty
:hook (after-init . global-clipetty-mode))