Ian's Emacs Config
This is my Emacs configuration. If you read this file, you may find things you can copy-paste into your Emacs config.
Here is a screenshot of my environment:
Some of the other screenshots throughout may not look exactly like this. That is because they are old screenshots.
Entrypoint
First I need to configure Emacs to load this file (ian.org
) as its first action when it starts up. By default, Emacs runs init.el
at the beginning of execution. The following piece of code tangles to init.el
, and init.el
containing the following must be checked in, because this snippet tangles this file (ian.org
), so it is this piece of code that starts the whole process of loading all of this configuration.
Since I want most of the configuration here in ian.org
, init.el
just holds the bare minimum code so that the bulk of the configuration can be checked in once, inside this file, rather than twice like the contents of init.el
. I'm using an example from orgmode.org to load the Org files and tangle them, then require
the output of this file from the call to tangle, run main
, and I'm done.
NOTE The filename ian.org
is hardcoded in this entrypoint routine in the place of the main configuration file. This is because, despite the public nature of my config, it is not intended to be used in whole by anyone but me. This allows me certain shortcuts, like hostname-specific configuration, and convention-over-configuration in ways I find intuitive without overly detailed documentation. It is my config, after all, so my main config file is ian.org
.
;;; init --- the Emacs entrypoint
;;; Commentary:
;;;
;;; Just load my customizations and execute -- org-mode bootstrap from
;;; https://orgmode.org/worg/org-contrib/babel/intro.html#literate-emacs-init
;;;
;;; Code:
;; Load up Org Mode and (now included) Org Babel for elisp embedded in Org Mode files
(setq dotfiles-dir (file-name-directory (or (buffer-file-name) load-file-name)))
(let* ((org-dir (expand-file-name
"lisp" (expand-file-name
"org" (expand-file-name
"src" dotfiles-dir))))
(org-contrib-dir (expand-file-name
"lisp" (expand-file-name
"contrib" (expand-file-name
".." org-dir))))
(load-path (append (list org-dir org-contrib-dir)
(or load-path nil))))
;; load up Org-mode and Org-babel
(require 'ob-tangle))
;; load up all literate org-mode files in this directory
(mapc #'org-babel-load-file (directory-files dotfiles-dir t "\\.org$"))
(require '~/.emacs.d/ian.el)
;; Load automatic and interactive customizations from this computer
(shell-command "touch ~/.emacs.d/.emacs-custom.el")
(setq custom-file "~/.emacs.d/.emacs-custom.el")
(load custom-file)
(provide 'init)
The rest of the code that is executed begins with the routines defined by this file.
Package Manager Bootstrap
The first thing that must be done is to prepare to manage third party packages, because my config is built on top of the work of many third party packages. I like to install and manage all of the packages I use as part of my configuration so that it can be duplicated across computers (more or less) and managed with git
, so I use use-package
to ensure that packages are installed from my configuration file.
Bootstrap sets up the ELPA, Melpa, and Org Mode repositories, sets up the package manager, installs use-package
if it is not found, configures use-package
and installs a few extra packages that acoutrement use-package
and will be used heavily throughout.
;;; ian.el --- my custom emacs config with no one else considered because fuck you
;;; naw but really I just don't have the time for that
;;;
;;; Commentary:
;;;
;;; After throwing away an old Emacs config, built when I had no idea what I was doing
;;; and abandoning the "wisdom of the crowds"-configured Spacemacs for better control
;;; here we are for better or worse
;;;
;;; Code:
(require 'package)
(setq package-archives '(("gnu" . "https://elpa.gnu.org/packages/")
("melpa" . "https://melpa.org/packages/")
("org" . "http://orgmode.org/elpa/")))
(package-initialize)
;; Now install use-package to enable us to use it
;; to manage the rest of our packages
(unless (package-installed-p 'use-package)
(progn
(unless package-archive-contents
(package-refresh-contents))
(package-install 'use-package)))
;; set ensure to be the default
(require 'use-package-ensure)
(setq use-package-always-ensure t)
;; these go in bootstrap because packages installed
;; with use-package use :diminish and :delight
(use-package diminish)
(use-package delight)
Once this is done I need to install and configure any third party packages that are used in many modes throughout Emacs. Some of these modes fundamentally change the Emacs experience and need to be present before everything can be configured.
Fundamental Package Installation and Configuration
First I need to install packages with a large effect and on which other packages are likely to depend. These are packages essential to my workflow. Configuration here should be config that must run early, before variables are set or language-related packages, which will likely rely on these being set.
Icons
Treemacs and Doom themes both rely upon all-the-icons
to look nice
(use-package all-the-icons)
Along the way nerd-icons also gets installed. On first run or after clearing out elpa/, need to run the following:
M-x nerd-icons-install-fonts
M-x all-the-icons-install-fonts
This installs the actual fonts and only needs to be called once. Maybe I'll automate it someday.
Treemacs
Treemacs provides a file browser on the left hand side of Emacs that I have grown to really like. It's great for exploring unfamiliar projects and modules.
It's installed early because many things have integrations with it, including some themes.
(use-package treemacs
:defer t
)
;; disable icons at least until they are fixed
;; see commit message for more details 7/11/23
(setq treemacs-no-png-images t)
(use-package treemacs-evil
:after (treemacs evil))
(use-package treemacs-projectile
:after (treemacs projectile))
(use-package treemacs-magit
:after (treemacs magit))
Theme
I'm mainly using the Doom Emacs theme pack. I think they're really nice to look at, especially with solaire-mode
.
First install the theme pack:
(use-package doom-themes
:config
;; Global settings (defaults)
(setq doom-themes-enable-bold t ; if nil, bold is universally disabled
doom-themes-enable-italic t
) ; if nil, italics is universally disabled
;; Corrects (and improves) org-mode's native fontification.
(doom-themes-org-config)
)
Protesilaos Stavrou has a nice theme pack too:
(use-package ef-themes)
Theme lists
I have separated the Doom themes into light and dark, so I can have a randomly chosen light theme in the late morning and early afternoon, and switch back to a dark theme at other times.
I'll curate the lists as I use the new functionality, to remove ones I don't like.
-
Light themes
(defvar light-theme-list '(doom-one-light doom-acario-light doom-fairy-floss doom-flatwhite doom-opera-light doom-gruvbox-light doom-horizon))
-
Dark themes
(defvar dark-theme-list '(doom-Iosvkem doom-challenger-deep doom-city-lights doom-dark+ doom-dracula doom-ephemeral doom-fairy-floss doom-gruvbox doom-henna doom-horizon doom-laserwave doom-material doom-miramare doom-molokai doom-monokai-classic doom-monokai-pro doom-moonlight doom-nord doom-nova doom-oceanic-next doom-old-hope doom-one doom-opera doom-outrun-electric doom-palenight doom-peacock doom-plain doom-rouge doom-snazzy doom-solarized-dark doom-spacegrey doom-tomorrow-night doom-vibrant doom-zenburn))
Noninteractive theme picker
For running at startup
(defun set-theme-at-specific-times ()
"Set light theme at 10AM, dark theme at 4PM"
(let ((now (decode-time))
(light-theme (nth (random (length light-theme-list)) light-theme-list))
(dark-theme (nth (random (length dark-theme-list)) dark-theme-list)))
(if (and (>= (nth 2 now) 10) (< (nth 2 now) 16))
(load-theme light-theme t)
(load-theme dark-theme t))))
(add-hook 'after-init-hook 'set-theme-at-specific-times)
(run-at-time "10:00am" nil #'set-theme-at-specific-times)
(run-at-time "4:00pm" nil #'set-theme-at-specific-times)
Interactive theme picker
Spawns Helm and allows you to pick, but from the appropriate list for the time of day. First the underlying implementation:
(defun choose-theme-impl (light-theme-list dark-theme-list)
"Choose a theme from the appropriate list based on the current time"
(let* ((now (decode-time))
(themes (if (and (>= (nth 2 now) 10) (< (nth 2 now) 16))
light-theme-list
dark-theme-list))
(theme-names (mapcar 'symbol-name themes))
(theme-name (helm :sources (helm-build-sync-source "Themes"
:candidates theme-names)
:buffer "*helm choose-theme*")))
(intern theme-name)))
Entrypoint
(defun choose-theme ()
"Choose a theme interactively using Helm"
(interactive)
(let ((theme (choose-theme-impl light-theme-list dark-theme-list)))
(load-theme theme t)))
Solaire Mode
Also some visual candy that makes "real" buffers more visible by changing the background color slightly vs e.g. compilation or magit buffers
(use-package solaire-mode)
;; treemacs got redefined as a normal window at some point
(push '(treemacs-window-background-face . solaire-default-face) solaire-mode-remap-alist)
(push '(treemacs-hl-line-face . solaire-hl-line-face) solaire-mode-remap-alist)
(solaire-global-mode +1)
Spacious Padding
More eye candy:
It increases the padding or spacing of frames and windows on demand. The idea with this package is to provide the means to easily toggle between terse and spacious views, depending on the user’s needs.
Don't know if I'll keep this one but I wanted to try it out
(use-package spacious-padding
:hook (after-init spacious-padding-mode))
Doom Modeline
The Doom Emacs project also provides a fancy modeline to go along with their themes.
(use-package doom-modeline
:config (doom-modeline-def-modeline 'main
'(bar matches buffer-info remote-host buffer-position parrot selection-info)
'(misc-info minor-modes checker input-method buffer-encoding major-mode process vcs " "))
:hook (after-init . doom-modeline-mode))
Emoji 🙏
Provided by emojify. Run emojify-download-emoji
;; 🙌 Emoji! 🙌
(use-package emojify
:config
(setq emojify-download-emojis-p t)
(emojify-set-emoji-styles '(unicode))
(add-hook 'after-init-hook #'global-emojify-mode))
Configure Recent File Tracking
Emacs comes with recentf-mode
which helps me remember what I was doing after I restart my session.
;; recent files mode
(recentf-mode 1)
(setq recentf-max-menu-items 25)
(setq recentf-max-saved-items 25)
;; ignore the elpa directory
(add-to-list 'recentf-exclude
"elpa/*")
Install and Configure Projectile
projectile
is a fantastic package that provides all kinds of project context-aware functions for things like:
- running grep, but only inside the project
- compiling the project from the project root without doing anything
- find files within the project, again without having to do anything extra
It's great, it gets installed early, can't live without it. 💘 projectile
(use-package projectile
:delight)
(use-package helm-projectile)
(use-package treemacs-projectile)
(projectile-mode +1)
TODO I've read about something called project.el
The impression that I got was that project.el
is a first-party replacement for Projectile in newer versions of Emacs. I don't know if this is true or not. I should investigate project.el
.
Install and Configure Evil Mode
evil-mode
fundamentally changes Emacs so that while editing all of the modes and keybindings from vim
are present. It's controversial but I think modal editing is brilliant and have been using vim
bindings since the mid-aughts. No going back.
(defun setup-evil ()
"Install and configure evil-mode and related bindings."
(use-package evil
:init
(setq evil-want-keybinding nil)
(setq evil-want-integration t)
:config
(evil-mode 1))
(use-package evil-collection
:after evil
:config
;; don't let evil-collection manage go-mode
;; it is overriding gd
(setq evil-collection-mode-list (delq 'go-mode evil-collection-mode-list))
(evil-collection-init))
;; the evil-collection overrides the worktree binding :(
(general-define-key
:states 'normal
:keymaps 'magit-status-mode-map
"Z" 'magit-worktree)
;; add fd as a remap for esc
(use-package evil-escape
:delight)
(evil-escape-mode 1)
(use-package evil-surround
:config
(global-evil-surround-mode 1))
(use-package undo-tree
:config
(global-undo-tree-mode)
(evil-set-undo-system 'undo-tree)
(setq undo-tree-history-directory-alist '(("." . "~/.emacs.d/undo"))))
;; add some advice to undo-tree-save-history to suppress messages
;; when it saves its backup files
(defun quiet-undo-tree-save-history (undo-tree-save-history &rest args)
(let ((message-log-max nil)
(inhibit-message t))
(apply undo-tree-save-history args)))
(advice-add 'undo-tree-save-history :around 'quiet-undo-tree-save-history)
(setq-default evil-escape-key-sequence "fd")
;; unbind RET since it does the same thing as j and in some
;; modes RET is used for other things, and evil conflicts
(with-eval-after-load 'evil-maps
(define-key evil-motion-state-map (kbd "RET") nil))
)
Install and Configure Keybindings Helper
General provides more consistent and convenient keybindings, especially with evil-mode
.
It's mostly used below in the global keybindings section.
(use-package general
:init
(setup-evil)
:config
(general-evil-setup))
Install and Configure Helm for Command and Control
Helm is a full-featured command and control package that fundamentally alters a number of core Emacs functions, including what appears when you press M-x
(with the way I have it configured, anyway).
(use-package helm
:delight
:config
(use-package helm-descbinds
:config
(helm-descbinds-mode))
(use-package helm-ag)
(global-set-key (kbd "M-x") #'helm-M-x)
(define-key helm-find-files-map "\t" 'helm-execute-persistent-action)
(setq helm-always-two-windows nil)
(setq helm-default-display-buffer-functions '(display-buffer-in-side-window))
(helm-mode 1))
Install and Configure Magit
Magit is an incredible integrated git
UI for Emacs.
(use-package magit)
;; disable the default emacs vc because git is all I use,
;; for I am a simple man
(setq vc-handled-backends nil)
The Magit author publishes an additional package called forge. Forge lets you interact with GitHub and Gitlab from inside of Emacs. There's planned support for Gogs, Gitea, etc.
(use-package forge
:after magit)
Forge has to be configured with something like .authinfo
or preferably authinfo.gpg
. Create a access token through the web UI of GitHub and place on the first line in $HOME/.authinfo
with the following format:
host api.github.com login gigawhitlocks^forge password TOKEN
but obviously replace TOKEN
with the access token. And use .authinfo.gpg
and encrypt it. Don't just use .authinfo
.
Also, I've only tried this with GitHub. But at least in the case of GitHub, once Forge is set up, it adds some niceties like this to the Magit overview. In this case, I'm looking at the history of a project and Forge automatically adds a link to the PR displayed as part of the commit title in history:
git-timemachine
Install and Configure git-timeline
lets you step through the history of a file.
(use-package git-timemachine)
;; This lets git-timemachine's bindings take precedence over evils'
;; (got lucky and happened to find this while looking for the package name, ha!)
;; @see https://bitbucket.org/lyro/evil/issue/511/let-certain-minor-modes-key-bindings
(eval-after-load 'git-timemachine
'(progn
(evil-make-overriding-map git-timemachine-mode-map 'normal)
;; force update evil keymaps after git-timemachine-mode loaded
(add-hook 'git-timemachine-mode-hook #'evil-normalize-keymaps)))
which-key
Install and Configure It can be difficult to to remember and discover all of the available shortcuts in Emacs, so which-key
pops up a special buffer to show you available shortcuts whenever you pause in the middle of a keyboard shortcut for more than a few seconds. It's really lovely.
(use-package which-key
:delight
:init
(which-key-mode)
(which-key-setup-minibuffer))
*compilation*
Colorize ANSI colors in If you run a command through M-x compile
by default Emacs prints ANSI codes literally, but a lot of tools use these for colors and this makes it so Emacs shows colors in the *compilation*
buffer.
(defun ansi ()
;; enable ANSI escape codes in compilation buffer
(use-package ansi-color)
;; slightly modified from
;; https://endlessparentheses.com/ansi-colors-in-the-compilation-buffer-output.html
(defun colorize-compilation ()
"Colorize from `compilation-filter-start' to `point'."
(let ((inhibit-read-only t))
(ansi-color-apply-on-region
compilation-filter-start (point))))
(add-hook 'compilation-filter-hook
#'colorize-compilation))
(ansi)
Scream when compilation is finished
Sometimes when the compile process takes more than a few seconds I change windows and get distracted. This hook plays a file through aplay
(something else that will break on a non-Linux machine) to notify me that compilation is done. I was looking for something like a kitchen timer but I couldn't find one so right now the vendored sound is the Wilhelm Scream.
(defvar isw-should-play-chime nil)
(setq isw-should-play-chime nil)
(defun isw-play-chime (buffer msg)
(if (eq isw-should-play-chime t)
(start-process-shell-command "chime" "*Messages*" "aplay /home/ian/.emacs.d/vendor/chime.wav")))
(add-to-list 'compilation-finish-functions 'isw-play-chime)
A function for toggling the screaming on and off. I love scream-when-finished but sometimes I'm listening to music or something and it gets a little ridiculous.
(defun toggle-screaming ()
(interactive)
(if (eq isw-should-play-chime t)
(progn
(setq isw-should-play-chime nil)
(message "Screaming disabled."))
(progn
(setq isw-should-play-chime t)
(message "Screaming enabled."))))
Configure the Startup Splashscreen
Following Spacemacs's style, I use the emacs-dashboard
project and all-the-icons
to provide an aesthetically pleasing splash screen with useful links to recently used files on launch.
Actually, looking at the project page, the icons don't seem to be working for me. Maybe I need to enable them. I'll investigate later.
;; first disable the default startup screen
(setq inhibit-startup-screen t)
(use-package dashboard
:config
(dashboard-setup-startup-hook)
(setq dashboard-startup-banner 'logo)
(setq dashboard-center-content t)
(setq dashboard-items '((recents . 5)
(bookmarks . 5)
(projects . 5))
)
)
(setq dashboard-set-footer nil)
Install templating tool and default snippets
YASnippet is really cool and allow fast insertion of boilerplate using templates. I've been meaning to use this more. Here are the YASnippet docs.
OK that example maybe isn't the best, but if you have yas-insert-snippet
bound to something and you're inserting something more complex it's.. probably worthwhile. I should use it more. You can also write your own snippets. I should figure that out.
(use-package yasnippet
:delight
:config
(use-package yasnippet-snippets))
Enable yas-mode everywhere
(yas-global-mode 1)
Smooth scrolling, distraction-free mode, and minimap
Extra Packages
Packages with a smaller effect on the experience.
prism colors by indent level
It takes over the color theme and I don't know if I want it on all the time but it's interesting and I want to have it installed so that I can turn it on in certain situations, like editing highly nested YAML, where it might be invaluable. If I can remember to use it :)
(use-package prism)
git-gutter shows unstaged changes in the gutter
(use-package git-gutter
:delight
:config
(global-git-gutter-mode +1))
Highlight the current line
I like to highlight the current line so that it is easy to identify where my cursor is.
(global-hl-line-mode)
(setq global-hl-line-sticky-flag t)
Rainbow delimiters make it easier to identify matching parentheses
(use-package rainbow-delimiters
:config
;; set up rainbow delimiters for Emacs lisp
(add-hook 'emacs-lisp-mode-hook #'rainbow-delimiters-mode)
)
restart-emacs does what it says on the tin
(use-package restart-emacs)
s is a string manipulation utility
I use this for a trim() function far down below. I think it gets pulled in as a dependency anyway, but in any case it provides a bunch of helper functions and stuff. Docs are here.
(use-package s)
a systemd file mode
Just provides syntax highlighting in .unit
files.
(use-package systemd)
Install and Configure Company for Auto-Completion
Great tab-complete and auto-complete with Company Mode.
;; auto-completion
(use-package company
:delight
:config
;; enable it everywhere
(add-hook 'after-init-hook 'global-company-mode)
;; tab complete!
(global-set-key "\t" 'company-complete-common))
;; icons
(use-package company-box
:hook (company-mode . company-box-mode))
;; extra documentation when idling
(use-package company-quickhelp)
(company-quickhelp-mode)
Install and Configure Flycheck for Linting
Flycheck is an on-the-fly checker that hooks into most language backends.
;; linter
(use-package flycheck
:delight
;; enable it everywhere
:init (global-flycheck-mode))
(add-hook 'flycheck-error-list-mode-hook
'visual-line-mode)
exec-path-from-shell
to manage the PATH
Install exec-path-from-shell mirrors PATH in zsh or Bash in macOS or Linux into Emacs so that the PATH in the shell and the PATH when calling commands from Emacs are the same.
(use-package exec-path-from-shell
:config
(exec-path-from-shell-initialize))
ace-window provides an ace-jump experience for switching windows
(use-package ace-window)
Install a mode for drawing indentation guides
This mode adds subtle coloration to indentation whitespace for whitespace-delimited languages like YAML where sometimes it can be difficult to see the nesting level of a given headline in deeply-nested configuration.
(use-package highlight-indent-guides)
Quick buffer switcher
PC style quick buffer switcher for Emacs
This switches Emacs buffers according to most-recently-used/least-recently-used order using C-tab and C-S-tab keys. It is similar to window or tab switchers that are available in PC desktop environments or applications.
Bound by default to C-<TAB>
and C-S-<TAB>
, I have decided that these are sane defaults. Just install this and turn it on.
(use-package pc-bufsw)
(pc-bufsw)
Writeable grep mode with ack
Writable grep mode allows you to edit the results from running grep on a project and easily save changes back to all of the original files
(use-package ack)
(use-package wgrep-ack)
Better help buffers
(use-package helpful)
(global-set-key (kbd "C-h f") #'helpful-callable)
(global-set-key (kbd "C-h v") #'helpful-variable)
(global-set-key (kbd "C-h k") #'helpful-key)
Quickly jump around buffers
(use-package ace-jump-mode)
TODO Workaround for emacs-sqlite until 29.1
I keep getting a warning about this, and it's annoying because I'm not ready to upgrade to 29.1 but I have to add this temporary configuration setting
(use-package sqlite3)
Dumb jump
Dumb jump provides an interface to grep that does a pretty good job of finding definitions when a smarter backend like LSP is not available. This registers it as a backend for XREF.
(use-package dumb-jump)
(add-hook 'xref-backend-functions #'dumb-jump-xref-activate)
(setq xref-show-definitions-function #'xref-show-definitions-completing-read)
Font
The FiraCode font is a programming-focused font with ligatures that looks nice and has a open license so I'm standardizing my editor configuration on that font
FiraCode Font Installation Script
Installing fonts is always a pain so I'm going to use a variation of the installation script that the FireCode devs provide under their manual installation guide. This should be Linux-distribution agnostic, even though the font can be installed as a system package with on all of my systems on 2022-02-19 Sat with just
sudo apt install fonts-firacode
because I don't intend to use Ubuntu as my only system forever. I just happen to be on Ubuntu on 2022-02-19 Sat.
But first, I want to be able to run this script every time Emacs starts, but only have the script actually do anything if the font is not already installed.
This guard will check to see if there's any font with 'fira' in it (case insensitive) and if so, just exits the script. This will happen on most executions.
set -eo pipefail
[[ $(fc-list | grep -i fira) != "" ]] && exit 0
Now here's the standard installation script
fonts_dir="${HOME}/.local/share/fonts"
if [ ! -d "${fonts_dir}" ]; then
mkdir -p "${fonts_dir}"
fi
version=5.2
zip=Fira_Code_v${version}.zip
curl --fail --location --show-error https://github.com/tonsky/FiraCode/releases/download/${version}/${zip} --output ${zip}
unzip -o -q -d ${fonts_dir} ${zip}
rm ${zip}
# for now we need the Symbols font, too
zip=FiraCode-Regular-Symbol.zip
curl --fail --location --show-error https://github.com/tonsky/FiraCode/files/412440/${zip} --output ${zip}
unzip -o -q -d ${fonts_dir} ${zip}
rm ${zip}
fc-cache -f
This installation script was sourced from https://github.com/tonsky/FiraCode/wiki/Linux-instructions#installing-with-a-package-manager
Enable FiraCode Font
Calling the script from above will install the font
(shell-command "chmod +x ~/.emacs.d/install-firacode-font.bash")
(shell-command "~/.emacs.d/install-firacode-font.bash")
Enable it
(add-to-list 'default-frame-alist '(font . "Fira Code-10"))
(set-frame-font "Fira Code-10" nil t)
Configure FiraCode special features
FiraCode offers ligatures for programming symbols, which is cool.
(use-package ligature
:load-path "./vendor/"
:config
;; Enable the "www" ligature in every possible major mode
(ligature-set-ligatures 't '("www"))
;; Enable traditional ligature support in eww-mode, if the
;; `variable-pitch' face supports it
(ligature-set-ligatures 'eww-mode '("ff" "fi" "ffi"))
;; ;; Enable ligatures in programming modes
(ligature-set-ligatures 'prog-mode '("www" "**" "***" "**/" "*>" "*/" "\\\\" "\\\\\\" "{-"
":::" ":=" "!!" "!=" "!==" "-}" "----" "-->" "->" "->>"
"-<" "-<<" "-~" "#{" "#[" "##" "###" "####" "#(" "#?" "#_"
"#_(" ".-" ".=" ".." "..<" "..." "?=" "??" ";;" "/*" "/**"
"/=" "/==" "/>" "//" "///" "&&" "||" "||=" "|=" "|>" "^=" "$>"
"++" "+++" "+>" "=:=" "==" "===" "==>" "=>" "=>>" "<="
"=<<" "=/=" ">-" ">=" ">=>" ">>" ">>-" ">>=" ">>>" "<*"
"<*>" "<|" "<|>" "<$" "<$>" "<!--" "<-" "<--" "<->" "<+"
"<+>" "<=" "<==" "<=>" "<=<" "<>" "<<" "<<-" "<<=" "<<<"
"<~" "<~~" "</" "</>" "~@" "~-" "~>" "~~" "~~>" "%%"))
;; disabled combinations that could be ligatures
;; "::"
(global-ligature-mode 't))
Language Configuration
This section contains all of the IDE-like features in my configuration, centered around LSP (lsp-mode) and DAP, at least for today.
Language Server Protocol (LSP)
LSP provides a generic interface for text editors to talk to various language servers on the backend. A few languages utilize LSP so it gets configured before the language-specific section.
(use-package lsp-mode
:init
;; use flycheck
(setq lsp-prefer-flymake nil)
(setq lsp-headerline-breadcrumb-enable nil))
;; treemacs integration
(use-package lsp-treemacs)
;; the UI
(use-package lsp-ui)
;; add a longer delay to the help mouseover
(setq lsp-ui-doc-delay 1)
;; linking breaks treemacs
;; also it's annoying
(setq lsp-enable-links nil)
;; helm integration
(use-package helm-lsp)
(setq lsp-eldoc-enable-hover t)
(setq lsp-ui-doc-enable t)
(setq lsp-ui-doc-include-signature t)
(setq lsp-ui-doc-position 'at-point)
(setq lsp-ui-doc-use-childframe t)
(setq lsp-ui-doc-use-webkit nil)
(setq lsp-lens-enable nil)
(general-define-key
:states 'normal
:keymaps 'prog-mode-map
",d" 'lsp-describe-thing-at-point
",gg" 'lsp-find-definition
",gt" 'lsp-find-type-definition
",i" 'lsp-find-implementation
",n" 'lsp-rename
",r" 'lsp-ui-peek-find-references
",R" 'lsp-find-references
",x" 'lsp-execute-code-action
",lsp" 'lsp-workspace-restart
"gd" 'lsp-find-definition
)
Fix background color of lsp-ui-doc in various themes
By default, for some reason, lsp-ui-doc chooses an ugly background color that looks bad and doesn't match the background surrounding most of the text.
I had to edit a few faces with Customize. Some notes:
-
By default, the background color is interrupted by a mismatch with
markdown-code-face
which doesn't matchlsp-ui-doc-background
-
Thus,
lsp-ui-doc-background
is set viaM-x customize-face
to inherit from (match)markdown-code-face
and saved in.emacs-custom.el
Tree Sitter
Tree-sitter reads the AST to provide better syntax highlighting
(use-package tree-sitter
:diminish)
(use-package tree-sitter-langs)
(add-hook 'tree-sitter-after-on-hook #'tree-sitter-hl-mode)
(global-tree-sitter-mode)
(use-package tree-sitter-langs
:ensure t
:after tree-sitter
:config
(tree-sitter-require 'tsx)
(add-to-list 'tree-sitter-major-mode-language-alist '(web-mode . tsx)))
YAML
(use-package yaml-mode)
(add-hook 'yaml-mode-hook 'highlight-indent-guides-mode)
;;(add-hook 'yaml-mode-hook 'origami-mode)
(general-define-key
:states 'normal
:keymaps 'yaml-mode-map
"zo" 'origami-open-node-recursively
"zO" 'origami-open-all-nodes
"zc" 'origami-close-node-recursively)
Rego
whatever that is
(use-package rego-mode)
Markdown
(use-package markdown-mode
:ensure t
:mode (("README\\.md\\'" . gfm-mode)
("\\.md\\'" . gfm-mode)
("\\.markdown\\'" . gfm-mode)))
(add-hook 'markdown-mode-hook 'visual-line-mode)
(add-hook 'markdown-mode-hook 'variable-pitch-mode)
;; this can go here because it affects Markdown's live preview mode
;; but I should consider putting it somewhere more general maybe?
(add-hook 'eww-mode-hook 'visual-line-mode)
;; show code blocks w/ monospace font
(set-face-attribute 'markdown-code-face nil :inherit 'fixed-pitch)
Docker
(use-package dockerfile-mode)
(add-to-list 'auto-mode-alist '("Dockerfile\\'" . dockerfile-mode))
(put 'dockerfile-image-name 'safe-local-variable #'stringp)
Python
auto-virtualenv
looks in $WORKON_HOME
for virtualenvs, and then I can run M-x pyvenv-workon RET project RET
to choose my virtualenv for project
, found in $WORKON_HOME
, or a symlink anyway.
(use-package auto-virtualenv)
(add-hook 'python-mode-hook 'auto-virtualenv-set-virtualenv)
(setenv "WORKON_HOME" "~/.virtualenvs")
So the convention for use is:
- Create a virtualenv as usual for the project
- Symlink it inside ~/.virtualenvs
M-x pyvenv-workon
Go
Go is my primary language so it's my most dynamic and complicated configuration.
Dependencies
Go support requires some dependencies. I will try to list them all here. Stuff I have installed has some overlap because of the in-progress move to LSP, but I'll prune it later.
-
First,
go
itself must be installed, install however, and avalailable on thePATH
. -
gopls
, the language server for LSP mentioned above https://github.com/golang/tools/blob/master/gopls/doc/user.md. I have been just running this off ofmaster
so I can experience all the latestbugsfeatures, so clone the gopls project (TODO find the url for it and put a link here) andgo install
it. After you're donegopls
should also be on thePATH
. Directions for configuringgopls
through this file are found here. -
golint
has to be installed independently
$ go get https://github.com/golang/lint
Nothing to do with Emacs, but eg
also looks really cool:
$ go get golang.org/x/tools/cmd/eg
golangci-lint
is a meta linter that calls a bunch of 3rd party linters (configurable) and replaces the old one that used to freeze my computer.go-metalinter
, I think, is what it was called. Anyway, it used to crash my computer and apparently that was a common experience. Anywaygolangci-lint
must be installed independently, too:
# install it into ./bin/
$ curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v1.23.6
Initial Setup
(defun set-gopls-lib-dirs ()
"Add $GOPATH/pkg/mod to the 'library path'."
;; stops lsp from continually asking if Go projects should be imported
(setq lsp-clients-go-library-directories
(list
"/usr"
(concat (getenv "GOPATH") "/pkg/mod"))))
(use-package go-mode
:hook ((go-mode . lsp-deferred)
(go-mode . set-gopls-lib-dirs)
(go-mode . yas-minor-mode))
:config
;; fixes ctrl-o after goto-definition by telling evil that godef-jump jumps
;; I don't believe I need to do this anymore, as I use lsp instead of godef now
(evil-add-command-properties #'godef-jump :jump t))
;; enable golangci-lint to work with flycheck
(use-package flycheck-golangci-lint
:hook (go-mode . flycheck-golangci-lint-setup))
Package and Configuration for Executing Tests
(use-package gotest)
(advice-add 'go-test-current-project :before #'projectile-save-project-buffers)
(advice-add 'go-test-current-test :before #'projectile-save-project-buffers)
(add-hook 'go-test-mode-hook 'visual-line-mode)
REPL
Gore provides a REPL and gorepl-mode lets you use it from Emacs. In order to use the REPL from Emacs, you must first install Gore:
go get -u github.com/motemen/gore/cmd/gore
Gore also uses gocode for code completion, so install that (even though Emacs uses go-pls for the same).
go get -u github.com/mdempsky/gocode
Once that's done gorepl-mode
is ready to be installed:
(use-package gorepl-mode)
Interactive debugger
I got jealous of a coworker with an IDE who apparently has an interactive debugger, so I got dap-mode
working 🙂
-
Installation and Configuration
Install
dap-mode
anddap-go
.dap-mode
is probably useful for other languages so at some point I will want to refactor it out and install it alongside LSP, but keepdap-go
here. Probably. But this works for now, and who knows, maybe debugging Go is really all I care about.(use-package dap-mode) (require 'dap-dlv-go) (dap-mode 0) (dap-ui-mode 0) (dap-ui-controls-mode 0) (tooltip-mode 1) (setq dap-ui-variable-length 100)
-
On first install
Theoretically you should be able to run this
M-x dap-go-setup
But it is subject to rate-limiting so I just checked in the results of calling this under
.extension
. It's all MIT-licensed so this is fine.
-
-
Use
-
When debugging a new executable for the first time
Run this command
M-x dap-debug-edit-template
and save the
(dap-register-debug-template )
call that is generated.. somewhere alongside the code hopefully. I'll come up with some convention for storing these. Maybe dir-locals (SPC p E
) -
Each time when ready to start debugging
Start debugging by running:
M-x dap-debug
Click in the margins to set breakpoints with
dap-ui-mode
enabled (🙌)
-
Mode-Specific Keybindings
(general-define-key
:states 'normal
:keymaps 'go-mode-map
",a" 'go-import-add
",d" 'lsp-describe-thing-at-point
"gd" 'lsp-find-definition
",gt" 'lsp-find-type-definition
",i" 'lsp-find-implementation
",n" 'lsp-rename
",r" 'lsp-ui-peek-find-references
",R" 'lsp-find-references
",tp" 'go-test-current-project
",tt" 'go-test-current-test
",tf" 'go-test-current-file
",x" 'lsp-execute-code-action
",lsp" 'lsp-workspace-restart
"gd" 'lsp-find-definition
;; using the ,c namespace for repl and debug stuff to follow the C-c
;; convention found in other places in Emacs
",cc" 'dap-debug
",cr" 'gorepl-run
",cg" 'gorepl-run-load-current-file
",cx" 'gorepl-eval-region
",cl" 'gorepl-eval-line
)
(autoload 'go-mode "go-mode" nil t)
(add-to-list 'auto-mode-alist '("\\.go\\'" . go-mode))
Hooks
;; disable "Organize Imports" warning that never goes away
(add-hook 'go-mode-hook
(lambda ()
;; Go likes origami-mode
;; (origami-mode)
;; lsp ui sideline code actions are annoying in Go
(setq-local lsp-ui-sideline-show-code-actions nil)))
;; sets the visual tab width to 2 spaces per tab in Go buffers
(add-hook 'go-mode-hook (lambda ()
(set (make-local-variable 'tab-width) 2)))
(defun lsp-go-install-save-hooks ()
(add-hook 'before-save-hook #'lsp-format-buffer t t)
(add-hook 'before-save-hook #'lsp-organize-imports t t))
(add-hook 'go-mode-hook #'lsp-go-install-save-hooks)
(setq lsp-file-watch-threshold 5000)
Exclude a certain folder from LSP projects
Certain projects use a gopath folder inside the project root and this confuses LSP/gopls.
(with-eval-after-load 'lsp-mode
(add-to-list 'lsp-file-watch-ignored-directories "[/\\\\]\\.GOPATH\\'"))
Incidentally, that regex up there is a fucking nightmare and Emacs Lisp should be ashamed. That or maybe there's some secret way to do it so there isn't backslash hell. But holy crap that is a horrible line of code. I think we can all agree with that.
Rust
To install the Rust language server:
- Install
rustup
. - Run
rustup component add rls rust-analysis rust-src
.
(use-package rust-mode
:mode (("\\.rs$" . rust-mode))
:hook ((rust-mode . lsp-deferred)))
(general-define-key
:states 'normal
:keymaps 'rust-mode-map
",d" 'lsp-describe-thing-at-point
",gg" 'lsp-find-definition
",gt" 'lsp-find-type-definition
",i" 'lsp-find-implementation
",n" 'lsp-rename
",r" 'lsp-find-references
",x" 'lsp-execute-code-action
",lsp" 'lsp-workspace-restart
"gd" 'lsp-find-definition
)
(defun lsp-rust-install-save-hooks ()
(add-hook 'before-save-hook #'lsp-format-buffer t t))
(add-hook 'rust-mode-hook #'lsp-rust-install-save-hooks)
Web
After some amount of searching and fumbling about I have discovered web-mode
which appears to be the one-stop-shop solution for all of your HTML and browser-related needs. It handles a whole slew of web-related languages and templating formats and plays nicely with LSP. It's also the only package that I could find that supported .tsx
files at all.
So yay for web-mode
!
(use-package web-mode
:mode (("\\.html$" . web-mode)
("\\.js$" . web-mode)
("\\.jsx$" . web-mode)
("\\.ts$" . web-mode)
("\\.tsx$" . web-mode)
("\\.css$" . web-mode)
("\\.svelte$" . web-mode))
:hook
((web-mode . lsp-deferred))
:config
(setq web-mode-enable-css-colorization t)
(setq web-mode-enable-auto-pairing t)
(setq web-mode-enable-auto-quoting nil))
enable jsx mode for all .js and .jsx files
If working on projects that do not use JSX, might need to move this to a project-specific config somewhere.
For now though, this is sufficient for me
(setq web-mode-content-types-alist
'(("jsx" . "\\.js[x]?\\'")))
Thanks to https://prathamesh.tech/2015/06/20/configuring-web-mode-with-jsx/
Setting highlighting for special template modes
;; web-mode can provide syntax highlighting for many template
;; engines, but it can't detect the right one if the template uses a generic ending.
;; If a project uses a generic ending for its templates, such
;; as .html, add it below. It would be more elegant to handle this by
;; setting this variable in .dir-locals.el for each project,
;; unfortunately due to this https://github.com/fxbois//issues/799 that
;; is not possible :(
;;(setq web-mode-engines-alist '(
;; ("go" . ".*example_project_dir/.*\\.html\\'")
;; add more projects here..
;; ))
JSON
(use-package json-mode
:mode (("\\.json$" . json-mode ))
)
(add-hook 'json-mode-hook 'highlight-indent-guides-mode)
Default Keybindings C-c C-f: format the region/buffer with json-reformat (https://github.com/gongo/json-reformat) C-c C-p: display a path to the object at point with json-snatcher (https://github.com/Sterlingg/json-snatcher) C-c P: copy a path to the object at point to the kill ring with json-snatcher (https://github.com/Sterlingg/json-snatcher) C-c C-t: Toggle between true and false at point C-c C-k: Replace the sexp at point with null C-c C-i: Increment the number at point C-c C-d: Decrement the number at point
Shell
TODO I don't know if this still works
Shell mode is pretty good vanilla, but I prefer to use spaces rather than tabs for indents with languages like Bash because they just tend to format more reliably. Tabs are .. theoretically more flexible, so maybe I can come back to consider this.
But for now, disable indent-tabs-mode
in shell script editing mode because I have been observing behavior from whitespace-cleanup-mode
that when indent-tabs-mode
is t
it will change 4 spaces to a tab even if there are other spaces being used for indent, even on the same line, and regardless as to the never-ending debate about spaces and tabs and all that, everyone can agree that 1) mixing spaces and tabs is terrible and 2) your editor shouldn't be mixing spaces and tabs automatically at pre-save time.
(add-hook 'sh-mode-hook
(lambda ()
(defvar-local indent-tabs-mode nil)))
Zsh
I also write Zsh scripts and Emacs doesn't detect automatically I think
(add-to-list 'auto-mode-alist '("\\.zsh\\'" . sh-mode))
Salt
(use-package salt-mode)
(add-hook 'salt-mode-hook
(lambda ()
(flyspell-mode 1)))
(add-hook 'salt-mode-hook 'highlight-indent-guides-mode)
(general-define-key
:states 'normal
:keymaps 'sh-mode-map
",c" (general-simulate-key "C-x h C-M-x")
)
Vyper
(use-package vyper-mode)
Elixir
(use-package elixir-mode
:hook
((elixir-mode . lsp-deferred))
)
;; Create a buffer-local hook to run elixir-format on save, only when we enable elixir-mode.
(add-hook 'elixir-mode-hook
(lambda () (add-hook 'before-save-hook 'elixir-format nil t)))
SQL
SQL support is pretty good out of the box but Emacs strangely doesn't indent SQL by default. This package fixes that.
(use-package sql-indent)
SQL doesn't – as far as I'm aware, and I'm not taking the time to look harder at the moment anyway – have an LSP backend (probably doesn't help that there are multiple dialects of SQL so I'd have to find one for PG or SQLite or whatever I'm using that day) so lsp-find-definition
doesn't work. Below I set gd
in evil-mode back to the default (evil-goto-definition
) and add dumb jump as a backend to xref so that it can be used for finding SQL function definitions. Works pretty well but I haven't tested to see if the new hook & the new xref-show-definitions-function values will affect non-SQL modes negatively.
It might be that this essentially sets up dumb-jump to work anywhere that I've not mapped gd
to lsp-find-function
and that's fine but I will have to move this code elsewhere so it makes more sense, if that is the case. A task for another day.
(general-define-key
:states 'normal
:keymaps 'sql-mode-map
"gd" 'evil-goto-definition
)
Emacs Lisp
I don't have any custom configuration for Emacs Lisp yet, but I am going to use this space to collect tools and resources that might become useful in the future, and which I may install.
A collection of development modes and utilities
https://github.com/p3r7/awesome-elisp
editing s-exps
https://github.com/p3r7/awesome-elisp#lispy https://github.com/abo-abo/lispy
Robot
ugh, Robot test framework files – I hate Robot and think it's fucking trash, but sometimes I have to edit it anyway
(use-package robot-mode)
Adaptive Wrap and Visual Line Mode
Here I've done some black magic fuckery for a few modes. Heathens in modern languages and also some other prose modes don't wrap their long lines at 80 characters like God intended so instead of using visual-column-mode which I think does something similar but probably would've been easier, I've defined an abomination of a combination of visual-line-mode
(built-in) and adaptive-wrap-prefix-mode to dynamically (visually) wrap and indent long lines in languages like Go with no line length limit so they look nice on my screen at any window width and don't change the underlying file — and it's actually pretty cool.
(use-package adaptive-wrap
:config
(setq-default adaptive-wrap-extra-indent 2)
(defun adaptive-and-visual-line-mode (hook)
(add-hook hook (lambda ()
(progn
(visual-line-mode)
(adaptive-wrap-prefix-mode)))))
(mapc 'adaptive-and-visual-line-mode
(list
'markdown-mode
'go-mode-hook
'js2-mode-hook
'yaml-mode-hook
'rjsx-mode-hook))
(add-hook 'compilation-mode-hook
#'adaptive-wrap-prefix-mode)
(setq compilation-scroll-output t))
Global Keybindings
Helper Functions
(defun find-initfile ()
"Open main config file."
(interactive)
(find-file "~/.emacs.d/ian.org"))
(defun find-initfile-other-frame ()
"Open main config file in a new frame."
(interactive)
(find-file-other-frame "~/.emacs.d/ian.org"))
(defun reload-initfile ()
"Reload the main config file."
(interactive)
(org-babel-tangle "~/.emacs.d/ian.org")
(byte-compile-file "~/.emacs.d/ian.el"))
(defun close-client-frame ()
"Exit emacsclient."
(interactive)
(server-edit "Done"))
(defun last-window ()
"Switch to the last window."
(interactive)
(other-window -1 t))
(defun toggle-line-numbers-rel-abs ()
"Toggles line numbers between relative and absolute numbering"
(interactive)
(if (equal display-line-numbers-type 'relative)
(setq display-line-numbers-type 'absolute)
(setq display-line-numbers-type 'relative))
(if (equal display-line-numbers-mode t)
(progn
(display-line-numbers-mode -1)
(display-line-numbers-mode))))
(defun random-theme (light-theme-list dark-theme-list)
"Choose a random theme from the appropriate list based on the current time"
(let* ((now (decode-time))
(themes (if (and (>= (nth 2 now) 10) (< (nth 2 now) 15))
light-theme-list
dark-theme-list)))
(nth (random (length themes)) themes)))
(defun load-next-favorite-theme ()
"Switch to a random theme appropriate for the current time."
(interactive)
(let ((theme (random-theme light-theme-list dark-theme-list)))
(load-theme theme t)
(message "Switched to theme: %s" theme)))
Main Global Keymap
These are all under SPACE, following the Spacemacs pattern. Yeah, my configuration is a little of Spacemacs, a little of Doom, and a little of whatever I feel inspired by.
These keybindings are probably the most opinionated part of my configuration. They're shortcuts I can remember, logically or not.
;; define the spacebar as the global leader key, following the
;; Spacemacs pattern, which I've been using since 2014
(general-create-definer my-leader-def
:prefix "SPC")
;; define SPC m for minor mode keys, even though I use , sometimes
(general-create-definer my-local-leader-def
:prefix "SPC m")
;; global keybindings with LEADER
(my-leader-def 'normal 'override
"aa" 'ace-jump-mode
"ag" 'org-agenda
"bb" 'helm-buffers-list
"TAB" #'switch-to-prev-buffer
"br" 'revert-buffer
"bd" 'evil-delete-buffer
"ds" (defun ian-desktop-save ()
(interactive)
(desktop-save "~/desktop-saves"))
"dr" (defun ian-desktop-read ()
(interactive)
(desktop-read "~/desktop-saves"))
"cc" 'projectile-compile-project
"ec" 'flycheck-clear
"el" 'flycheck-list-errors
"en" 'flycheck-next-error
"ep" 'flycheck-previous-error
"Fm" 'make-frame
"Ff" 'toggle-frame-fullscreen
"ff" 'helm-find-files
"fr" 'helm-recentf
"fd" 'dired
"fed" 'find-initfile
"feD" 'find-initfile-other-frame
"feR" 'reload-initfile
"gb" 'magit-blame
"gs" 'magit-status
"gg" 'magit
"gt" 'git-timemachine
"gd" 'magit-diff
"go" 'browse-at-remote
"gi" 'helm-imenu
"h" 'hyperbole
"jj" 'bookmark-jump
"js" 'bookmark-set
"jo" 'org-babel-tangle-jump-to-org
"ic" 'insert-char
"is" 'yas-insert-snippet
"n" '(:keymap narrow-map)
"oo" 'browse-url-at-point
"p" 'projectile-command-map
"pf" 'helm-projectile-find-file
"p!" 'projectile-run-async-shell-command-in-root
"si" 'yas-insert-snippet
"sn" 'yas-new-snippet
"sp" 'helm-projectile-ag
"qq" 'save-buffers-kill-terminal
"qr" 'restart-emacs
"qz" 'delete-frame
"ta" 'treemacs-add-project-to-workspace
"thi" (defun ian-theme-information ()
"Display the last applied theme."
(interactive)
(let ((last-theme (car (reverse custom-enabled-themes))))
(if last-theme
(message "Last applied theme: %s" last-theme)
(message "No themes are currently enabled."))))
"thr" 'load-random-theme
"thl" (defun ian-load-light-theme ()
(interactive)
(load-theme
(nth
(random
(length light-theme-list)) light-theme-list)))
"thd" (defun ian-load-dark-theme ()
(interactive)
(load-theme
(nth
(random
(length
dark-theme-list)) dark-theme-list)))
"thh" 'choose-theme
"thc" 'load-theme
"thn" 'load-next-favorite-theme
"tnn" 'display-line-numbers-mode
"tnt" 'toggle-line-numbers-rel-abs
"tr" 'treemacs-select-window
"ts" 'toggle-screaming
"tt" 'toggle-transparency
"tp" (defun ian-toggle-prism () (interactive) (prism-mode 'toggle))
"tw" 'whitespace-mode
"w-" 'split-window-below
"w/" 'split-window-right
"wb" 'last-window
"wj" 'evil-window-down
"wk" 'evil-window-up
"wh" 'evil-window-left
"wl" 'evil-window-right
"wd" 'delete-window
"wD" 'delete-other-windows
"ww" 'ace-window
"wo" 'other-window
"w=" 'balance-windows
"W" '(:keymap evil-window-map)
"SPC" 'helm-M-x
)
;; global VISUAL mode map
(general-vmap
";" 'comment-or-uncomment-region)
;; top right button on my trackball is equivalent to click (select) +
;; RET (open) on files in Treemacs
(general-define-key
:keymaps 'treemacs-mode-map
"<mouse-8>" 'treemacs-RET-action)
Org Mode Settings
Some default evil bindings
(use-package evil-org)
Image drag-and-drop for org-mode
(use-package org-download)
Autocomplete for Org blocks (like source blocks)
(use-package company-org-block) ;; TODO configuration
JIRA support in Org
(use-package ox-jira)
Install some tools for archiving web content into Org
(use-package org-web-tools)
(setq org-export-coding-system 'utf-8)
;; Fontify the whole line for headings (with a background color).
(setq org-fontify-whole-heading-line t)
;; disable the weird default editing window layout in org-mode
;; instead, just replace the current window with the editing one..
(setq org-src-window-setup 'current-window)
;; indent and wrap long lines in Org
(add-hook 'org-mode-hook 'org-indent-mode)
(add-hook 'org-mode-hook 'visual-line-mode)
;; enable execution of languages from Babel
(org-babel-do-load-languages 'org-babel-load-languages
'(
(shell . t)
)
)
(my-local-leader-def
:states 'normal
:keymaps 'org-mode-map
"y" 'org-store-link
"i" 'org-toggle-inline-images
"p" 'org-insert-link
"x" 'org-babel-execute-src-block
"s" 'org-insert-structure-template
"e" 'org-edit-src-code
"t" 'org-babel-tangle
"o" 'org-export-dispatch
)
(general-define-key
:states 'normal
:keymaps 'org-mode-map
"TAB" 'evil-toggle-fold)
;; github-flavored markdown
(use-package ox-gfm)
;; htmlize prints the current buffer or file, as it would appear in
;; Emacs, but in HTML! It's super cool and TODO I need to move this
;; use-package statement somewhere I can talk about htmlize outside of
;; a comment
(use-package htmlize)
;; enable markdown export
(eval-after-load "org"
(progn
'(require 'ox-md nil t)
'(require 'ox-gfm nil t)))
;; todo states
(setq org-todo-keywords
'((sequence "TODO(t)" "|" "IN PROGRESS(p)" "|" "DONE(d)" "|" "STUCK(s)" "|" "WAITING(w)")
(sequence "OPEN(o)" "|" "INVESTIGATE(v)" "|" "IMPLEMENT(i)" "|" "REVIEW(r)" "|" "MERGED(m)" "|" "RELEASED(d)" "|" "ABANDONED(a)")
(sequence "QUESTION(q)" "|" "ANSWERED(a)")))
;; todo faces
(setq org-todo-keyword-faces
'(("IN PROGRESS" . org-warning) ("STUCK" . org-done)
("WAITING" . org-warning)))
;; enable org-protocol
(require 'org-protocol)
;; enter follows links.. how was this not a default?
(setq org-return-follows-link t)
Use a variable-pitch font in Org-Mode
Org is mostly prose and prose should be read in a variable-pitch font where possible. This changes fonts in Org to be variable-pitch where it makes sense
(add-hook 'org-mode-hook 'variable-pitch-mode)
Inside of code blocks I want a fixed-pitch font
(defun ian-org-fixed-pitch ()
"Fix fixed pitch text in Org Mode"
(set-face-attribute 'org-table nil :inherit 'fixed-pitch)
(set-face-attribute 'org-block nil :inherit 'fixed-pitch))
(add-hook 'org-mode-hook 'ian-org-fixed-pitch)
Useful anchors in HTML export
This is taken from github.com/alphapapa's Unpackaged.el collection, unmodified.
(eval-when-compile
(require 'easy-mmode)
(require 'ox))
(define-minor-mode unpackaged/org-export-html-with-useful-ids-mode
"Attempt to export Org as HTML with useful link IDs.
Instead of random IDs like \"#orga1b2c3\", use heading titles,
made unique when necessary."
:global t
(if unpackaged/org-export-html-with-useful-ids-mode
(advice-add #'org-export-get-reference :override #'unpackaged/org-export-get-reference)
(advice-remove #'org-export-get-reference #'unpackaged/org-export-get-reference)))
(defun unpackaged/org-export-get-reference (datum info)
"Like `org-export-get-reference', except uses heading titles instead of random numbers."
(let ((cache (plist-get info :internal-references)))
(or (car (rassq datum cache))
(let* ((crossrefs (plist-get info :crossrefs))
(cells (org-export-search-cells datum))
;; Preserve any pre-existing association between
;; a search cell and a reference, i.e., when some
;; previously published document referenced a location
;; within current file (see
;; `org-publish-resolve-external-link').
;;
;; However, there is no guarantee that search cells are
;; unique, e.g., there might be duplicate custom ID or
;; two headings with the same title in the file.
;;
;; As a consequence, before re-using any reference to
;; an element or object, we check that it doesn't refer
;; to a previous element or object.
(new (or (cl-some
(lambda (cell)
(let ((stored (cdr (assoc cell crossrefs))))
(when stored
(let ((old (org-export-format-reference stored)))
(and (not (assoc old cache)) stored)))))
cells)
(when (org-element-property :raw-value datum)
;; Heading with a title
(unpackaged/org-export-new-title-reference datum cache))
;; NOTE: This probably breaks some Org Export
;; feature, but if it does what I need, fine.
(org-export-format-reference
(org-export-new-reference cache))))
(reference-string new))
;; Cache contains both data already associated to
;; a reference and in-use internal references, so as to make
;; unique references.
(dolist (cell cells) (push (cons cell new) cache))
;; Retain a direct association between reference string and
;; DATUM since (1) not every object or element can be given
;; a search cell (2) it permits quick lookup.
(push (cons reference-string datum) cache)
(plist-put info :internal-references cache)
reference-string))))
(defun unpackaged/org-export-new-title-reference (datum cache)
"Return new reference for DATUM that is unique in CACHE."
(cl-macrolet ((inc-suffixf (place)
`(progn
(string-match (rx bos
(minimal-match (group (1+ anything)))
(optional "--" (group (1+ digit)))
eos)
,place)
;; HACK: `s1' instead of a gensym.
(-let* (((s1 suffix) (list (match-string 1 ,place)
(match-string 2 ,place)))
(suffix (if suffix
(string-to-number suffix)
0)))
(setf ,place (format "%s--%s" s1 (cl-incf suffix)))))))
(let* ((title (org-element-property :raw-value datum))
(ref (url-hexify-string (substring-no-properties title)))
(parent (org-element-property :parent datum)))
(while (--any (equal ref (car it))
cache)
;; Title not unique: make it so.
(if parent
;; Append ancestor title.
(setf title (concat (org-element-property :raw-value parent)
"--" title)
ref (url-hexify-string (substring-no-properties title))
parent (org-element-property :parent parent))
;; No more ancestors: add and increment a number.
(inc-suffixf ref)))
ref)))
(add-hook 'org-mode-hook 'unpackaged/org-export-html-with-useful-ids-mode)
Miscellaneous standalone global configuration changes
Allow local variables marked safe to be applied without notice
(setq enable-local-variables :safe)
Opening the Remote Repo in the Browser from Emacs
browse-at-remote.el solves this
(use-package browse-at-remote)
Opening Sources in Emacs from the Browser
https://orgmode.org/worg/org-contrib/org-protocol.html
First use this .desktop
file to register a handler for the new protocol scheme:
[Desktop Entry]
Name=org-protocol
Comment=Intercept calls from emacsclient to trigger custom actions
Categories=Other;
Keywords=org-protocol;
Icon=emacs
Type=Application
Exec=/home/ian/bin/org-protocol %u
#Exec=emacsclient -- %u
Terminal=false
StartupWMClass=Emacs
MimeType=x-scheme-handler/org-protocol;
After tangling that file to its destination, run the following command to update the database:
update-desktop-database ~/.local/share/applications/
Add the custom org-protocol
script to intercept calls from the browser, do any necessary pre-processing, and hand off the corrected input to emacsclient
:
# for some reason the bookmarklet strips a colon, so use sed to remove
# the botched prefix and rebuild it correctly
emacsclient -- org-protocol://open-source://$(echo "$@" | sed 's#org-protocol://open-source//##g') | tee /tmp/xdg-emacsclient
# that's probably a useless call to echo but whatever
For now this is extremely rudimentary and I will improve it as needed.
Manual Steps:
-
The first time, add a button in the browser by creating a bookmarklet containing the following target:
javascript:location.href='org-protocol://open-source://'+encodeURIComponent(location.href)
-
Add an entry to
org-protocol-project-alist
, defined in the local machine's hostname-specific config found inlocal/
. An example can be found on the Worg page above, but here it is again for easy reference:
(setq org-protocol-project-alist
'(("Worg"
:base-url "https://orgmode.org/worg/"
:working-directory "/home/user/worg/"
:online-suffix ".html"
:working-suffix ".org")
("My local Org-notes"
:base-url "http://localhost/org/"
:working-directory "/home/user/org/"
:online-suffix ".php"
:working-suffix ".org")))
N.B. this code block does not get tangled into ian.el
.
-
TODO automate the cloning of unknown repos and addition to this list
I want to be able to press the button on new repos that I haven't cloned yet, and have them dumped to a sane location and then added to the list and opened.
TRAMP settings
Only one setting at the moment: use ssh
instead of scp
when accessing files with ssh:
schemes
(setq tramp-default-method "ssh")
Disable most warnings
Honestly I'm not good enough at Emacs to make sense of most of them anyway
(setq warning-minimum-level :emergency)
Theme Switching Helper
Automatically calls disable-theme on the current theme before loading a new theme! Allows easy theme switching with just M-x load-theme
.
Thanks to https://www.simplify.ba/articles/2016/02/13/loading-and-unloading-emacs-themes/.
(defun load-theme--disable-old-theme (theme &rest args)
"Disable current theme before loading new one."
(mapcar #'disable-theme custom-enabled-themes))
(advice-add 'load-theme :before #'load-theme--disable-old-theme)
Save the current theme to a global variable so it can be referenced later
(defun load-theme--save-new-theme (theme &rest args)
(setq ian-current-theme theme))
(advice-add 'load-theme :before #'load-theme--save-new-theme)
There are a few occasions where the Org fixed-width fonts don't get reapplied correctly. This solves most of them, and eventually I may iterate on it, if the edge cases bother me enough.
(defun ian-restart-org-advice (&rest _args)
(org-mode-restart))
(advice-add 'load-theme :after #'ian-restart-org-advice)
Line Numbers in Programming Buffers
(add-hook 'prog-mode-hook 'display-line-numbers-mode)
(setq display-line-numbers-type 'relative)
Transparency toggle
I definitely lifted this from somewhere but failed to document where I got it :\ Probably from Spacemacs. Thanks, Spacemacs.
(defun toggle-transparency ()
(interactive)
(let ((alpha (frame-parameter nil 'alpha)))
(set-frame-parameter
nil 'alpha
(if (eql (cond ((numberp alpha) alpha)
((numberp (cdr alpha)) (cdr alpha))
;; Also handle undocumented (<active> <inactive>) form.
((numberp (cadr alpha)) (cadr alpha)))
100)
'95 '(100 . 100)))))
Switch to last buffer
This one lifted from https://emacsredux.com/blog/2013/04/28/switch-to-previous-buffer/
TODO: Make this behave like alt-tab in Windows, but for buffers. I think hycontrol
may come in handy (Hyperbole).
(defun er-switch-to-previous-buffer ()
(concat
"Switch to previously open buffer."
"Repeated invocations toggle between the two most recently open buffers.")
(interactive)
(switch-to-buffer (other-buffer (current-buffer) 1)))
Fix Home/End keys
Emacs has weird behavior by default for Home and End and this change makes the behavior "normal" again.
(global-set-key (kbd "<home>") 'move-beginning-of-line)
(global-set-key (kbd "<end>") 'move-end-of-line)
Customize the frame (OS window) title
Taken from StackOverflow, at least for now, which does 90% of what I want and can serve as a future reference of how to customize this aspect of Emacs. This displays the file name and major mode in the OS title bar. Will have to find the documentation that defines the format string passed to frame-title-format
at some point.
(setq-default frame-title-format '("%f [%m]"))
Tweak align-regexp
Configure align-regexp to use spaces instead of tabs. This is mostly for this file. When my keybindings are in two columns and M-x align-regexp
uses tabs, the columns look aligned in Emacs but unaligned on GitHub. Using spaces faces this. This snippet effects that change.
Lifted from StackOverflow:
https://stackoverflow.com/questions/22710040/emacs-align-regexp-with-spaces-instead-of-tabs
(defadvice align-regexp (around align-regexp-with-spaces activate)
(let ((indent-tabs-mode nil))
ad-do-it))
Configure automatic backup/recovery files
I don't like how Emacs puts temp files in the same directory as the file, as this litters the current working directory and makes git branches dirty. These are some tweaks to store those files in /tmp
.
(setq make-backup-files nil)
(setq backup-directory-alist `((".*" . "/tmp/.emacs-saves")))
(setq backup-by-copying t)
(setq delete-old-versions t)
TODO Clean whitespace on save in all modes
I have to actually go in and configure this because the defaults keep giving me fucking heartburn. It keeps messing with the whitespace in files that are none of its business. Maybe I just need to carefully enable it for certain modes? idk, too much magic, no time to look into it right now.
;; (add-hook 'before-save-hook 'whitespace-cleanup)
Autosave
Automatically saves the file when it's been idle for 5 minutes.
;; autosave
(setq auto-save-visited-interval 300)
(auto-save-visited-mode
:diminish
)
Default window size
Just a bigger size that I prefer..
(add-to-list 'default-frame-alist '(width . 128))
(add-to-list 'default-frame-alist '(height . 60))
Unclutter global modeline
Some global minor modes put themselves in the modeline and it gets noisy, so remove them from the modeline.
;; hide some modes that are everywhere
(diminish 'eldoc-mode)
(diminish 'undo-tree-mode)
(diminish 'auto-revert-mode)
(diminish 'evil-collection-unimpaired-mode)
(diminish 'yas-minor-mode-major-mode)
Less annoying bell
Flashes the modeline foreground instead of whatever the horrible default behavior was (I don't even remember).
(setq ring-bell-function
(lambda ()
(let ((orig-fg (face-foreground 'mode-line)))
;; change the flash color here
;; overrides themes :P
;; guess that's one way to do it
(set-face-foreground 'mode-line "#F2804F")
(run-with-idle-timer 0.1 nil
(lambda (fg) (set-face-foreground 'mode-line fg))
orig-fg))))
(from Emacs wiki)
Remove toolbar, scrollbars, and menu
Removes the toolbar and menu bar (file menu, etc) in Emacs because I just use M-x
for everything.
(when (fboundp 'menu-bar-mode) (menu-bar-mode -1))
(when (fboundp 'tool-bar-mode) (tool-bar-mode -1))
(scroll-bar-mode -1)
(defun my/disable-scroll-bars (frame)
(modify-frame-parameters frame
'((vertical-scroll-bars . nil)
(horizontal-scroll-bars . nil))))
(add-hook 'after-make-frame-functions 'my/disable-scroll-bars)
Enable the mouse in the terminal
(xterm-mouse-mode 1)
Disable "nice" names in Customize
I prefer that Customize display the names of variables that I can change in this file, rather than the human-readable names for people who customize their Emacs through M-x customize
(setq custom-unlispify-tag-names nil)
Smart formatting for many languages
;; auto-format different source code files extremely intelligently
;; https://github.com/radian-software/apheleia
;; (use-package apheleia
;; :config
;; (apheleia-global-mode +1))
Add support for browsing Gemini-space
Gemini is a new (circa 2019) Gopher-ish hypertext protocol. Browsing in Emacs is nice.
Install a browser, elpher..
(use-package elpher)
And a mode
(use-package gemini-mode)
Don't require a final newline
Very occasionally this causes problems and it's not something that I actually care about. To be honest I do not know why Emacs has a default behavior where it adds a newline to the end of the file on save.
(setq require-final-newline nil)
Caps lock mode
For those of us who did away with the caps lock button but write SQL sometimes
(use-package caps-lock)
Allow swapping windows with ctrl + shift + left-click-drag
(defvar window-swap-origin nil)
(defun window-swap-start (event)
"Start swapping windows using mouse events."
(interactive "e")
(setq window-swap-origin (posn-window (event-start event))))
(defun window-swap-end (event)
"End swapping windows using mouse events."
(interactive "e")
(let ((origin window-swap-origin)
(target (posn-window (event-end event))))
(window-swap-states origin target))
(setq window-swap-origin nil))
(global-set-key (kbd "<C-S-mouse-1>") 'window-swap-start)
(global-set-key (kbd "<C-S-drag-mouse-1>") 'window-swap-end)
Render this file for display on the web
This defines a command that will export this file to GitHub flavored Markdown and copy that to README.md so that this file is always the one that appears on the GitHub repository landing page, but in the correct format and everything.
(defun render-configfile-for-web ()
(interactive)
(when (string=
(file-name-nondirectory (buffer-file-name))
"ian.org")
(org-html-export-to-html)
(org-gfm-export-to-markdown)
(if (find-buffer-visiting "~/.emacs.d/README.md")
(kill-buffer-ask (find-buffer-visiting "~/.emacs.d/README.md")))
(delete-file "README.md" t)
(rename-file "ian.md" "README.md")
)
)
Update README.md git hook
Before commit, generate the README.md file from the updated configuration.
TODO Figure out why this produces "args out of bounds" error
# emacsclient -e '(progn (find-file "~/.emacs.d/ian.org") (render-configfile-for-web))'
# git add README.md ian.html
I think the command being passed to emacsclient
here might be a bit brittle and this approach assumes Emacs is already running, which will be annoying (I'll have to disable this hook) if I'm ever using git
on the command line for this repo but given that this repo is.. what it is.. this seems to be working well enough.
Hostname-based tweaks
This is a simple convention that I use for loading machine-specific configuration for the different machines I run Emacs on.
- looks for Org files in
/home/$USER/.emacs.d/local/
with a name that is the same as the hostname of the machine. - shells out to call
hostname
to determine the hostname. - tangles that .org file to a .el file and executes it
This allows configuration to diverge to meet needs that are unique to a specific workstation.
(let ;; find the hostname and assign it to a variable
((hostname (string-trim-right
(shell-command-to-string "hostname"))))
(progn
(org-babel-tangle-file
(concat "~/.emacs.d/local/" hostname ".org")
(concat hostname ".el"))
(load (concat "~/.emacs.d/local/" hostname ".el"))
(require 'local)))
There must be an Org file in local/
named $(hostname).org
or init actually breaks. This isn't great but for now I've just been making a copy of one of the existing files whenever I start on a new machine. It may someday feel worth my time to automate this, but so far it hasn't been worth it, and I just create local/"$(hostname).org"
as part of initial setup, along with other tasks that I do not automate in this file.
Secrets
Load in any additional settings that I do not wish to make public, if set
(if (file-exists-p "~/.secret.el")
(progn
(load-file "~/.secret.el")
(require '.secret)))
Footer
Start server
(server-start)
End of file
Everything after this point in the config file must not be emacs-lisp
(provide '~/.emacs.d/ian.el)
;;; ian.el ends here
Styles for HTML export
We can spruce up the HTML representation of this file with a little bit of CSS.
body {
background-image: url("EmacsIcon.svg");
background-size: 100%;
background-repeat: no-repeat;
background-position: right top;
background-size: 500px 500px;
background-color: #F2F2F2;
}
#content {
font-family: Sans;
font-size: 1.2em;
width: 90%;
max-width: 950px;
margin-left: auto;
margin-right: auto;
padding: 25px;
background-color: rgba(255, 255, 255, .5);
}
.validation {
display: none;
}
a {
color: #EF0FFF;
}
a:visited {
color: #076678;
}
a:hover {
color: #FFBC42;
}
a:active {
color: #F74343;
}
div.org-src-container {
background-color: #FFFFE0;
width: 100%;
height: 100%;
overflow: hidden;
}
pre.src {
width: 100%;
height: 100%;
overflow: scroll;
margin-left: 20px;
-ms-overflow-style: none; /* Internet Explorer 10+ */
scrollbar-width: none; /* Firefox */
}
pre.src::-webkit-scrollbar {
display: none;
}
img {
max-width: 100%;
}
pre.example {
padding: 10px;
width: 100%;
overflow-x: scroll;
-ms-overflow-style: none; /* Internet Explorer 10+ */
scrollbar-width: none; /* Firefox */
}
pre.example::-webkit-scrollbar {
display: none;
}
Launching Emacsclient
Nifty shell function for hassle-free starting of emacsclient
args=""
nw=false
# check if emacsclient is already running
if pgrep -U $(id -u) emacsclient > /dev/null; then running=true; fi
# check if the user wants TUI mode
for arg in "$@"; do
if [ "$arg" = "-nw" ] || [ "$arg" = "-t" ] || [ "$arg" = "--tty" ]
then
nw=true
fi
done
# if called without arguments - open a new gui instance
if [ "$#" -eq "0" ] || [ "$running" != true ]; then
args=(-c $args) # open emacsclient in a new window
fi
if [ "$#" -gt "0" ]; then
# if 'em -' open standard input (e.g. pipe)
if [[ "$1" == "-" ]]; then
TMP="$(mktemp /tmp/emacsstdin-XXX)"
cat >$TMP
args=($args --eval '(let ((b (generate-new-buffer "*stdin*"))) (switch-to-buffer b) (insert-file-contents "'${TMP}'") (delete-file "'${TMP}'"))')
else
args=($@ $args)
fi
fi
# emacsclient $args
if $nw; then
emacsclient "${args[@]}"
else
(nohup emacsclient "${args[@]}" > /dev/null 2>&1 &) > /dev/null
fi
Running Emacs properly from the GUI
This .desktop
file calls emacs
when it's not already running, and emacsclient
otherwise. Slow on first launch, then fast for every new frame thereafter.
Tangling this file will install the .desktop file to the correct location (~/.local/share/applications/Emacsclient.desktop
).
[Desktop Entry]
Name=Emacs
GenericName=Text Editor
Comment=Edit text
MimeType=text/english;text/plain;text/x-makefile;text/x-c++hdr;text/x-c++src;text/x-chdr;text/x-csrc;text/x-java;text/x-moc;text/x-pascal;text/x-tcl;text/x-tex;application/x-shellscript;text/x-c;text/x-c++;
Exec=emacsclient -c -a "emacs" %F
Icon=emacs
Type=Application
Terminal=false
Categories=Development;TextEditor;Utility;
StartupWMClass=Emacs
TODO Figure out how to run Emacs as a daemon so that closing the last frame doesn't exit
Launching in headless mode introduces some font problems (fonts don't load when changing themes) that I haven't been able to debug.
Compiling Emacs from Source
Some notes on the dependencies that I found were needed to build Emacs 28.1 on fresh Ubuntu with the configuration flags that I like
./autogen.sh
sudo apt-get install make autoconf libx11-dev libmagickwand-dev libgtk-3-dev libwebkit2gtk-4.0-dev libgccjit-11-dev libxpm-dev libgif-dev libgnutls28-dev libjansson-dev libncurses-dev texinfo
./configure --without-toolkit-scroll-bars --with-imagemagick --with-x --with-xwidgets --with-json --with-x-toolkit=gtk3 --with-native-compilation --with-mailutils