#+TITLE: Emacs Configuration #+TODO: TODO FIXME | DONE FIXED * Setup ** Installing Emacs Because I wanted to play with some of the latest goodies like ~project.el~ and ~tab-bar.el~ I ended up having to compile Emacs from source to get the latest. *Getting the Source* Emacs is an old project and the Git repo is suitably large, since I'm only after a particular branch we can ask Git to fetch just that branch. #+begin_src sh git clone --depth 1 --branch emacs-27 https://git.savannah.gnu.org/git/emacs.git #+end_src *Build Dependencies* After enabling ~deb-src~ repos in Ubuntu getting all the build dependencies for Emacs was easy enough. I also made sure ~libjansson~ was available so that I could get native JSON parsing - useful if I ever setup ~lsp-mode~ or ~eglot~ #+begin_src sh sudo apt build-dep emacs sudo apt install libjansson libjansson-dev #+end_src *Building* With the prerequisites taken care of building and installing is the familiar ~configure~, ~make~, ~make install~ process. #+begin_src sh ./configure make -j 4 make install #+end_src ** Config Start My ~init.el~ file starts here #+begin_src emacs-lisp :tangle init.el ;;; init.el --- Emacs Configuration -*- lexical-binding: t -*- (defconst emacs-start-time (current-time)) (defconst mmdc-path "/home/alex/.npm-packages/bin/mmdc") #+end_src ** Packaging Setting up the package archives #+begin_src emacs-lisp :tangle init.el (require 'package) (setq package-archives '(("melpa" . "https://melpa.org/packages/") ("org" . "https://orgmode.org/elpa/") ("elpa" . "https://elpa.gnu.org/packages/"))) #+end_src On first boot of a new install we of course are going to want to install a bunch of packages so it makes sense to ensure we have an up to date archive #+begin_src emacs-lisp :tangle init.el (package-initialize) (unless package-archive-contents (package-refresh-contents)) #+end_src ** Use Package I use [[https://github.com/jwiegley/use-package][use-package]] heavily in my configuration, let's make sure it's available. #+begin_src emacs-lisp :tangle init.el (unless (package-installed-p 'use-package) (package-install 'use-package)) #+end_src * Misc Settings ** Backups Don't clutter directories with autosaves and backups... #+begin_src emacs-lisp :tangle init.el (setq auto-save-default nil create-lockfiles nil make-backup-files nil) #+end_src Most of what I work with is in Git so I'd rather if buffers just kept sync with what's on disk #+begin_src emacs-lisp :tangle init.el (global-auto-revert-mode 1) #+end_src ** Encoding #+begin_src emacs-lisp :tangle init.el (set-language-environment "UTF-8") (set-default-coding-systems 'utf-8) #+end_src ** Long Lines By default, truncate long lines #+begin_src emacs-lisp :tangle init.el (setq-default truncate-lines t) #+end_src ** Whitespace #+begin_src emacs-lisp :tangle init.el (use-package whitespace :init (setq sentence-end-double-space nil whitespace-style '(face empty trailing)) (setq-default indent-tabs-mode nil) (global-whitespace-mode) (add-hook 'before-save-hook #'whitespace-cleanup)) #+end_src * UI Disable a bunch of UI elements #+begin_src emacs-lisp :tangle init.el (scroll-bar-mode -1) (tool-bar-mode -1) (menu-bar-mode -1) (blink-cursor-mode -1) (setq inhibit-startup-message t) #+end_src ** Buffers *** IBuffer #+begin_src emacs-lisp :tangle init.el :noweb yes (use-package ibuffer :bind (("C-x C-b" . ibuffer)) :config <>) #+end_src **** Window Placement I like ~ibuffer~ to open itself in a bottom side window, like many of the "additional" utilities I put down there #+begin_src emacs-lisp :noweb-ref window-placement-rules ,(me/display-buffer-in-panel "\\*Ibuffer\\*") #+end_src In order for ~ibuffer~ to call [[help:display-buffer][display-buffer]] and thus have the rules defined in [[help:display-buffer-alist][display-buffer-alist]] take effect we need to ensure that ~ibuffer~ opens itself in an "other window" #+begin_src emacs-lisp :noweb-ref ibuffer-config (setq ibuffer-use-other-window t) #+end_src ** Completion *** IComplete #+begin_src emacs-lisp :tangle init.el (use-package icomplete :init (fido-mode 1)) #+end_src ** Fonts I quite like the Ubuntu family of fonts #+begin_src emacs-lisp :tangle init.el (set-face-attribute 'default nil :font "Ubuntu Mono" :height 125) (set-face-attribute 'fixed-pitch nil :font "Ubuntu Mono" :height 125) (set-face-attribute 'variable-pitch nil :font "Ubuntu Light" :height 125) #+end_src ** Icons Why not? 😃 #+begin_src emacs-lisp :tangle init.el (use-package all-the-icons :ensure t) #+end_src *Dired* #+begin_src emacs-lisp (use-package all-the-icons-dired :ensure t :hook (dired-mode . all-the-icons-dired-mode)) #+end_src ** Line Numbers I want Emacs by default to enable line numbers in buffers #+begin_src emacs-lisp :tangle init.el (global-display-line-numbers-mode 1) #+end_src Unless I specify a particular mode in which to disable them #+begin_src emacs-lisp :tangle init.el (dolist (hook '(doc-view-mode-hook eshell-mode-hook gfm-mode-hook org-mode-hook shell-mode-hook term-mode-hook)) (add-hook hook (lambda () (display-line-numbers-mode 0)))) #+end_src ** Modeline #+begin_src emacs-lisp :tangle init.el (use-package doom-modeline :ensure t :init (doom-modeline-mode 1) :config (column-number-mode 1) (size-indication-mode 1) (setq doom-modeline-buffer-file-name-style 'relative-to-project doom-modeline-buffer-modification-icon t doom-modeline-buffer-state-icon t doom-modeline-height 25 doom-modeline-major-mode-icon t doom-modeline-major-mode-color-icon nil doom-modeline-minor-modes nil)) #+end_src ** Tab Bar Emacs 27 comes with vim style tabs (i.e. a tab holds a collection of windows in some layout ) via ~tab-bar.el~. While I want to use them, I'd rather not see the tabs themselves rendered #+begin_src emacs-lisp :tangle init.el (use-package tab-bar :config (setq tab-bar-show nil)) #+end_src ** Theme #+begin_src emacs-lisp :tangle init.el (use-package modus-themes :ensure t :bind ("" . modus-themes-toggle) :init (setq modus-themes-diffs 'desaturated modus-themes-headings '((t . rainbow-section-no-bold)) modus-themes-intense-hl-line t modus-themes-lang-checkers 'straight-underline modus-themes-links 'faint-neutral-underline modus-themes-org-blocks 'grayscale modus-themes-paren-match 'intense-bold modus-themes-region 'bg-only-no-extend modus-themes-scale-headings t modus-themes-slanted-constructs t) ;; Default to the light theme (modus-themes-load-operandi) (show-paren-mode 1)) #+end_src ** Windows #+begin_src emacs-lisp :noweb yes :tangle init.el <> (use-package window :init <> <> :bind (("" . window-toggle-side-windows))) #+end_src *** Placement After using Emacs for any length of time, you'll quickly find that new windows pop open all the time in various locations as you call different commands. After finding [[https://www.youtube.com/watch?v=rjOhJMbA-q0][this video]] on the [[help:display-buffer-alist][display-buffer-alist]] variable, it turns out Emacs offers a very rich framework for controlling what windows get opened where - I should have guessed! #+begin_src emacs-lisp :noweb-ref window-placement :noweb yes (setq display-buffer-alist `(,(me/display-buffer-in-panel "\\*Help\\*") ,(me/display-buffer-in-panel "\\*Messages\\*") ,(me/display-buffer-in-panel "\\*\\(e?shell\\)\\*") ("\\*Process List\\*" ;; Setting no-other-window etc seems to break C-x C-c (display-buffer-in-side-window) ;; Error => Wrong type argument window-live-p, nil... (side . bottom) (slot . 0) (window-height . 0.25)) ,(me/display-buffer-in-panel (me/buffer-select-by-major-mode 'compilation-mode)) ,(me/display-buffer-in-top-window "\\*Completions\\*") <> )) #+end_src I'd rather have the left/right side windows take the full height of the frame, thankfully [[help:window-sides-vertical][window-sides-vertical]] is just the option I'm looking for #+begin_src emacs-lisp :noweb-ref window-config (setq window-sides-vertical t) #+end_src **** Select By Major Mode The following function handles selecting buffers based on their major mode. **Requires lexical-binding** #+begin_src emacs-lisp :noweb-ref window-functions (defun me/buffer-select-by-major-mode (mode) "A filter for use with `display-buffer-alist', will select a buffer if it matches the given major-mode" (lambda (buffer action) (with-current-buffer buffer (eq major-mode mode)))) #+end_src **** Display in Panel The following action is essentially the same as [[help:display-buffer-in-side-window][display-buffer-in-side-window]] but additionally enables [[help:tab-line-mode][tab-line-mode]] for that buffer. #+begin_src emacs-lisp :noweb-ref window-functions (defun me/display-tabbed-buffer-in-side-window (buffer alist) "See `display-buffer-in-side-window'" (display-buffer-in-side-window buffer alist) (with-current-buffer buffer (tab-line-mode))) #+end_src The following then makes use of the above action to place matching buffers in a "panel" similar to how VSCode does things #+begin_src emacs-lisp :noweb-ref window-functions (defun me/display-buffer-in-panel (predicate) "Display buffers matching the given PREDICATE in the panel. To borrow terminology from VSCode the panel is that collapsable window at the bottom of the screen. " `(,predicate (me/display-tabbed-buffer-in-side-window) (window-height . 0.25) (side . bottom) (slot . 0) (window-parameters . ((no-other-window . t) (no-delete-other-windows . t))))) #+end_src ***** FIXME Workaround Unfortunately for some reason the call to ~tab-line-mode~ does not take effect for shells and I'm not sure why yet. So let's also invoke it via the mode hooks #+begin_src emacs-lisp :noweb-ref window-functions (dolist (hook '(shell-mode-hook eshell-mode-hook)) (add-hook hook (lambda () (tab-line-mode)))) #+end_src **** Display In Top Window I quite like having any temporary buffers like =*Completions*= pop up in a top side window and then vanish again. #+begin_src emacs-lisp :noweb-ref window-functions (defun me/display-buffer-in-top-window (predicate) "Display buffers matching the given PREDICATE in a top side window." `(,predicate (display-buffer-in-side-window) (window-height . 0.2) (side . top) (slot . 0) (window-parameters . ((no-other-window . t))))) #+end_src * Programs ** Dired #+begin_src emacs-lisp :tangle init.el :noweb yes <> (use-package dired :bind (("C-x d" . me/dired-open-directory)) :hook (dired-mode . me/dired-mode-tweaks)) #+end_src *** Open dired directory A quick command that opens the ~default-directory~ in ~dired~ #+begin_src emacs-lisp :noweb-ref dired-functions (defun me/dired-open-directory () (interactive) (dired default-directory)) #+end_src *** Tweaks A collection of tweaks to apply to new ~dired~ buffers #+begin_src emacs-lisp :noweb-ref dired-functions (defun me/dired-mode-tweaks () (dired-hide-details-mode)) #+end_src *** Window Placement Ensure that ~dired~ buffers open in a side window #+begin_src emacs-lisp :noweb-ref window-placement-rules (,(me/buffer-select-by-major-mode 'dired-mode) (display-buffer-in-side-window) (window-width . 0.15) (side . left) (slot . 0) (window-parameters . ((no-other-window . t)))) #+end_src ** Elfeed [[https://github.com/skeeto/elfeed][elfeed]] is an RSS feed reader for Emacs. #+begin_src emacs-lisp :tangle init.el :noweb yes <> (use-package elfeed :bind (("C-c e" . me/elfeed-start) :map elfeed-search-mode-map ("g" . me/elfeed-update) :map elfeed-show-mode-map ("q" . delete-window)) :ensure t :config (setq elfeed-use-curl t) <>) #+end_src *** Startup Custom startup function to ensure that elfeed runs in a dedicated tab. #+begin_src emacs-lisp :noweb-ref elfeed-functions (defun me/elfeed-start-new-tab () "Does the work of creating a new tab" (tab-bar-new-tab) (tab-bar-rename-tab "elfeed") (elfeed)) (defun me/elfeed-start () "Switch to the elfeed tab, create one if it doesn't exist." (interactive) (let ((tabs (mapcar (lambda (tab) (alist-get 'name tab)) (tab-bar-tabs)))) (if (member "elfeed" tabs) (tab-bar-select-tab-by-name "elfeed") (me/elfeed-start-new-tab)))) #+end_src *** Feeds I know that there's the [[https://github.com/remyhonig/elfeed-org][elfeed-org]] package that lets you configure elfeed feeds through an org file, but I thought it would be a good exercise in Elisp to see if I could put something similar together. Rather than using headlines I've thrown all the links and corresponding tags into a table and wrote a few functions that will convert the table into the format required by [[help:elfeed-feeds][elfeed-feeds]] and set the variable to the result. #+NAME: elfeed-feed-table | URL | Tags | |------------------------------------------------------------------------------+-------------| | https://pointieststick.com/feed/ | linux kde | | https://blog.gtk.org/rss | linux gtk | | https://blogs.gnome.org/shell-dev/rss | linux gnome | | https://sachachua.com/blog/category/emacs/feed/ | emacs | | https://www.youtube.com/feeds/videos.xml?channel_id=UC5Qw7uMu8_QMJHBw3mQJ62w | linux yt | #+NAME:feeds #+begin_src emacs-lisp :noweb-ref elfeed-functions (defun me/feed-table-row-to-item (row) "Convert a ROW from a feed table into a valid elfeed entry" (let* ((url (car row)) (tags (split-string (cadr row) " "))) (append (list url) (mapcar 'make-symbol tags)))) (defun me/feed-table-to-list (feed-table) "Convert an orgmode FEED-TABLE to a list compatible with elfeed." (mapcar 'me/feed-table-row-to-item feed-table)) (defun me/feed-table-extract () "Find the table called `elfeed-feed-table' in my config file and use it to set the `elfeed-feeds' variable." (with-temp-buffer (insert-file-contents "/home/alex/.emacs.d/README.org") (org-table-map-tables (lambda () (setq tbl-name (plist-get (cadr (org-element-at-point)) :name)) (if (string= tbl-name "elfeed-feed-table") (let ((tbl (cdr (cdr (org-table-to-lisp))))) (setq elfeed-feeds (me/feed-table-to-list tbl))))) t))) (defun me/elfeed-update (arg) "Refresh all RSS feeds. This simply calls `elfeed-update' unless the prefix arg is set or `elfeed-feeds' is nil in which case it will call `me/feed-table-extract' beforehand." (interactive "P") (if (or (not (null arg)) (null elfeed-feeds)) (me/feed-table-extract)) (elfeed-update)) #+end_src *** Window Placement Open article buffers below the main summary list. #+begin_src emacs-lisp :noweb-ref window-placement-rules ("\\*elfeed-entry\\*" (display-buffer-reuse-mode-window display-buffer-at-bottom) (window-height . 0.8)) #+end_src In order for this setting to work as expected though we need to tweak how elfeed manages its buffers #+begin_src emacs-lisp :noweb-ref elfeed-config (setq elfeed-show-entry-switch 'pop-to-buffer) (setq elfeed-show-entry-delete 'delete-window) #+end_src ** Git *** Git Gutter #+begin_src emacs-lisp :tangle init.el (use-package git-gutter :config (global-git-gutter-mode 1) (set-face-foreground 'git-gutter:added "forest green") (set-face-foreground 'git-gutter:modified "goldenrod") (set-face-foreground 'git-gutter:deleted "brown") (setq git-gutter:added-sign "▐" git-gutter:modified-sign "▐" git-gutter:removed-sign "▐")) #+end_src *** Magit #+begin_src emacs-lisp :tangle init.el (use-package magit :bind (("C-x g" . magit-status))) #+end_src ** LSP Mode #+begin_src emacs-lisp :tangle init.el (use-package lsp-mode :ensure t :init (setq lsp-keymap-prefix "C-c l") :hook ((python-mode . lsp)) :commands lsp) #+end_src ** Org Mode #+begin_src emacs-lisp :tangle init.el :noweb yes <> <> (use-package org :hook (org-mode . me/org-mode-tweaks) :bind (("C-c a" . org-agenda) ("C-c c" . org-capture)) :config (setq org-directory "~/Documents/org/") <>) #+end_src *** Org Agenda #+begin_src emacs-lisp :noweb-ref org-config (setq org-agenda-files (list org-directory)) #+end_src *** Org Babel In order to execute code in source blocks we need to ensure that ~org-babel~ has loaded support for it #+begin_src emacs-lisp :noweb-ref org-config (org-babel-do-load-languages 'org-babel-load-languages '((emacs-lisp . t) (python . t) (shell . t))) #+end_src *** Org Capture #+begin_src emacs-lisp :noweb-ref org-config (setq org-capture-templates '(("t" "Task" entry (file+headline "life.org" "Events") "* TODO %?\n") ("e" "Event" entry (file+headline "life.org" "Events") "* %?\nSCHEDULED: %^t") ("j" "Journal" entry (file+headline "life.org" "Journal") "* %u\n%?\n\n** Exercise\n" :prepend t))) #+end_src *** TODOs and Habits #+begin_src emacs-lisp :noweb-ref org-config (add-to-list 'org-modules 'org-habit t) (setq org-adapt-indentation 'headline-data org-habit-show-all-today t org-log-into-drawer t) #+end_src *** Tweaks A collection of tweaks to apply when opening a new org file #+begin_src emacs-lisp :noweb-ref org-functions (defun me/org-mode-tweaks () (setq-local fill-column 80) (turn-on-auto-fill) (flyspell-mode) (variable-pitch-mode 1) ;; Switch certain elements back to fixed pitch (set-face-attribute 'org-block nil :foreground nil :inherit 'fixed-pitch) (set-face-attribute 'org-link nil :inherit '(button fixed-pitch)) (set-face-attribute 'org-code nil :inherit '(shadow fixed-pitch)) (set-face-attribute 'org-table nil :inherit '(shadow fixed-pitch)) (set-face-attribute 'org-verbatim nil :inherit '(shadow fixed-pitch)) (set-face-attribute 'org-special-keyword nil :inherit '(font-lock-comment-face fixed-pitch)) (set-face-attribute 'org-meta-line nil :inherit '(font-lock-comment-face fixed-pitch)) (set-face-attribute 'org-checkbox nil :inherit 'fixed-pitch)) #+end_src ** Project Management #+begin_src emacs-lisp :tangle init.el :noweb yes <> <> (use-package project :bind (("C-x p f" . project-find-file) ("C-x p s" . me/project-search))) #+end_src *** Project -wide Search #+begin_src emacs-lisp :noweb-ref project-functions (defun me/project-search () "Execute a project wide search with ripgrep." (interactive) (let ((dir (cdr (project-current t))) (query (read-string "Search query: "))) (rg query "*" dir))) #+end_src **** TODO Check for a prefix argument and prompt for the filename pattern to search on? *** Additional Packages [[https://github.com/dajva/rg.el][rg]] is an Emacs frontend to [[https://github.com/BurntSushi/ripgrep][ripgrep]]. #+begin_src emacs-lisp :noweb-ref project-packages (use-package rg :ensure t) #+end_src * Programming ** C #+begin_src emacs-lisp :noweb yes :tangle init.el <> (use-package cc-mode :bind (:map c-mode-map ("C-c d" . me/start-debugging) ("C-c g" . recompile)) :config (setq-default c-basic-offset 4) (setq compilation-scroll-output t)) #+end_src *** Start Debugging Making use of ~tab-bar.el~ here is a custom function that starts a debugging session by first opening a new tab. This allows for the use of ~gdb-many-windows~ without messing with the current window layout. #+begin_src emacs-lisp :noweb-ref c-functions (defun me/start-debugging () (interactive) (let ((program (read-string "Debug program: "))) (tab-new) (setq gdb-many-windows t) (gdb (format "gdb -i=mi %s" program)))) #+end_src *** Stop Debugging This complements the function above, by listening for the end of the debugging session and closing the tab. I don't really understand how this works, but I adapted it from [[https://www.doof.me.uk/2019/06/09/making-emacs-gud-usable/][this blogpost]] #+begin_src emacs-lisp :noweb-ref c-functions (advice-add 'gud-sentinel :after (lambda (proc msg) (when (memq (process-status proc) '(signal exit)) (tab-close)))) #+end_src ** Python #+begin_src emacs-lisp :noweb yes :tangle init.el <> <> (use-package python :bind (:map python-mode-map ("C-c C-p" . me/python-open-repl) ("C-c g" . recompile)) :hook (python-mode . me/python-mode-tweaks) :config (setq compilation-scroll-output t) <>) #+end_src *** Tweaks Tweaks to apply when opening Python files #+begin_src emacs-lisp :noweb-ref python-functions (defun me/python-mode-tweaks () (setq-local fill-column 88)) #+end_src *** Opening a Python REPL The builtin [[help:python-mode][python-mode]] has a [[help:run-python][run-python]] command that will launch a Python REPL that we can interact with. Unfortunately by default it will just try runnning your system python - not very useful. Instead I have written a function that builds on ~project.el~ that will attempt to find the project's virtualenv and run a Jupyter REPL, falling back to Python if it is not installed. #+begin_src emacs-lisp :noweb-ref python-functions (defun me/python-open-repl () "Open a Python REPL in the correct virtualenv for the project. This will first look for Jupyter and will fall back to Python if it's not installed. It will attempt to find the root folder for the current package and open the shell there. " (interactive) (let* ((dir (cdr (project-current t))) (paths (list (concat dir ".env/bin/jupyter") (concat dir ".env/bin/python"))) (path (car (seq-filter 'file-exists-p paths)))) (setq python-shell-interpreter path python-shell-prompt-detect-failure-warning nil) (if (string-match-p (regexp-quote "jupyter") path) (setq python-shell-interpreter-args "console --simple-prompt") (setq python-shell-interpreter-args "-i")) (let* ((package-dir (me/python-find-setup-py (buffer-file-name (current-buffer)))) (default-directory (file-name-directory package-dir))) (run-python)))) #+end_src So that we get to use Jupyer's tab completion, we just need to tell Emacs to not use its own. #+begin_src emacs-lisp :noweb-ref python-config (add-to-list 'python-shell-completion-native-disabled-interpreters "jupyter") #+end_src Ensure that the Python REPL opens in the "Panel" #+begin_src emacs-lisp :noweb-ref window-placement-rules ,(me/display-buffer-in-panel (me/buffer-select-by-major-mode 'inferior-python-mode)) #+end_src *** Flake8 I want to run ~flake8~ on the current package every time I save a file. The method I'm currently using relies on finding the ~setup.py~ file for the current package and using [[help:compilation-start][compilation-start]] to run ~flake8~ in a compilation buffer. #+begin_src emacs-lisp :noweb-ref python-functions (defun me/python-flake8-project () "Run flake8 on the current project in a compilation buffer. This function will attempt to find the setup.py file for the package currently being edited using `me/python-find-setup-py'. If a filepath is found, its parent directory is assumed to be the package root and flake8 will be run in a compilation buffer via `compilation-start'." (interactive) (let* ((filename (buffer-file-name (current-buffer))) (setup-py (me/python-find-setup-py filename))) (unless (null setup-py) (let ((default-directory (file-name-directory setup-py))) (compilation-start "flake8" nil (lambda (_modename) (format "%s: flake8" default-directory))))))) #+end_src The following methods handle the recursing up the directory tree to find the ~setup.py~ #+begin_src emacs-lisp :noweb-ref python-functions (defun me/python-find-setup-py--from-dir (dir) (if (string= dir "/") nil (let* ((setup-py (concat dir "setup.py"))) (if (file-exists-p setup-py) setup-py (me/python-find-setup-py (file-name-directory (directory-file-name dir))))))) (defun me/python-find-setup-py (filename) "Find the setup.py file that corresponds with the package that contains FILENAME" (me/python-find-setup-py--from-dir (file-name-directory filename))) #+end_src *** Additional Packages [[https://github.com/pythonic-emacs/blacken][blacken]] will automatcially apply [[https://github.com/psf/black][black]] to the buffer on save. #+begin_src emacs-lisp :noweb-ref python-packages (use-package blacken :ensure t :hook (python-mode . blacken-mode)) #+end_src This will of course require the ~black~ command to be available, easiest way to do this is to install it via [[https://github.com/pipxproject/pipx][pipx]] #+begin_src sh pipx install black #+end_src ** Mermaid [[https://mermaid-js.github.io/mermaid/#/][Mermaid Diagrams]] #+begin_src emacs-lisp :tangle init.el (use-package mermaid-mode :ensure t :config (setq mermaid-mmdc-location mmdc-path)) #+end_src *** ob-mermaid [[https://github.com/arnm/ob-mermaid][ob-mermaid]] allows for mermaid diagrams to be used within Org Mode files #+begin_src emacs-lisp :noweb-ref org-packages (use-package ob-mermaid :ensure t :config (setq ob-mermaid-cli-path mmdc-path)) #+end_src * Prose ** Markdown #+begin_src emacs-lisp :tangle init.el :noweb yes <> (use-package markdown-mode :ensure t :hook (gfm-mode . me/gfm-mode-tweaks) :mode (("\\.md\\'" . gfm-mode) ("\\.markdown\\'" . gfm-mode))) #+end_src *** Tweaks A collection of tweaks to apply when opening a new markdown file #+begin_src emacs-lisp :noweb-ref markdown-functions (defun me/gfm-mode-tweaks () (setq-local fill-column 80) (turn-on-auto-fill) (flyspell-mode) (variable-pitch-mode 1) ;; Switch certain elements back to fixed pitch (set-face-attribute 'markdown-metadata-key-face nil :inherit 'fixed-pitch) (set-face-attribute 'markdown-metadata-value-face nil :inherit 'fixed-pitch)) #+end_src * Finishing Up ** Custom Ensure that anything set through ~custom~ is saved to a separate file #+begin_src emacs-lisp :tangle init.el (setq custom-file "~/.emacs.d/custom.el") (load custom-file 'noerror) #+end_src ** Startup Time Add a log message that gives us an indication on how long it took to load the config. #+begin_src emacs-lisp :tangle init.el (let ((startup-time (float-time (time-subtract (current-time) emacs-start-time)))) (message "Loaded configuration in %.3fs" startup-time)) #+end_src ** Auto Tangling The following ~Local Variables~ block sets up an on save hook that automatically tangles this file so that ~init.el~ is always in sync with the latest. # Local Variables: # eval: (add-hook 'after-save-hook (lambda () (org-babel-tangle)) nil t) # End: