sewh / emacs-literate-config

2 stars 0 forks source link

+TITLE: emacs configuration

+AUTHOR: sewh

Note: to navigate this file on GitHub use the navigator on the top left.

This document is written in [[https://orgmode.org/][org-mode]], a very powerful package for Emacs that many regard as its 'killer feature'. At its core, =org-mode= is a markup language like markdown. However it has deep integration into Emacs that makes it much more powerful. One very cool features of =org-mode= is that it lets you embed source code blocks in documents and then evaluate them. This lets us write a document with Emacs lisp in it and then load it as our configuration file. This is called a [[https://harryrschwartz.com/2016/02/15/switching-to-a-literate-emacs-configuration][literate Emacs config]].

You need to download this file, =configuration.org=, the =init.el= file and put them both into =~/.emacs.d/=. If you already have an =init.el= then just make sure that this line is in there somewhere:

+begin_src

(org-babel-load-file (concat user-emacs-directory "configuration.org"))

+end_src

This will make sure this config is loaded at init time.

The first thing we're going to add is a command to reload the config file to save some typing:

+begin_src emacs-lisp

(defun reload-config () (interactive) (org-babel-load-file (concat user-emacs-directory "configuration.org")))

+end_src

Reloading the config can now be done with =M-x reload-config=.

Now for editing the config. I always get tired of having to =C-x C-f ~/.emacs.d/configuration.org~= so let's create a little function to solve that.

+begin_src emacs-lisp

(defun edit-config () (interactive) (find-file (concat user-emacs-directory "configuration.org")))

+end_src

I thought I'd write a few words in case anyone reading isn't living the Emacs dream and wonders why in the age of Vim, VSCode, IntelliJ, etc, we would even consider using Emacs.

In a word: trust. Trust that Emacs is unlikely to contain any user-hostile functionality and trust that Emacs will be actively maintained for the foreseeable future, so time invested in learning Emacs is unlikely to be wasted.

The first point is very important. Emacs is developed by volunteers and sponsored by the GNU project. It is Free Software and the source code is available. There is nothing up the developer's sleeve. Compare this to VSCode:

Not great!

The second point is about software survivability. Yes Emacs is a little bit of a learning curve but it will very likely last you your career. The initial release of GNU Emacs was 1976, almost 50 years ago. The idea of learning a tool once and being able to depend on it indefinitely is very appealing.

So trust is a big factor. But trust alone is not the only reason to use Emacs. Emacs has a massive and thriving ecosystem of packages that not only add support for new languages, but can also add completely new features to the editor itself. Many of them are best in class, like =org-mode= and Magit. It's also very easy to write a small bit of code and support some bespoke bit of your workflow. There is no other editor as powerful as Emacs.

** Package Repositories

The default package repository is a little bit bare, and most interesting packages are hosted in [[https://melpa.org/][MELPA]]. We add MELPA as an available package repository:

+begin_src emacs-lisp

(require 'package) (add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t) (package-initialize)

+end_src

** Installing use-package

=use-package= is a nice little package to help install and configure Emacs packages. If it's not installed, we refresh our package cache and install it:

+begin_src emacs-lisp

(unless (require 'use-package nil 'noerror) (package-refresh-contents) (package-install 'use-package))

+end_src

** Updating Packages

Emacs doesn't automatically upgrade packages, but luckily there is a package that will do that for us.

+begin_src emacs-lisp

(use-package auto-package-update :ensure t)

+end_src

~chomp~ deletes whitespace at the start and end of a string.

+begin_src emacs-lisp

(defun chomp (str) "Chomp leading and tailing whitespace from STR." (while (string-match "\`\n+\|^\s-+\|\s-+$\|\n+\'" str) (setq str (replace-match "" t t str))) str)

+end_src

Generating a randomish string is useful occasionally. Here is a function that generates a random alpha-numeric character and another that chains that together to create a string.

+begin_src emacs-lisp

(defun random-alnum () "generate a 'random' alpha-numeric character" (let* ((alnum "abcdefghijklmnopqrstuvwxyz0123456789") (i (% (abs (random)) (length alnum)))) (substring alnum i (1+ i))))

(defun random-alnum-string (length) "generate a 'random' alpha-numeric string of length `length'" (let ((output "")) (dotimes (_num length output) (setq output (concat output (random-alnum)))) output))

+end_src

** Setting the Font

My currently preferred font is Fira Code, and size 14 is a decent size for me. Adding this to the ~default-frame-alist~ ensures that this font is the default for all Emacs frames.

+begin_src emacs-lisp

(setq default-frame-alist '((font . "Fira Code-14")))

+end_src

** Setting the Theme

+begin_src emacs-lisp

(use-package spacemacs-theme :ensure t :defer t :init (load-theme 'spacemacs-light 't))

+end_src

** Remove the Toolbar and Scrollbar

I don't like the toolbar or the scrollbar so I disable them:

+begin_src emacs-lisp

(when (display-graphic-p) (tool-bar-mode -1) (scroll-bar-mode -1))

+end_src

I keep the menu bar around because it does come in handy every now and then. However, it just takes up space in a non-interactive CLI session so I disable it when not running graphically.

+begin_src emacs-lisp

(unless (display-graphic-p) (menu-bar-mode -1))

+end_src

** System Bell

No thank you. Disable!

+begin_src emacs-lisp

(setq ring-bell-function 'ignore)

+end_src

** Blinking Cursor

I don't like blinking cursors. Get it gone!

+begin_src emacs-lisp

(blink-cursor-mode 0)

+end_src

** Using 'y' or 'n' rather than 'yes' or 'no'

Sometimes emacs will ask you a question and expect you to type 'yes' or 'no' which is a little tedious. This change will get emacs to prompt for 'y' or 'n' instead.

+begin_src emacs-lisp

(defalias 'yes-or-no-p 'y-or-n-p)

+end_src

** Smooth Scrolling (Emacs 29+)

If we have the new pixel scrolling mode, then we should enable it.

+begin_src emacs-lisp

(when (boundp 'pixel-scroll-precision-mode) (pixel-scroll-precision-mode 1))

+end_src

Using C-o to move around windows can be a pain. Using windmove lets you move around with arrow keys instead.

+begin_src emacs-lisp

(windmove-default-keybindings 'meta)

+end_src

** Tabs and Spaces

I prefer to use spaces instead of tabs:

+begin_src emacs-lisp

(setq-default indent-tabs-mode nil)

+end_src

Emacs also doesn't indent on the 'Enter' key by default. That's easily changed with a key binding:

+begin_src emacs-lisp

(global-set-key (kbd "RET") 'newline-and-indent)

+end_src

** Automatically Updating Files When They Change on Disk

+begin_src emacs-lisp

(global-auto-revert-mode)

+end_src

** Line Numbers

Line numbers are great for programming but not so great for other modes, like =org-mode= and terminals. Therefore we only turn on line number mode when we're in the fundamental programming mode:

+begin_src emacs-lisp

(add-hook 'prog-mode-hook #'display-line-numbers-mode)

+end_src

** Automatically Adding Parenthesis

I find it useful to have Emacs automatically add parenthesis, especially when messing around with Emacs lisp. Instead of using the built-in packages, I use one called [[https://github.com/Fuco1/smartparens][smartparens]] since it's a little bit smarter:

+begin_src emacs-lisp

(use-package smartparens :ensure t :init (require 'smartparens-config) (smartparens-global-mode))

+end_src

** Custom File

Emacs stores some configuration in a file, that by default is the =init.el= file. This makes vendoring the =init.el= in Git tricky, so we can change it to its own file:

+begin_src emacs-lisp

(setq custom-file (concat user-emacs-directory "custom.el")) (when (file-exists-p custom-file) (load-file custom-file))

+end_src

** Backups

Emacs defaults to storing backup files in the same directory as the file being edited. This can create a jumble of files and confuse git. This little bit of configuration ensures that backup files are stored in Emacs' working directory instead:

+begin_src emacs-lisp

(setq backup-directory (concat user-emacs-directory "backups"))

(unless (file-directory-p backup-directory) (mkdir backup-directory))

(setq backup-directory-alist `((".*" . ,backup-directory)))

+end_src

** Hash Key

Sometimes Mac keyboards make it difficult to enter in a hash symbol. To (sort of) get around this, I add a new keybinding it enter a hash key.

+begin_src emacs-lisp

(global-set-key (kbd "C-c 3") (lambda () (interactive) (insert "#")))

+end_src

Emacs has a built in, live, spellchecker called Flyspell. We use flyspell for org mode configuration later on, so we need to make sure it's configured to use an English (GB) dictionary:

+begin_src emacs-lisp

(setq ispell-dictionary "british")

+end_src

I find it useful to quickly spin up new temporary files. I've created a quick elisp function to do this.

+begin_src emacs-lisp

(defun random-temporary-file-name () "creates a random temporary file name" (concat "emacs-tmp-" (random-alnum-string 10)))

(defun temporary-file (file-name) "spawns a quick temporary file" (interactive "sFile name (can be empty): ") (let* ((full-file-name (if (string= "" file-name) (random-temporary-file-name) file-name)) (full-path (concat (temporary-file-directory) full-file-name ))) (find-file full-path)))

; make this a quick keybinding (global-set-key (kbd "C-`") #'temporary-file)

+end_src

The default Emacs minibuffer is fine, but the Ivy package really improves it. With Ivy, you get completions, previews, and many other packages integrate with it.

+begin_src emacs-lisp

(use-package ivy :ensure t :init (ivy-mode 1) (setq ivy-use-virtual-buffers t) (setq enable-recursive-minibuffers t))

+end_src

Ivy also powers a replacement for the default =C-s= search that's really powerful and a great way to navigate around code called Swiper. We download that as well.

+begin_src emacs-lisp

(use-package swiper :ensure t :bind (("C-s" . swiper)))

+end_src

Finally, we can use another Ivy powered tool called 'Counsel' to add some extra functionality to Emacs. I like =counsel-rg= because it lets me search a directory with [[https://github.com/BurntSushi/ripgrep][ripgrep]], perhaps the fastest search tool around, with a Swiper-like interface.

+begin_src emacs-lisp

(use-package counsel :ensure t :bind (("C-c k" . counsel-rg)))

+end_src

org-mode is the package that this very config is written in. It's pretty good out of the box, but I feel that it benefits from a bit of configuration. Ideally, I'd like org-mode to:

+begin_src emacs-lisp

(use-package org :init (setq org-directory "~/org") :config (org-babel-do-load-languages 'org-babel-load-languages (append org-babel-load-languages '((python . t) (shell . t)))) :hook ((org-mode . flyspell-mode) (org-mode . org-indent-mode) (org-mode . visual-line-mode)) )

+end_src

Company is a completion UI framework. LSP mode will use Company to do inline completions.

+begin_src emacs-lisp

(use-package company :ensure t :config (setq company-dabbrev-downcase 0) (setq company-idle-delay 0) )

+end_src

eglot is an LSP package for Emacs. We use it for autocomplete and other fancy features. eglot was chosen because it will be baked into Emacs 29.

+begin_src emacs-lisp

(use-package eglot :ensure t)

+end_src

Magit is a helper for managing Git repositories. It is another Emacs 'killer feature' and makes frequent Git commands really fast to execute.

+begin_src emacs-lisp

(use-package magit :ensure t :bind (("C-x g" . magit-status)) :hook ((magit-mode . magit-auto-revert-mode)))

+end_src

Projectile is a project management system that helps keep multiple projects tidy when running in one emacs instance. I have chosen to override ==C-x p= as the Projectile leader sequence; I tried using =C-c p= but I kept hitting =C-x= instead.

I've also added an extra little Projectile helper function to spawn a new instance of vterm in the project. By default, Projectile only spawns one vterm and will drop you into it whenever you run the vterm sequence. I tend to like a few terminals in one project at once so I wrote that quick function.

+begin_src emacs-lisp

(use-package projectile :ensure t :init (projectile-mode +1) :bind (:map projectile-mode-map ("C-x p" . projectile-command-map) ("C-x p x V" . (lambda () (interactive) (projectile-run-vterm 't)))) )

+end_src

Dired is Emacs' built-in file management tool. It is basically the output from ~ls~ but actionable. Basic file operations are really quick in Dired. Another benefit of Dired is that it integrates with [[https://www.emacswiki.org/emacs/TrampMode][TRAMP mode]], so you can list and modify directories on remote hosts over SSH (and all the other protocols that TRAMP supports).

One of the nice, non-default, behaviours of Dired is that you can open two Dired buffers side by side and copy between them with the 'C' key. We enable this here:

+begin_src emacs-lisp

(setq dired-dwim-target t)

+end_src

I also find myself needing to create files in the dired UI which isn't supported by default. I've chosen 't' for 'touch'.

+begin_src emacs-lisp

(eval-after-load "dired" '(progn (define-key dired-mode-map (kbd "t") (lambda (file-name) (interactive "sTouch file: ") (shell-command (concat "touch " (dired-current-directory) "/" file-name)) (revert-buffer)))))

+end_src

Dired, by default, opens a new buffer when a new directory is opened. This can create a lot of dired buffers. There is another command, ~a~, which opens a new directory buffer and deletes the old one. It needs to be enabled.

+begin_src emacs-lisp

(put 'dired-find-alternate-file 'disabled nil)

+end_src

** Dired Sidebar

I'm going through a sidebar phase. Dired sidebar seems like the vogue one to use. Config taken from the [[https://github.com/jojojames/dired-sidebar][Dired Sidebar README file.]]

+begin_src emacs-lisp

(use-package vscode-icon :ensure t)

(use-package dired-sidebar :bind (("C-x C-n" . dired-sidebar-toggle-sidebar)) :ensure t :commands (dired-sidebar-toggle-sidebar) :init (add-hook 'dired-sidebar-mode-hook (lambda () (unless (file-remote-p default-directory) (auto-revert-mode)))) :config (push 'toggle-window-split dired-sidebar-toggle-hidden-commands) (push 'rotate-windows dired-sidebar-toggle-hidden-commands)

(setq dired-sidebar-subtree-line-prefix "__")
(setq dired-sidebar-theme 'vscode)
(setq dired-sidebar-use-term-integration t))

+end_src

Emacs has a built in terminal emulator, but it's not a /true/ VTY emulator and struggles with some of the more involved commands. There's a package called =vterm= which provides a much more robust terminal emulator.

+begin_src emacs-lisp

(use-package vterm :ensure t)

+end_src

I have also written a quick function to make a new vterm with a specific buffer name. This is really useful when you need to start multiple terminal emulators:

+begin_src emacs-lisp

(defun vterm-named (name) (interactive "sTerminal name: ") (let ((term-name (concat "vterm-" name))) (vterm term-name)))

(global-set-key (kbd "C-x v") #'vterm-named)

+end_src

vterm also [[https://github.com/akermu/emacs-libvterm/blob/master/README.md][recommends]] using helpers in ~.zshrc~ to extend it's functionality. I'll record the ones I use here for reference.

+begin_src sh

vterm_printf() { if [ -n "$TMUX" ] && ([ "${TERM%%-}" = "tmux" ] || [ "${TERM%%-}" = "screen" ] ); then

Tell tmux to pass the escape sequences through

    printf "\ePtmux;\e\e]%s\007\e\\" "$1"
elif [ "${TERM%%-*}" = "screen" ]; then
    # GNU screen (screen, screen-256color, screen-256color-bce)
    printf "\eP\e]%s\007\e\\" "$1"
else
    printf "\e]%s\e\\" "$1"
fi

}

+end_src

Emacs doesn't yet have a Dockerfile mode, so we need to fetch one:

+begin_src emacs-lisp

(use-package dockerfile-mode :ensure t)

+end_src

We leave most of the Python heavy lifting to LSP mode (documented later on). However, there's a helpful package for managing virtual environments that's very handy. I'm a user of [[https://python-poetry.org/][Poetry]], so I point the ~venv-location~ variable at the directory that Poetry stores its virtual environments.

+begin_src emacs-lisp

(use-package virtualenvwrapper :ensure t :config (venv-initialize-interactive-shells) (setq venv-location (concat (getenv "HOME") "/.cache/pypoetry/virtualenvs")))

+end_src

+begin_src emacs-lisp

(defun find-rust-analyzer-directory () "Run rustup to discover rust-analyzer path" (let* ((output (chomp (shell-command-to-string "rustup which --toolchain stable rust-analyzer")))) (if (string-match-p "not found" output) nil (file-name-directory output))))

(defun add-rust-analyzer-to-path () "Add the parent folder of rust-analyzer to the PATH variable and Emacs' exec-path" (let* ((ra-dir (find-rust-analyzer-directory)))

  ; first update the path
  (when (and ra-dir (not (string-match-p ra-dir (getenv "PATH")))) ; we found the ra dir + ra dir not in path
    (setenv "PATH"
            (concat ra-dir ":" (getenv "PATH"))))

  ; now update the exec-path variable
  (when (and ra-dir (not (member ra-dir exec-path)))
    (add-to-list 'exec-path ra-dir))))

(use-package rustic :ensure t :hook ((rustic-mode . company-mode)) :init (add-rust-analyzer-to-path) :config (setq ;; eglot seems to be the best option right now. rustic-lsp-client 'eglot rustic-format-on-save nil ;; Prevent automatic syntax checking, which was causing lags and stutters. eglot-send-changes-idle-time (* 60 60) ) ;; Disable the annoying doc popups in the minibuffer. (add-hook 'eglot-managed-mode-hook (lambda () (eldoc-mode -1))) )

+end_src

Emacs doesn't yet have an in-built YAML mode so we need to install one:

+begin_src emacs-lisp

(use-package yaml-mode :ensure t)

+end_src

There's quite a nice mode for Markdown support, so let's install it:

+begin_src emacs-lisp

(use-package markdown-mode :ensure t)

+end_src

Flycheck is an error checking framework for Emacs. It's useful to get feedback on errors with code.

+begin_src emacs-lisp

(use-package flycheck :ensure t)

+end_src

=wgrep= lets you edit recursive grep buffers and commit your changes back to those files. Really useful for bulk renaming things.

+begin_src emacs-lisp

(use-package wgrep :ensure t)

+end_src

Ensure we hae a mode for editing Clojure and CIDER for interactive development.

+begin_src emacs-lisp

(use-package clojure-mode :ensure t)

(use-package cider :ensure t)

+end_src

The modern Linux firewall is nftables, and it has its own syntax. We should support this (because emacs doesn't do a good job of managing it on its own.)

+begin_src emacs-lisp

(use-package nftables-mode :ensure t)

+end_src

+begin_src emacs-lisp

(add-hook 'conf-toml-mode-hook (lambda () (setq tab-width 4)))

+end_src