46 KiB
Emacs Configuration
- Prelude
- Customize
- Package Management
- Emacs Server
- UI
- Autocompletion
- Org
- Language Integrations
- Tool Integrations
- Backup and Autosave
- Remote Access
- Printing
- Misc
- Coda
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:
- Start at indentation level 0
- Decrease indentation level if the line starts with "end"
- Indent to same level as a previous "end" line
- Increase indentation level if the previous line ends with "is"
- 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)