.emacs.d/config.org

22 KiB
Raw Blame History

Emacs Configuration

Shout out to Harry R. Schwartz; A whole bunch of this config (including the idea of embeddeding the lot in an Org document) is yanked from his dotfiles repo.

The rest of this config grabs packages via use-package, so that needs to be set up to install them if they aren't already.

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

UI

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

  (setq inhibit-startup-screen t)

I like a little more line spacing than default.

  (setq-default line-spacing 0.2)

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)))

Colour Scheme

Currently using spacemacs-theme's light variant, but I prefer a pure white background to the off-white it has by default.

  (use-package spacemacs-theme
    :defer t)
  (setq spacemacs-theme-custom-colors
        '((bg1 . "#ffffff")
          (comment-bg . "#ffffff")))
  (load-theme 'spacemacs-light t)

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))))

Org

I use a couple non-standard bits and pieces, but not a whole bunch. I really like the <s to insert a source block thing (which was deprecated); org-tempo brings that back.

  (use-package org
    :ensure org-plus-contrib
    :config
    (require 'org-tempo))

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)))

Org is nice for scratch space

  (setq initial-major-mode 'org-mode)
  (setq initial-scratch-message "")

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 Emacs Lisp and shell here.

  (org-babel-do-load-languages
   'org-babel-load-languages
   '((emacs-lisp . t)
     (shell . 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)

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)

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"))

Roam

A Zettelkasten in org mode? Yes please. It does need sqlite3 installed outside of Emacs land.

  (use-package org-roam)

As stated in the manual, org-roam needs to know where notes are stored. I'm going to go with ~/org/zet, as its nice having all org documents under ~/org, but also we need to distinguish zettels from other org stuff.

  (make-directory "~/org/zet" t)
  (setq org-roam-directory "~/org/zet")

And, also as recommended, we'll start org-roam-mode after init:

  (add-hook 'after-init-hook 'org-roam-mode)

Hook it into Ido.

  (setq org-roam-completion-system 'ido)

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)))

Version Control

Git

magit is truly a wonderful creation! Only deviations from defaults here are a 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))

Language Integrations

Generic

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

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

And generally indenting with spaces is more common, so make that the default:

  (setq-default indent-tabs-mode nil)

C

