.emacs.d/config.org

46 KiB
Raw Permalink Blame History

Emacs Configuration

Prelude

Before doing anything else, set a higher gc-cons-threshold (I'm using 100 MiB here) so that we're not garbage collecting during initialisation to make startup a little faster. We'll save the original value to restore it at the end of initialisation.

  (setq original-gc-cons-threshold gc-cons-threshold)
  (setq gc-cons-threshold (* 100 1024 1024))

Customize

Hey, customize, leave my ~/.emacs.d/init.el alone!

  (setq custom-file (concat user-emacs-directory "customize.el"))
  (load custom-file t)

Package Management

MELPA

Let's be real here, all the good stuff's on MELPA.

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

Initialization

We now need to run package-initialize to load and activate packages. The documentation advises doing this early in configuration.

  (package-initialize)

We also need to fetch the package archives from ELPA and MELPA, unless they have already been fetched:

  (unless package-archive-contents
    (package-refresh-contents))

use-package

The rest of this config grabs packages via use-package, so that needs to be installed:

  (when (not (package-installed-p 'use-package))
    (package-install 'use-package))

The wanted behaviour for use-package here is to ensure all used packages are present.

  (require 'use-package-ensure)
  (setq use-package-always-ensure t)

Hostname

It's useful to have the short hostname available in case we want to configure something differently on certain machines. The full hostname can be retrieved with system-name, but this has the local domain on the end; we want to cut that off.

  (defun cut-at (delimeter string)
    (substring string 0 (seq-position string delimeter)))

  (defvar hostname (cut-at ?. (system-name)))

Emacs Server

Start an Emacs server if one is not running already:

  (require 'server)
  (unless (server-running-p)
    (server-start))

With this, files opened with emacsclient in a terminal will open in Emacs. Most of the time I use Emacs itself for my file browsing and terminal needs but emacsclient is still handy from time-to-time.

UI

The start-up message gets pretty annoying, so disable that.

  (setq inhibit-startup-screen t)

Also, the menu-, tool- and scroll-bar are ugly, take up space and I don't use them.

  (menu-bar-mode -1)
  (tool-bar-mode -1)
  (scroll-bar-mode -1)

It's nice to have an inverse of C-x o for switching between windows. other-window, the function that C-x o is bound to, takes an argument COUNT that determines how many windows it skips forwards so we can simply pass -1 to other-window in a lambda and bind to that:

  (global-set-key (kbd "C-x O")
                  (lambda ()
                    (interactive)
                    (other-window -1)))

Font

I find serif fonts much nicer to read, so definitely change the default face to a serif font. Font size seems inconsistent across different systems for some reason, so need to determine the font height based off the hostname.

(let ((font-height
(pcase hostname
  ("zora" 100)
  ("eddie" 100)
  ("mandarax" 115)
  ("valis" 80)
  ("wipc23120423" 100)
  (_ 110))))
 (set-face-attribute 'default nil
			   :family "Courier 10 Pitch"
			   :height font-height))

I also like a little more line spacing than default, again makes code nicer to read.

  (setq-default line-spacing 0.2)

Colour Scheme

Currently using spacemacs-theme's light variant.

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

The first tweak I make is changing the borders around the mode line to be two pixels thick and the same colour as the background (of the active mode line that is).

  (set-face-attribute 'mode-line nil
                      :box '(:line-width 2 :color "#e7e5eb" :style nil))
  (set-face-attribute 'mode-line-inactive nil
                      :box '(:line-width 2 :color "#e7e5eb" :style nil))

I also set the right window divider to the same colour as the header background.

  (set-face-attribute 'window-divider nil :foreground "#efeae9")
  (setq initial-frame-alist '((right-divider-width . 1)))

Opening buffers in current window

There are several places where buffers open in different windows to the currently selected one. I find this behaviour annoying and I don't understand why anyone would like it. display-buffer-alist provides a mechanism for preventing this where there isn't a better way, as seems to be the case with shell and help buffers.

  (defun open-in-same-window-p (buffer-name action)
    (or (string= (upcase buffer-name) "*SHELL*")
        (string= (upcase buffer-name) "*HELP*")))

  (setq display-buffer-alist
        '((open-in-same-window-p . (display-buffer-same-window . nil))))

Autocompletion

Enable company-mode globally, and hook it into completion-at-point-functions.

  (use-package company
    :config
    (add-hook 'after-init-hook 'global-company-mode)
    (add-to-list 'company-backends 'company-capf))

And enable ido-mode everywhere, with flexible matching.

  (use-package ido
    :config
    (setq ido-enable-flex-matching t)
    (add-hook 'after-init-hook
              (lambda ()
                (ido-everywhere)
                (ido-mode t))))

Snippets

YASnippet is the de facto snippet engine. There are two packages for it: one for the engine itself and one with a library of snippets:

  (use-package yasnippet)
  (use-package yasnippet-snippets)

I don't want it enabled all the time, only in certain modes. The yas-minor-mode function is provided for this, but it requires that the snippet tables are loaded beforehand, so that has to be done now.

  (yas-reload-all)

Org

Code and Quote block shortcuts

I am a big fan of using <s for source blocks and <q for quotes; these are enabled by the org-tempo module, which is included in org but not loaded by default.

  (use-package org :config (require 'org-tempo))

However, I have recently discovered, much to my despair, that these shortcuts do not work if there are tabs in the line ahead of them! Quite ridiculous. Easily worked around, however; I am going to ensure that spaces are used for indentation when in org mode by setting indent-tabs-mode to nil in a hook:

  (add-hook 'org-mode-hook (lambda () (setq indent-tabs-mode nil)))

Keybindings

A keybinding to add a new heading is super useful

  (add-hook 'org-mode-hook
            (lambda ()
              (define-key org-mode-map
                          (kbd "<C-M-return>")
                          'org-insert-heading-after-current)))

Source Blocks

Pressing tab inside a source block should indent appropriately for its language.

  (setq org-src-tab-acts-natively t)

babel lets us evaluate Org documents containing source blocks! I've left the enabling of this for most languages to the section for that language, but I'll add Shell and Elisp here.

  (org-babel-do-load-languages
   'org-babel-load-languages
   '((emacs-lisp . t)
 (shell . t)))

The interface org-babel exposes is a little annoying as later in this config we'll want to preserve the prior value of org-babel-load-languages when enabling another language. However, org-babel-do-load-languages sets the passed symbol to the passed value with set-default before going through the pairs in org-babel-load-languages. Don't ask me why, seems like an obviously bad design. Workaround is to define a function here to use later so at least it won't look ugly.

  (defun enable-org-babel-lang (lang)
    "Enable executing source block in the passed language in
  org-mode. Doesn't affect other enabled languages."
    (org-babel-do-load-languages
     'org-babel-load-languages
     (append org-babel-load-languages `((,lang . t)))))

By default trying to execute a source block prompts you, which is super annoying since I'm realistically not going to try to run any code from Org documents I haven't written, so that needs disabling. You can do that by setting org-confirm-babel-evaluate to nil.

  (setq org-confirm-babel-evaluate nil)

Another annoying thing that happens by default is the clobbering of the window layout when you open a source block. You can change that by setting org-src-window-setup.

  (setq org-src-window-setup 'split-window-below)

Asyncronous Execution

ob-async makes source blocks with the :async keyword execute asyncronously, super handy for long-running snippets etc.

  (use-package ob-async)

Exporting

I very rarely want a table of contents, as most of my org documents are pretty short.

  (setq org-export-with-toc nil)

Also don't want section numbering for similar reasons:

  (setq org-export-with-section-numbers nil)

HTML

htmlize is needed for decent HTML exporting, but there is no need for all that stuff at the bottom.

  (use-package htmlize)
  (setq org-html-postamble nil)

I like margins and line height:

  (setq org-html-style
        "<style>
    body {
      margin: 3em auto;
      max-width: 42em;
      padding: 0 2em;
    }
  </style>")

LaTeX

Use minted (LaTeX package) to do syntax highlighting in code blocks:

  (add-to-list 'org-latex-packages-alist '("" "minted"))
  (setq org-latex-listings 'minted)

minted actually calls pygments through the shell, which pdflatex doesn't like; you have to tell it not to worry, and that everything is going to be OK.

  (setq org-latex-pdf-process
        '("xelatex -shell-escape -interaction nonstopmode -output-directory %o %f"
          "xelatex -shell-escape -interaction nonstopmode -output-directory %o %f"
          "xelatex -shell-escape -interaction nonstopmode -output-directory %o %f"))

Default Applications

It's all fun and games until C-c C-e h o opens the source code.

  (setq org-file-apps
        '(("html" . "firefox %s")
          (auto-mode . emacs)))

Indentation

Setting org-adapt-indentation to t ensures that Org will indent text under a headline:

  (setq org-adapt-indentation t)

Language Integrations

Generic

Generally, 8-character-wide tabs are not my thing.

  (setq-default tab-width 4)
  (setq-default basic-offset 4)

Use tabs as god intended:

  (setq-default indent-tabs-mode t)

Language Server Protocol

LSP seems to be the way forward in terms of IDE-like features in Emacs; grab lsp-mode and enable lsp-deferred:

  (use-package lsp-mode
    :init (setq lsp-keymap-prefix "C-c l")
    :commands (lsp lsp-deferred))

lsp-deferred means that the LSP server will only be started once a buffer is actually opened, which makes more sense to me.

Also going to give lsp-ui a shot, which displays a bunch of information from the language server in the buffer. It looks like it could be a bit much but we'll see.

  (use-package lsp-ui :commands lsp-ui-mode)

For LSP's most excellent autocompletion to work properly yas-minor-mode must be enabled, so hook that into lsp-mode:

  (add-hook 'lsp-mode-hook
            (lambda ()
              (yas-minor-mode)))

To enable Ido integration:

  (require 'lsp-ido)

Smart Tabs

Indent with tabs and align with spaces. Installing the package here but it's enabled on a per-language basis in the languages' individual config sections.

  (use-package smart-tabs-mode)

The package has this really annoying behaviour that it turns itself off if indent-tabs-mode is nil, even when you just explicitly turned it on. The solution on the Emacs wiki is to set indent-tabs-mode to t in a c-mode-common hook, which is a bit of a hack, but I tried my own approach and it didn't work for no apparent reason so I'm just going to do as I'm told.

  (add-hook 'c-mode-common-hook
            (lambda () (setq indent-tabs-mode t)))

C

For indenting style, I like BSD-style but with 4-char-wide indents

  (add-hook 'c-mode-hook (lambda ()
                           (c-set-style "bsd")
                           (setq c-basic-offset 4)))

And we want to enable smart tabs:

  (smart-tabs-insinuate 'c)

There's a lot of boilerplate in C, so I want YASnippet enabled.

  (add-hook 'c-mode-hook (lambda () (yas-minor-mode)))

C++

Essentially the same story as for C.

  (add-hook 'c++-mode-hook #'lsp-deferred)
  (add-hook 'c++-mode-hook (lambda ()
                             (c-set-style "bsd")
                             (setq c-basic-offset 4)))
  (smart-tabs-insinuate 'c++)

Haskell

My workflow with Haskell is very REPL-based, so I always want interactive-haskell-mode on.

  (use-package haskell-mode)
  (require 'haskell-interactive-mode)
  (add-hook 'haskell-mode-hook 'interactive-haskell-mode)

And, of course, that REPL needs to be taking advantage of parallelism!

  (require 'haskell-process)
  (set-variable 'haskell-process-args-ghci
                '("-threaded" "+RTS" "-N8" "-RTS"))

Idris

The only thing to change from the defaults here is to add a more convenient way to case-split.

  (use-package idris-mode)
  (add-hook 'idris-mode-hook
            (lambda ()
              (define-key idris-mode-map (kbd "C-c SPC")
                'idris-case-split)))

Rust

rust-mode provides basic support:

  (use-package rust-mode)

Then rust-analyzer via LSP does the rest :)

  (add-hook 'rust-mode-hook #'lsp-deferred)

Lisps

Common Lisp

Use SLIME and Quicklisp for Common Lisp (SBCL).

  (use-package slime)
  (setq inferior-lisp-program "sbcl")
  (global-set-key (kbd "C-c s") 'slime-selector)
  (let ((helper-filename "~/quicklisp/slime-helper.el"))
    (when (file-exists-p helper-filename)
      (load (expand-file-name helper-filename))))

This assumes that SBCL and Quicklisp, along with Quicklisp's SLIME helper, are installed. Once SBCL is installed, Quicklisp and its SLIME helper can be installed by grabbing the installer and loading it with SBCL.

  curl -O https://beta.quicklisp.org/quicklisp.lisp
  sbcl --load quicklisp.lisp

That will open a REPL with the quicklisp-quickstart system loaded. At that REPL, run:

  (quicklisp-quickstart:install)
  (ql:add-to-init-file)
  (ql:quickload "quicklisp-slime-helper")
Source blocks

Enable execution of CL source blocks in Org mode:

  (enable-org-babel-lang 'lisp)
ASDF

SLIME has a contrib for ASDF integration, slime-asdf. This is enabled by adding it to slime-contribs:

  (add-to-list 'slime-contribs 'slime-asdf)

Paredit

paredit is generally very useful for balancing parenthesis so we want that turned on for all the lisps. Additionally, it's nice to have an entire expression highlighted when the cursor is on one of its enclosing parens.

  (use-package paredit)
  (setq lispy-mode-hooks
        '(emacs-lisp-mode-hook
          lisp-mode-hook
          racket-mode-hook
          scheme-mode-hook
          slime-repl-mode-hook))
  (dolist (hook lispy-mode-hooks)
    (add-hook hook (lambda ()
                     (setq show-paren-style 'expression)
                     (paredit-mode))))

YAML

I don't really like YAML if I'm honest, but it's used a lot so…

  (use-package yaml-mode)

Javascript

The first bit of this setup (js2-mode, js2-refactor and xref-js2) is essentially copied from this Emacs cafe post.

First of all we want to grab js2-mode and enable it for javascript buffers. It extends the default js-mode and builds an AST which can be used by other packages.

  (use-package js2-mode)
  (add-to-list 'auto-mode-alist '("\\.js\\'" . js2-mode))

Refactoring

js2-refactor provides refactoring tools based of said AST, so enable that and its keybindings:

  (use-package js2-refactor)
  (add-hook 'js2-mode-hook #'js2-refactor-mode)
  (js2r-add-keybindings-with-prefix "C-c C-r")

It provides a kill function with nice semantics for Javascript we definitely want that instead of the generic kill.

  (define-key js2-mode-map (kbd "C-k") #'js2r-kill)

Find references / jump to definition

Then we get to xref-js2, which adds stuff for jumping to references and definitions (uses the ag tool, so that must be installed in the environment):

  (use-package xref-js2)

js-mode binds M-., which conflicts with xref-js2 so we need to unbind that:

  (define-key js-mode-map (kbd "M-.") nil)

And hook it up to js2-mode:

  (add-hook 'js2-mode-hook (lambda ()
    (add-hook 'xref-backend-functions #'xref-js2-xref-backend nil t)))

Using local tools from NPM

To use tools locally by NPM, there is add-node-modules-path:

  (use-package add-node-modules-path)
  (eval-after-load 'js2-mode
    '(add-hook 'js2-mode-hook #'add-node-modules-path))

Autoformatting

Prettier seems low-effort to set up :D

  (use-package prettier-js)
  (add-hook 'js2-mode-hook 'prettier-js-mode)

Prolog

prolog-mode comes with Emacs, but .pl files are assumed to be Perl (which I never use, it scares me), so we need to change that.

  (add-to-list 'auto-mode-alist '("\\.pl\\'" . prolog-mode))

Dockerfiles

Grab dockerfile-mode for syntax highlighting etc in Dockerfiles:

  (use-package dockerfile-mode)

Zig

zig-mode provides basic language integration for Zig:

  (use-package zig-mode)

There's a language server implementation for Zig so we'll be using that via lsp-mode alongside zig-mode.

  (add-hook 'zig-mode-hook #'lsp-deferred)

GLSL

Firstly, glsl-mode provides basic support:

  (use-package glsl-mode)

It's a C-like language, so I want bsd code style and smart-tabs. The former is easy:

  (add-hook 'glsl-mode-hook
            (lambda ()
              (c-set-style "bsd")
              (setq c-basic-offset 4)))

Since smart-tabs doesn't support GLSL out of the box, we need to add support with smart-tabs-add-language-support. There's an example of how to use it on Emacs Wiki:

  (smart-tabs-add-language-support c++ c++-mode-hook
    ((c-indent-line . c-basic-offset)
     (c-indent-region . c-basic-offset)))

c-indent-line et al will do fine for GLSL too since its syntax is very similar to C's, so adding support for it looks very similar to that example:

  (smart-tabs-add-language-support glsl glsl-mode-hook
    ((c-indent-line . c-basic-offset)
     (c-indent-region . c-basic-offset)))

Now that support is added, smart-tabs-insinuate should do its job:

  (smart-tabs-insinuate 'glsl)

Mermaid

Mermaid is a diagramming language. First of all we need syntax highlighting etc. This is provided by mermaid-mode:

  (use-package mermaid-mode)

Also install ob-mermaid to add mermaid support to org-babel:

  (use-package ob-mermaid)

And finally allow execution of mermaid source blocks (used to view the diagrams):

  (enable-org-babel-lang 'mermaid)

crontab

Using crontab-mode, because it's called crontab-mode lol

  (use-package crontab-mode)

Python

Going to use LSP for Python:

  (add-hook 'python-mode-hook #'lsp-deferred)

Ada

Old ada-mode

Unfortunately, the ada-mode on ELPA is hot garbage. It requires a custom parser to be built from its sources, and as far as I can tell it's completely broken: every version I've tried to build has produced multiple compile errors.

There was a more basic ada-mode built in to Emacs, and thankfully someone else has already done the hard work of bundling that up Using the old version of Ada Mode for Emacs. They've provided a ZIP file with all the neccessary files bundled into it, which can be grabbed with the following:

  cd ~/Downloads
  curl -LO https://tkurtbond.github.io/emacs/old-ada-mode.zip
  unzip -d ~/.emacs.d old-ada-mode.zip

The directory ~/.emacs.d/ada-mode than has to be added to load-path and autoloaded:

  (let* ((home (getenv "HOME"))
         (path (concat home "/.emacs.d/ada-mode")))
    (add-to-list 'load-path path))

  (autoload 'ada-mode "ada-mode")

LSP

lsp-mode with ada_language_server provides all the IDE-esque niceties:

  (add-hook 'ada-mode-hook #'lsp-deferred)

ada_language_server has to be installed manually. There are linux builds available on the GitHub releases page.

Indentation

Set the indent width to 4:

  (setq ada-indent 4)

GPRBuild files

GPRBuild files are pretty straightforward, but there doesn't seem to be a major mode on ELPA for them so going to make a basic one.

Custom major mode

Start out by defining a list of keywords:

  (defvar gpr-keywords
    '("case" "end" "external" "for" "is" "null"
      "package" "project" "use" "when" "with"))

And a list of builtins:

  (defvar gpr-builtins
    '("Compiler" "Default_Switches" "Exec_Dir" "Library_Dir"
      "Library_Kind" "Library_Name" "Local_Configuration_Pragmas"
      "Main" "Object_Dir" "Source_Dirs"))

Define some font lock regexes:

  (defvar gpr-font-lock-defaults
    (let ((string-regex (rx (sequence "\"" (*? (not "\"")) "\"")))
          (constant-regex (rx (or (+ digit) "True" "False"))))
      `((,string-regex . font-lock-string-face)
        (,constant-regex . font-lock-constant-face)
        (,(regexp-opt gpr-builtins 'words) . font-lock-builtin-face)
        (,(regexp-opt gpr-keywords 'words) . font-lock-keyword-face))))

Create a variable for the indent width:

  (defvar gpr-indent-width 4)

We then need to define a function for indentation, which is non-trivial. A simple set of rules that gets us most of the way is:

  1. Start at indentation level 0
  2. Decrease indentation level if the line starts with "end"
  3. Indent to same level as a previous "end" line
  4. Increase indentation level if the previous line ends with "is"
  5. Otherwise indent to level 0
  (defconst gpr-block-start-regex
    (rx (sequence line-start
                  (zero-or-more not-newline)
                  "is"
                  (zero-or-more blank)
                  line-end)))

  (defconst gpr-block-end-regex
    (rx (sequence line-start
                  (zero-or-more blank)
                  "end")))

  (defun gpr-indent-line ()
    "Indent the current line as GPRBuild code"
    (interactive)
    (beginning-of-line)
    (indent-line-to (gpr-get-indent-level)))

  (defun gpr-get-indent-level ()
    (cond ((bobp) 0)
          ((looking-at-p gpr-block-end-regex)
           (save-excursion
             (forward-line -1)
             (max (- (current-indentation) gpr-indent-width) 0)))
          (t (gpr-get-indent-level-from-previous))))

  (defun gpr-get-indent-level-from-previous ()
    (save-excursion
      (let (indent)
        (while (not indent)
          (forward-line -1)
          (setq indent
                (cond ((looking-at-p gpr-block-start-regex)
                       (+ (current-indentation) gpr-indent-width))
                      ((looking-at-p gpr-block-end-regex)
                       (current-indentation))
                      ((bobp) 0))))
        indent)))

Define the mode, inheriting from prog-mode:

  (define-derived-mode gpr-mode prog-mode "GPRBuild"
    "GPR Mode is a major mode for editing GPRBuild files"
    (set (make-local-variable 'comment-start) "--")
    (set (make-local-variable 'comment-end) "")

    (set (make-local-variable 'font-lock-defaults)
         '(gpr-font-lock-defaults))

    (set (make-local-variable 'indent-line-function)
         'gpr-indent-line))

Finally, add an auto-load-alist entry for .gpr files:

  (add-to-list 'auto-mode-alist '("\\.gpr\\'" . gpr-mode))

Lua

Just using basic lua-mode package:

  (use-package lua-mode)

I want to indent with tabs (set to 4 characters wide):

  (setq lua-indent-level 4)

I also want to be able to run lua-format on files with C-c f like I have with clang-format. The first step for this is to make an interactive function to run the formatter; this can be done with call-process-region.

  (defvar lua-format-binary "lua-format")

  (defun lua-format ()
    (interactive)
    (if (executable-find lua-format-binary)
        (let ((start (if (region-active-p) (region-beginning) (point-min)))
              (end (if (region-active-p) (region-end) (point-max))))
          (call-process-region start end lua-format-binary t '(t nil)))
      (error "%s" (concat lua-format-binary " not found."))))

This then needs to be assigned to the keybinding:

  (add-hook
   'lua-mode-hook
   (lambda () (define-key lua-mode-map (kbd "C-c f") 'lua-format)))

BASIC

basic-mode provides syntax highlighting and a few nice features:

  (use-package basic-mode)

As well as .bas files, I want to open all .bbc files in basic-mode:

  (add-to-list 'auto-mode-alist '("\\.bbc\\'" . basic-mode))

Nix

Basic editing support comes from nix-mode:

  (use-package nix-mode)

And nix-update provides a convenient way to update fetch blocks:

  (use-package nix-update)

SCAD

There is a language server for OpenSCAD, but I think I'll just stick to the basic mode:

  (use-package scad-mode)

Go

First of all, of course, install go-mode:

  (use-package go-mode)

This package provides a convenient lil function to use gofmt to format a buffer; I want to run this whenever I save a go source file. This is pretty easily done by adding a before-save-hook in a go-mode-hook (hey, I heard you like hooks…)

  (add-hook 'go-mode-hook
            (lambda ()
              (add-hook 'before-save-hook 'gofmt-before-save)))

Tool Integrations

Git

magit is truly a wonderful creation! Add keybinding for magit-status and a maximum length for the summary line of commit messages (after which the excess is highlighted).

  (use-package magit
    :bind
    ("C-x g" . magit-status)
    :config
    (setq git-commit-summary-max-length 72))

By default, magit-status will open itself in a different window to the one you open it in. I really don't understand how this is useful but thankfully this is Emacs so the behaviour can be tweaked. The default behaviour does make sense for other magit windows, just not magit-status.

The behviour I want can be achieved by setting magit-display-buffer-function to something which will open the buffer in the current window if and only if it's a magit-status-mode window.

  (setq magit-display-buffer-function
        (lambda (buffer)
          (display-buffer
           buffer
           (when (eq (with-current-buffer buffer major-mode)
                     'magit-status-mode)
             '(display-buffer-same-window)))))

Now I'm thinking it I could customise this further as I often am annoyed by diffs opening in a different window but I think I'll leave it at that for now as I'm not sure precisely what behaviour I'd want.

Docker

I use docker quite a lot, unfortunately, so it's nice to be able to spawn containers etc from Emacs. The docker package provides a few nice bits and bobs.

  (use-package docker
    :bind ("C-c d" . docker))

Build systems

CMake

I hate it, but it's everywhere. cmake-mode provides basic syntax highlighting etc.

  (use-package cmake-mode)

Meson

Use meson-mode for syntax highlighting etc in meson.build files.

  (use-package meson-mode)

Cargo

I never really use Rust without Cargo, so always turn on the minor mode for Cargo in Rust buffers.

  (use-package cargo)
  (add-hook 'rust-mode-hook 'cargo-minor-mode)

BitBake

I hate Yocto and its tools but alas, I must use it for work :(

  (use-package bitbake)

clang-format

Most of the time, lsp-mode is fine for formatting, but sometimes it doesn't work (mostly just because I haven't gone through the effort to set it up) but I still want to be able to auto-format code easily (that is to say, with a convenient keybinding). The clang-format package provides Elisp functions for invoking it.

  (use-package clang-format)

I want a keybinding that formats the region if its active, or the whole buffer otherwise. It seems that there's no function which does that out of the box, so that has to be defined first:

  (defun clang-format-region-or-buffer ()
    "Format the region if it's active, otherwise format the entire buffer."
      (interactive)
      (if (use-region-p)
          (clang-format-region (region-beginning) (region-end))
        (clang-format-buffer)))

With that defined, the keybinding can be added to C mode.

  (add-hook
   'c-mode-hook
   (lambda ()
     (define-key c-mode-map (kbd "C-c f")
       'clang-format-region-or-buffer)))

GraphViz

The graphviz-dot-mode package provides some integration for the GraphViz suite of tools, and a mode for the 'dot' language they use:

  (use-package graphviz-dot-mode)

Man pages

Man page support is built in to Emacs but it's one of those annoying things where it will open in the "other" window instead of where you ran M-x man from. Thankfully, this behaviour can be changed by setting Man-notify-method. The value 'pushy makes the man page open in the current window.

  (setq Man-notify-method 'pushy)

Script-Fu

GIMP has a scheme-based language Script-Fu built into it that you can use to script things (based). Sadly, the built-in console is rather lackluster as a coding environment. Happily, there is an option to run a server which listens for Script-Fu commands on a TCP port, so I can use comint to make my own lil interface in Emacs.

It's things like this that make me really glad I switched to Emacs because this is ridiculously cool. By my definition of "cool" anyway (what can I say, I'm a massive nerd).

I should probably extract this and make a standalone package out of it and stick it on Melpa at some point.

REPL

The Script-Fu server request format is very simple:

Bytes Description
0 'G' magic byte (47h)
1-2 Length of expression (BE 32-bit unsigned int)
3+ Expression

Writing an encoder for this is pretty trivial:

  (defun script-fu-repl-encode-request (input)
    (let* ((len (length input))
           (hi (logand (lsh len -8) #xff))
           (lo (logand len #xff))
           (hdr (vector ?G hi lo)))
      (vconcat hdr (encode-coding-string input 'utf-8))))

We then want a sender function to use with comint-mode that applies this encoding. Unfortunately, it seems that there is no comint-send-bytes or similar function to directly send a byte vector to the comint process. I did try just sending the request as a string with some invalid characters at the start but ran into issues: Emacs would sometimes insert unicode control characters into the data, which GIMP understandably didn't appreciate.

The method I ended up with is to create a temporary, unibyte buffer, stick the data in there and then use comint-send-region to send the data. It's a bit of a kludge but it seems like it should be reasonably robust.

  (defun script-fu-repl-comint-send-bytes (proc bytes)
    (let ((temp-buffer (generate-new-buffer "*script-fu-repl-tmp*")))
      (unwind-protect
          (with-current-buffer temp-buffer
            (set-buffer-multibyte nil)
            (insert (apply #'string (append bytes nil)))
            (comint-send-region proc (point-min) (point-max)))
        (kill-buffer temp-buffer))))

With that handled, implementing the sender function itself is nice and easy:

  (defun script-fu-repl-send (proc input)
    (let ((request (script-fu-repl-encode-request input)))
      (script-fu-repl-comint-send-bytes proc request)))

The response format is similarly simple:

Bytes Description
0 'G' magic byte (47h)
1 Status code 0 on success, 1 on error
2-3 Length of response text
4 Response text

For now, we only care about the response text, so all we need to do is skip the first 4 bytes and add a trailing newline.

  (defun script-fu-repl-decode-response (response)
    (concat (substring response 4) "\n"))

Another thing is adding a prompt to the comint buffer the server doesn't send one, so we have to add it ourselves.

  (defvar script-fu-repl-prompt "> ")
  (defun script-fu-repl-insert-prompt (output)
    (unless (string-blank-p output)
      (let ((proc (get-buffer-process (current-buffer))))
        (goto-char (process-mark proc))
        (unless (looking-back script-fu-repl-prompt)
          (insert script-fu-repl-prompt)
          (set-marker (process-mark proc) (point)))))
    output)

A mode for the client buffer can then be derived from comint-mode.

  (define-derived-mode script-fu-repl-mode comint-mode "Script-Fu REPL"
    (setq-local comint-prompt-read-only t)
    (setq-local comint-prompt-regexp nil)
    (setq-local comint-input-sender #'script-fu-repl-send)
    (add-hook 'comint-preoutput-filter-functions
              'script-fu-repl-decode-response nil t)
    (add-hook 'comint-output-filter-functions
              'script-fu-repl-insert-prompt nil t))

Now, to create a function to create or get the current REPL buffer. The comint-check-proc function can be used to test whether the buffer is already set up. Rather nicely, make-comint-in-buffer supports passing a (HOST . SERVICE) pair to specify a TCP connection to open (via open-network-stream) so this is pretty simple. In both cases, we want to return the client buffer for the caller to use.

  (defvar script-fu-repl-server '("localhost" . 10008))
  (defun script-fu-repl ()
    (interactive)
    (let ((buffer (get-buffer-create "*Script-Fu REPL*")))
      (when (not (comint-check-proc buffer))
        (make-comint-in-buffer "Script-Fu REPL"
                               buffer script-fu-repl-server)
        (with-current-buffer buffer (script-fu-repl-mode)))
      (pop-to-buffer buffer '((display-buffer-in-direction)
                              (direction . below)
                              (window-height . 0.3)))
      buffer))

Code Editing

With the client stuff done, we can define the code editing mode:

  (define-derived-mode script-fu-mode scheme-mode "Script-Fu")

Now to define something to send an expression or region to the REPL. Since script-fu-repl returns the buffer we can use that to transparently start a REPL or get the existing one if one's already running.

  (defun script-fu-mode-send-region-or-sexp ()
    (interactive)
    (let ((code (if (use-region-p)
                    (let ((start (region-beginning))
                          (end (region-end)))
                      (buffer-substring-no-properties start end))
                  (thing-at-point 'sexp t))))
      (if (not code) (message "No code to send.")
        (let* ((repl-proc (get-buffer-process (script-fu-repl))))
          (script-fu-repl-send repl-proc code)))))

  (define-key script-fu-mode-map (kbd "C-c C-c")
              'script-fu-mode-send-region-or-sexp)

And finally, a similar thing for the whole file:

  (defun script-fu-mode-send-file ()
    (interactive)
    (let* ((repl-proc (get-buffer-process (script-fu-repl)))
           (buffer-contents
            (buffer-substring-no-properties (point-min)
                                            (point-max))))
      (script-fu-repl-send repl-proc buffer-contents)))
  (define-key script-fu-mode-map (kbd "C-c C-l")
              'script-fu-mode-send-file)

I think that's all I need for now!

Backup and Autosave

Keep $PWD Tidy

Emacs' default behaviour of dumping temporary files in the current directory is quite a pain, so we want to get it to instead stick them in a dedicated directory somewhere far away.

We can do that for auto-save files by setting the variable auto-save-file-name-transforms:

  (make-directory "~/.emacs-tmp/auto-save" t)
  (setq auto-save-file-name-transforms '((".*" "~/.emacs-tmp/auto-save" t)))

And for backup files there's backup-directory-alist.

  (make-directory "~/.emacs-tmp/backup" t)
  (setq backup-directory-alist '(("." . "~/.emacs-tmp/backup")))

Backup by Copying

By default Emacs moves a file to the backup location and then creates a copy in the original location, which apart from being a very strange thing to do also messes up hard links. Setting backup-by-copying changes it to the more obvious behaviour of simply copying the file to the backup location.

  (setq backup-by-copying t)

Use trash

Commands like delete-file and delete-directory, as well as deletion commands in Dired can be made to move things to trash, instead of permanently deleting them. This done by setting the delete-by-moving-to-trash variable to t.

  (setq delete-by-moving-to-trash t)

Remote Access

Sudo/doas on Remote Hosts

To edit files as root on remote hosts with sudo or doas while also tunneling over SSH, you need to configure TRAMP to use SSH as a proxy. This is done by adding to tramp-default-proxies-alist, as detailed in the TRAMP manual:

  (add-to-list 'tramp-default-proxies-alist
               '(nil "\\`root\\'" "/ssh:%h:"))
  (add-to-list 'tramp-default-proxies-alist
               '((regexp-quote (system-name)) nil nil))

Printing

  (setq ps-paper-type 'a4
        ps-font-size 10
        ps-print-header nil)

Misc

God mode

God mode essentially makes Emacs a bit more VI-like by introducing a mode where modifier keys are implicitly held down, thereby reducing the amount of 'emacs claw' required for most commands. It runs as a global minor mode.

  (use-package god-mode)
  (god-mode)

In order to enter normal mode, god-mode-all must be ran, so we'll bind C-. to that:

  (global-set-key (kbd "C-.") #'god-mode-all)

I find it jarring and confusing to have it on and off in different types of buffers, so will just disable the exemptions:

  (setq god-exempt-major-modes nil)
  (setq god-exempt-predicates nil)

Coda

Now that initialization is finished, gc-cons-threshold should be set back to its default value:

  (setq gc-cons-threshold original-gc-cons-threshold)