For C, I like to indent with tabs and align with spaces: this behaviour is provided by smart-tabs-mode.

  (use-package smart-tabs-mode)
  (smart-tabs-insinuate 'c)

I'll generally format my code in BSD style but I also use clang-format a lot, so I have a keybinding to run that.

  (setq c-default-style "bsd")
  (setq c-basic-offset 4)

  (use-package clang-format)
  (add-hook 'c-mode-hook
            (lambda ()
              (define-key c-mode-map (kbd "C-M-f")
                'clang-format-buffer)))

Meson is my build system of choice for C, but I also use CMake a lot.

  (use-package meson-mode)
  (use-package cmake-mode)

Code Navigation

Using GNU Global for now, so hook ggtags-mode into c-mode:

  (use-package ggtags
    :config
    (add-hook 'c-mode-common-hook
              (lambda () (ggtags-mode 1))))

And, of course, add some keybindings

  (define-key ggtags-mode-map (kbd "C-c g r") 'ggtags-find-reference)
  (define-key ggtags-mode-map (kbd "C-c g d") 'ggtags-find-definition)
  (define-key ggtags-mode-map (kbd "C-c g u") 'ggtags-update-tags)

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

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

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

Lisps

Common Lisp

Use SLIME and Quicklisp for Common Lisp (SBCL), with a convenient binding for slime-selector

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

And we also want to enable execution of CL source blocks in Org mode, which we do by adding an item to org-babel-load-languages.

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

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))))

Scheme and Racket

Geiser is a pretty complete collection of Scheme things for Emacs. Only change from the defaults is to open the REPL in the current window instead of creating a new one.

  (use-package geiser
    :config
    (setq geiser-repl-use-other-window nil))

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))

Desktop

EXWM

One must fulfil the meme of doing everything with Emacs… still got a lot of tweaking to do here before I'm happy.

  (use-package exwm
    :config
    (require 'exwm-config)
    (exwm-config-default))

Multi-monitor

Multi-monitor support is provided in exwm-randr:

  (require 'exwm-randr)
  (exwm-randr-enable)

When I have my laptop connected to a monitor I want the built-in display to turn off, but turn back on when it's disconnected. Turns out this is a total pain.

To start with we need a function to tell whether a monitor's attached. exwm-randr provides exwm-randrget-monitors, but its result is not as I'd expect a list of monitors, but instead a rather complicated mess that is (as far as I can tell) undocumented. Rather than trying to figure out what was going on there, I opted for the search-in-shell-command-output route

  (defun hdmi-connected-p ()
    (string-match-p "HDMI-2 connected"
                    (shell-command-to-string "xrandr")))

With that defined, an exwm-randr-screen-change-hook can then be added to turn the built-in display on and off appropriately.

  (add-hook 'exwm-randr-screen-change-hook
            (lambda ()
              (let ((xrandr-command
                     (if (hdmi-connected-p)
                         "xrandr --output eDP-1 --off --output HDMI-2 --auto"
                       "xrandr --output eDP-1 --auto")))
                (start-process-shell-command "xrandr" nil xrandr-command))))

Mode Line

Clock

The time is a useful thing to know… and 12-hour clock is for losers.

  (setq display-time-24hr-format t)
  (display-time-mode 1)

Battery

Also useful to know, but only on a laptop… once I'm using this configuration on Mandarax as well I'll probably have to conditionally disable it.

  (display-battery-mode 1)

Passwords

This was a little more work than I expected… password-store provides a nice interface to pass, but annoyingly appears to depend on f without declaring so.

  (use-package password-store)
  (use-package f)

However, in order for it to actually work, EasyPG had to be configured to use loopback for pinentry.

  (setq epa-pinentry-mode 'loopback)

gpg-agent also had to be configured to allow loopback for pinentry this was done by adding allow-loopback-pinentry to gpg-agent.conf.

With that all working, all that remains is to add a convenient keybinding for getting a password, which is done by the function password-store-copy. C-c C-p does conflict with some modal bindings, but I think that's fine as most of the time I'll need a password it'll be in some X window anyway… and besides, M-x pass-co RET isn't bad for when it does happen to conflict.

  (global-set-key (kbd "C-c C-p") 'password-store-copy)

TODO Pinentry prompt bugginess

When pass tries to bring up the the pinentry prompt it freezes everything up and I have to C-g to get the prompt to appear. Definitely not ideal but it does work so imma fix that at some other time.

Mail

Currently using mu4e for mail. Not sure whether this is my 'final' set up, I might give notmuch a try at some point.

mu4e is a bit annoying as it's bundled along with mu rather than being loaded from ELPA or MELPA, so it can't be loaded with use-package. Indeed, how to load it depends on how mu was packaged. On NixOS at least, mu4e gets put in a place where Emacs is able to find it, so it just needs to be require'd.

  (require 'mu4e)

I'm sure this will break at some point; when it does, probably makes sense to do something like:

  (let ((uname-output (shell-command-to-string "uname -a")))
    (cond ((string-match-p "NixOS" uname-output) nil)
          ...))
  (require 'mu4e)

To get the correct address by default:

  (setq user-mail-address "cdo@wip.sh")

And to avoid being tickled:

  (setq mail-host-address "hactar")

Automatic updating

For updating through mu4e to actually work, mu4e-get-mail-command needs to be set to offlineimap. New mail can be then fetched with mu4e-update-mail-and-index.

  (setq mu4e-get-mail-command "offlineimap")

Sometimes (like when waiting for on a particular email) it might be useful to have the update run periodically. This can be done with run-with-timer. By only actually updating if fetch-mail non-nil, we give ourselves a way to turn it off.

  (setq mu4e-get-mail-command "offlineimap")
  (defvar fetch-mail nil "Controls whether mail is periodically fetched.")
  (run-with-timer 0 120 (lambda ()
                          (when fetch-mail
                            (mu4e-update-mail-and-index t))))

And then we need something to run through M-x to do that:

  (defun toggle-mail-fetching ()
    "Toggle periodic mail fetching."
    (interactive)
    (setq fetch-mail (not fetch-mail))
    (message "Mail fetching %s"
             (if fetch-mail "enabled" "disabled")))

Mode-line alert

mu4e-alert provides a convenient little icon that shows up whenever mu4e has unread mail.

  (use-package mu4e-alert
    :config
    (add-hook 'after-init-hook
              #'mu4e-alert-enable-mode-line-display))

Sending with sendmail

I have msmtp set up so use that to send mail.

  (setq send-mail-function 'sendmail-send-it)

Multimedia

EMMS seems like a decent multimedia system for Emacs and why not enable all the stable features to start with. Also, mplayer makes a good fallback player.

  (use-package emms
    :config
    (emms-all)
    (add-to-list 'emms-player-list 'emms-player-mplayer))

Browser

To actually get stuff to show up in the browser it seems you have to define a filter that includes everything, because fuck sane defaults.

  (emms-browser-make-filter "all" 'ignore)

MPD

To get EMMS to talk to MPD, we need to tell it how to connect to it, and to use it for getting track info and playing tracks:

  (add-to-list 'emms-info-functions 'emms-info-mpd)
  (add-to-list 'emms-player-list 'emms-player-mpd)
  (setq emms-player-mpd-server-name "localhost"
        emms-player-mpd-server-port "6600"
        emms-player-mpd-music-directory "~/mus")

With those options in place, connecting should work fine (assuming the underlying system has MPD running).

  (emms-player-mpd-connect)
  (emms-cache-set-from-mpd-all)

Podcasts

Elfeed supports media enclosures, so it's ideal for podcasts out-of-the-box.

  (use-package elfeed
    :config
    (setq elfeed-feeds
          '("https://www.patreon.com/rss/seanmcarroll?auth=xZISWBuCvZ1rKXy547HnRXQVyBIscY1P"
            "https://www.patreon.com/rss/plasticpills?auth=S0ExMga6Cco6F4DN30W6Sg9kUciLdjXR"
            "https://feeds.transistor.fm/on-the-metal-0294649e-ec23-4eab-975a-9eb13fd94e06"
            "https://www.pine64.org/feed/mp3/")))

Gopher

Elpher is a gopher and gemini browser for Emacs that looks rather nice.

  (use-package elpher)