Because I wanted to play with some of the latest goodies like project.el
and
tab-bar.el
I ended up having to compile Emacs from source to get the latest.
Getting the Source
Emacs is an old project and the Git repo is suitably large, since I’m only after a particular branch we can ask Git to fetch just that branch.
git clone --depth 1 --branch emacs-27 https://git.savannah.gnu.org/git/emacs.git
Build Dependencies
After enabling deb-src
repos in Ubuntu getting all the build dependencies for Emacs
was easy enough. I also made sure libjansson
was available so that I could get native
JSON parsing - useful if I ever setup lsp-mode
or eglot
sudo apt build-dep emacs
sudo apt install libjansson libjansson-dev
Building
With the prerequisites taken care of building and installing is the familiar
configure
, make
, make install
process.
./configure
make -j 4
make install
My init.el
file starts here
;;; init.el --- Emacs Configuration -*- lexical-binding: t -*-
(defconst emacs-start-time (current-time))
(defconst mmdc-path "/home/alex/.npm-packages/bin/mmdc")
Setting up the package archives
(require 'package)
(setq package-archives '(("melpa" . "https://melpa.org/packages/")
("org" . "https://orgmode.org/elpa/")
("elpa" . "https://elpa.gnu.org/packages/")))
On first boot of a new install we of course are going to want to install a bunch of packages so it makes sense to ensure we have an up to date archive
(package-initialize)
(unless package-archive-contents
(package-refresh-contents))
I use use-package heavily in my configuration, let’s make sure it’s available.
(unless (package-installed-p 'use-package)
(package-install 'use-package))
Don’t clutter directories with autosaves and backups…
(setq auto-save-default nil
create-lockfiles nil
make-backup-files nil)
Most of what I work with is in Git so I’d rather if buffers just kept sync with what’s on disk
(global-auto-revert-mode 1)
(set-language-environment "UTF-8")
(set-default-coding-systems 'utf-8)
By default, truncate long lines
(setq-default truncate-lines t)
(use-package whitespace
:init
(setq sentence-end-double-space nil
whitespace-style '(face empty trailing))
(setq-default indent-tabs-mode nil)
(global-whitespace-mode)
(add-hook 'before-save-hook #'whitespace-cleanup))
Disable a bunch of UI elements
(scroll-bar-mode -1)
(tool-bar-mode -1)
(menu-bar-mode -1)
(blink-cursor-mode -1)
(setq inhibit-startup-message t)
(use-package ibuffer
:bind (("C-x C-b" . ibuffer))
:config
<<ibuffer-config>>)
I like ibuffer
to open itself in a bottom side window, like many of the
“additional” utilities I put down there
,(me/display-buffer-in-panel "\\*Ibuffer\\*")
In order for ibuffer
to call display-buffer and thus have the rules defined in
display-buffer-alist take effect we need to ensure that ibuffer
opens itself
in an “other window”
(setq ibuffer-use-other-window t)
(use-package icomplete
:init
(fido-mode 1))
I quite like the Ubuntu family of fonts
(set-face-attribute 'default nil :font "Ubuntu Mono" :height 125)
(set-face-attribute 'fixed-pitch nil :font "Ubuntu Mono" :height 125)
(set-face-attribute 'variable-pitch nil :font "Ubuntu Light" :height 125)
Why not? 😃
(use-package all-the-icons
:ensure t)
Dired
(use-package all-the-icons-dired
:ensure t
:hook (dired-mode . all-the-icons-dired-mode))
I want Emacs by default to enable line numbers in buffers
(global-display-line-numbers-mode 1)
Unless I specify a particular mode in which to disable them
(dolist (hook '(doc-view-mode-hook
eshell-mode-hook
gfm-mode-hook
org-mode-hook
shell-mode-hook
term-mode-hook))
(add-hook hook (lambda () (display-line-numbers-mode 0))))
(use-package doom-modeline
:ensure t
:init (doom-modeline-mode 1)
:config
(column-number-mode 1)
(size-indication-mode 1)
(setq doom-modeline-buffer-file-name-style 'relative-to-project
doom-modeline-buffer-modification-icon t
doom-modeline-buffer-state-icon t
doom-modeline-height 25
doom-modeline-major-mode-icon t
doom-modeline-major-mode-color-icon nil
doom-modeline-minor-modes nil))
Emacs 27 comes with vim style tabs (i.e. a tab holds a collection of windows in some
layout ) via tab-bar.el
. While I want to use them, I’d rather not see the tabs
themselves rendered
(use-package tab-bar
:config
(setq tab-bar-show nil))
(use-package modus-themes
:ensure t
:bind ("<f5>" . modus-themes-toggle)
:init
(setq modus-themes-diffs 'desaturated
modus-themes-headings '((t . rainbow-section-no-bold))
modus-themes-intense-hl-line t
modus-themes-lang-checkers 'straight-underline
modus-themes-links 'faint-neutral-underline
modus-themes-org-blocks 'grayscale
modus-themes-paren-match 'intense-bold
modus-themes-region 'bg-only-no-extend
modus-themes-scale-headings t
modus-themes-slanted-constructs t)
;; Default to the light theme
(modus-themes-load-operandi)
(show-paren-mode 1))
<<window-functions>>
(use-package window
:init
<<window-config>>
<<window-placement>>
:bind (("<f8>" . window-toggle-side-windows)))
After using Emacs for any length of time, you’ll quickly find that new windows pop open all the time in various locations as you call different commands. After finding this video on the display-buffer-alist variable, it turns out Emacs offers a very rich framework for controlling what windows get opened where - I should have guessed!
(setq display-buffer-alist `(,(me/display-buffer-in-panel "\\*Help\\*")
,(me/display-buffer-in-panel "\\*Messages\\*")
,(me/display-buffer-in-panel "\\*\\(e?shell\\)\\*")
("\\*Process List\\*" ;; Setting no-other-window etc seems to break C-x C-c
(display-buffer-in-side-window) ;; Error => Wrong type argument window-live-p, nil...
(side . bottom)
(slot . 0)
(window-height . 0.25))
,(me/display-buffer-in-panel
(me/buffer-select-by-major-mode 'compilation-mode))
,(me/display-buffer-in-top-window "\\*Completions\\*")
<<window-placement-rules>>
))
I’d rather have the left/right side windows take the full height of the frame, thankfully window-sides-vertical is just the option I’m looking for
(setq window-sides-vertical t)
The following function handles selecting buffers based on their major mode. **Requires lexical-binding**
(defun me/buffer-select-by-major-mode (mode)
"A filter for use with `display-buffer-alist', will select a
buffer if it matches the given major-mode"
(lambda (buffer action)
(with-current-buffer buffer
(eq major-mode mode))))
The following action is essentially the same as display-buffer-in-side-window but additionally enables tab-line-mode for that buffer.
(defun me/display-tabbed-buffer-in-side-window (buffer alist)
"See `display-buffer-in-side-window'"
(display-buffer-in-side-window buffer alist)
(with-current-buffer buffer
(tab-line-mode)))
The following then makes use of the above action to place matching buffers in a “panel” similar to how VSCode does things
(defun me/display-buffer-in-panel (predicate)
"Display buffers matching the given PREDICATE in the panel.
To borrow terminology from VSCode the panel is that collapsable
window at the bottom of the screen.
"
`(,predicate
(me/display-tabbed-buffer-in-side-window)
(window-height . 0.25)
(side . bottom)
(slot . 0)
(window-parameters . ((no-other-window . t)
(no-delete-other-windows . t)))))
Unfortunately for some reason the call to tab-line-mode
does not take effect
for shells and I’m not sure why yet. So let’s also invoke it via the mode hooks
(dolist (hook '(shell-mode-hook
eshell-mode-hook))
(add-hook hook (lambda () (tab-line-mode))))
I quite like having any temporary buffers like *Completions*
pop up in a top
side window and then vanish again.
(defun me/display-buffer-in-top-window (predicate)
"Display buffers matching the given PREDICATE in a top side window."
`(,predicate
(display-buffer-in-side-window)
(window-height . 0.2)
(side . top)
(slot . 0)
(window-parameters . ((no-other-window . t)))))
<<dired-functions>>
(use-package dired
:bind (("C-x d" . me/dired-open-directory))
:hook (dired-mode . me/dired-mode-tweaks))
A quick command that opens the default-directory
in dired
(defun me/dired-open-directory ()
(interactive)
(dired default-directory))
A collection of tweaks to apply to new dired
buffers
(defun me/dired-mode-tweaks ()
(dired-hide-details-mode))
Ensure that dired
buffers open in a side window
(,(me/buffer-select-by-major-mode 'dired-mode)
(display-buffer-in-side-window)
(window-width . 0.15)
(side . left)
(slot . 0)
(window-parameters . ((no-other-window . t))))
elfeed is an RSS feed reader for Emacs.
<<elfeed-functions>>
(use-package elfeed
:bind (("C-c e" . me/elfeed-start)
:map elfeed-search-mode-map
("g" . me/elfeed-update)
:map elfeed-show-mode-map
("q" . delete-window))
:ensure t
:config
(setq elfeed-use-curl t)
<<elfeed-config>>)
Custom startup function to ensure that elfeed runs in a dedicated tab.
(defun me/elfeed-start-new-tab ()
"Does the work of creating a new tab"
(tab-bar-new-tab)
(tab-bar-rename-tab "elfeed")
(elfeed))
(defun me/elfeed-start ()
"Switch to the elfeed tab, create one if it doesn't exist."
(interactive)
(let ((tabs (mapcar (lambda (tab) (alist-get 'name tab)) (tab-bar-tabs))))
(if (member "elfeed" tabs)
(tab-bar-select-tab-by-name "elfeed")
(me/elfeed-start-new-tab))))
I know that there’s the elfeed-org package that lets you configure elfeed feeds through an org file, but I thought it would be a good exercise in Elisp to see if I could put something similar together.
Rather than using headlines I’ve thrown all the links and corresponding tags into a table and wrote a few functions that will convert the table into the format required by elfeed-feeds and set the variable to the result.
URL | Tags |
---|---|
https://pointieststick.com/feed/ | linux kde |
https://blog.gtk.org/rss | linux gtk |
https://blogs.gnome.org/shell-dev/rss | linux gnome |
https://sachachua.com/blog/category/emacs/feed/ | emacs |
https://www.youtube.com/feeds/videos.xml?channel_id=UC5Qw7uMu8_QMJHBw3mQJ62w | linux yt |
(defun me/feed-table-row-to-item (row)
"Convert a ROW from a feed table into a valid elfeed entry"
(let* ((url (car row))
(tags (split-string (cadr row) " ")))
(append (list url) (mapcar 'make-symbol tags))))
(defun me/feed-table-to-list (feed-table)
"Convert an orgmode FEED-TABLE to a list compatible with elfeed."
(mapcar 'me/feed-table-row-to-item feed-table))
(defun me/feed-table-extract ()
"Find the table called `elfeed-feed-table' in my config file
and use it to set the `elfeed-feeds' variable."
(with-temp-buffer
(insert-file-contents "/home/alex/.emacs.d/README.org")
(org-table-map-tables (lambda ()
(setq tbl-name (plist-get (cadr (org-element-at-point)) :name))
(if (string= tbl-name "elfeed-feed-table")
(let ((tbl (cdr (cdr (org-table-to-lisp)))))
(setq elfeed-feeds (me/feed-table-to-list tbl)))))
t)))
(defun me/elfeed-update (arg)
"Refresh all RSS feeds.
This simply calls `elfeed-update' unless the prefix arg is set
or `elfeed-feeds' is nil in which case it will call
`me/feed-table-extract' beforehand."
(interactive "P")
(if (or (not (null arg))
(null elfeed-feeds))
(me/feed-table-extract))
(elfeed-update))
Open article buffers below the main summary list.
("\\*elfeed-entry\\*"
(display-buffer-reuse-mode-window display-buffer-at-bottom)
(window-height . 0.8))
In order for this setting to work as expected though we need to tweak how elfeed manages its buffers
(setq elfeed-show-entry-switch 'pop-to-buffer)
(setq elfeed-show-entry-delete 'delete-window)
(use-package git-gutter
:config
(global-git-gutter-mode 1)
(set-face-foreground 'git-gutter:added "forest green")
(set-face-foreground 'git-gutter:modified "goldenrod")
(set-face-foreground 'git-gutter:deleted "brown")
(setq git-gutter:added-sign "▐"
git-gutter:modified-sign "▐"
git-gutter:removed-sign "▐"))
(use-package magit
:bind (("C-x g" . magit-status)))
(use-package lsp-mode
:ensure t
:init
(setq lsp-keymap-prefix "C-c l")
:hook ((python-mode . lsp))
:commands lsp)
<<org-packages>>
<<org-functions>>
(use-package org
:hook (org-mode . me/org-mode-tweaks)
:bind (("C-c a" . org-agenda)
("C-c c" . org-capture))
:config
(setq org-directory "~/Documents/org/")
<<org-config>>)
(setq org-agenda-files (list org-directory))
In order to execute code in source blocks we need to ensure that org-babel
has
loaded support for it
(org-babel-do-load-languages 'org-babel-load-languages
'((emacs-lisp . t)
(python . t)
(shell . t)))
(setq org-capture-templates
'(("t" "Task" entry (file+headline "life.org" "Events")
"* TODO %?\n")
("e" "Event" entry (file+headline "life.org" "Events")
"* %?\nSCHEDULED: %^t")
("j" "Journal" entry (file+headline "life.org" "Journal")
"* %u\n%?\n\n** Exercise\n" :prepend t)))
(add-to-list 'org-modules 'org-habit t)
(setq org-adapt-indentation 'headline-data
org-habit-show-all-today t
org-log-into-drawer t)
A collection of tweaks to apply when opening a new org file
(defun me/org-mode-tweaks ()
(setq-local fill-column 80)
(turn-on-auto-fill)
(flyspell-mode)
(variable-pitch-mode 1)
;; Switch certain elements back to fixed pitch
(set-face-attribute 'org-block nil :foreground nil :inherit 'fixed-pitch)
(set-face-attribute 'org-link nil :inherit '(button fixed-pitch))
(set-face-attribute 'org-code nil :inherit '(shadow fixed-pitch))
(set-face-attribute 'org-table nil :inherit '(shadow fixed-pitch))
(set-face-attribute 'org-verbatim nil :inherit '(shadow fixed-pitch))
(set-face-attribute 'org-special-keyword nil
:inherit '(font-lock-comment-face fixed-pitch))
(set-face-attribute 'org-meta-line nil
:inherit '(font-lock-comment-face fixed-pitch))
(set-face-attribute 'org-checkbox nil :inherit 'fixed-pitch))
<<project-functions>>
<<project-packages>>
(use-package project
:bind (("C-x p f" . project-find-file)
("C-x p s" . me/project-search)))
(defun me/project-search ()
"Execute a project wide search with ripgrep."
(interactive)
(let ((dir (cdr (project-current t)))
(query (read-string "Search query: ")))
(rg query "*" dir)))
rg is an Emacs frontend to ripgrep.
(use-package rg
:ensure t)
<<c-functions>>
(use-package cc-mode
:bind (:map c-mode-map
("C-c d" . me/start-debugging)
("C-c g" . recompile))
:config
(setq-default c-basic-offset 4)
(setq compilation-scroll-output t))
Making use of tab-bar.el
here is a custom function that starts a debugging session by
first opening a new tab. This allows for the use of gdb-many-windows
without messing
with the current window layout.
(defun me/start-debugging ()
(interactive)
(let ((program (read-string "Debug program: ")))
(tab-new)
(setq gdb-many-windows t)
(gdb (format "gdb -i=mi %s" program))))
This complements the function above, by listening for the end of the debugging session and closing the tab. I don’t really understand how this works, but I adapted it from this blogpost
(advice-add 'gud-sentinel :after
(lambda (proc msg)
(when (memq (process-status proc) '(signal exit))
(tab-close))))
<<python-functions>>
<<python-packages>>
(use-package python
:bind (:map python-mode-map
("C-c C-p" . me/python-open-repl)
("C-c g" . recompile))
:hook (python-mode . me/python-mode-tweaks)
:config
(setq compilation-scroll-output t)
<<python-config>>)
Tweaks to apply when opening Python files
(defun me/python-mode-tweaks ()
(setq-local fill-column 88))
The builtin python-mode has a run-python command that will launch a Python REPL that we can interact with. Unfortunately by default it will just try runnning your system python - not very useful.
Instead I have written a function that builds on project.el
that will attempt to find
the project’s virtualenv and run a Jupyter REPL, falling back to Python if it is not
installed.
(defun me/python-open-repl ()
"Open a Python REPL in the correct virtualenv for the
project.
This will first look for Jupyter and will fall back to Python if
it's not installed.
It will attempt to find the root folder for the current package
and open the shell there.
"
(interactive)
(let* ((dir (cdr (project-current t)))
(paths (list
(concat dir ".env/bin/jupyter")
(concat dir ".env/bin/python")))
(path (car (seq-filter 'file-exists-p paths))))
(setq python-shell-interpreter path
python-shell-prompt-detect-failure-warning nil)
(if (string-match-p (regexp-quote "jupyter") path)
(setq python-shell-interpreter-args "console --simple-prompt")
(setq python-shell-interpreter-args "-i"))
(let* ((package-dir (me/python-find-setup-py
(buffer-file-name (current-buffer))))
(default-directory (file-name-directory package-dir)))
(run-python))))
So that we get to use Jupyer’s tab completion, we just need to tell Emacs to not use its own.
(add-to-list 'python-shell-completion-native-disabled-interpreters
"jupyter")
Ensure that the Python REPL opens in the “Panel”
,(me/display-buffer-in-panel
(me/buffer-select-by-major-mode 'inferior-python-mode))
I want to run flake8
on the current package every time I save a file. The
method I’m currently using relies on finding the setup.py
file for the current
package and using compilation-start to run flake8
in a compilation buffer.
(defun me/python-flake8-project ()
"Run flake8 on the current project in a compilation buffer.
This function will attempt to find the setup.py file for the
package currently being edited using `me/python-find-setup-py'.
If a filepath is found, its parent directory is assumed to be the
package root and flake8 will be run in a compilation buffer via
`compilation-start'."
(interactive)
(let* ((filename (buffer-file-name (current-buffer)))
(setup-py (me/python-find-setup-py filename)))
(unless (null setup-py)
(let ((default-directory (file-name-directory setup-py)))
(compilation-start "flake8" nil
(lambda (_modename)
(format "%s: flake8" default-directory)))))))
The following methods handle the recursing up the directory tree to find the setup.py
(defun me/python-find-setup-py--from-dir (dir)
(if (string= dir "/")
nil
(let* ((setup-py (concat dir "setup.py")))
(if (file-exists-p setup-py)
setup-py
(me/python-find-setup-py
(file-name-directory (directory-file-name dir)))))))
(defun me/python-find-setup-py (filename)
"Find the setup.py file that corresponds with the package that
contains FILENAME"
(me/python-find-setup-py--from-dir
(file-name-directory filename)))
blacken will automatcially apply black to the buffer on save.
(use-package blacken
:ensure t
:hook (python-mode . blacken-mode))
This will of course require the black
command to be available, easiest way to do this
is to install it via pipx
pipx install black
(use-package mermaid-mode
:ensure t
:config
(setq mermaid-mmdc-location mmdc-path))
ob-mermaid allows for mermaid diagrams to be used within Org Mode files
(use-package ob-mermaid
:ensure t
:config
(setq ob-mermaid-cli-path mmdc-path))
<<markdown-functions>>
(use-package markdown-mode
:ensure t
:hook (gfm-mode . me/gfm-mode-tweaks)
:mode (("\\.md\\'" . gfm-mode)
("\\.markdown\\'" . gfm-mode)))
A collection of tweaks to apply when opening a new markdown file
(defun me/gfm-mode-tweaks ()
(setq-local fill-column 80)
(turn-on-auto-fill)
(flyspell-mode)
(variable-pitch-mode 1)
;; Switch certain elements back to fixed pitch
(set-face-attribute 'markdown-metadata-key-face nil :inherit 'fixed-pitch)
(set-face-attribute 'markdown-metadata-value-face nil :inherit 'fixed-pitch))
Ensure that anything set through custom
is saved to a separate file
(setq custom-file "~/.emacs.d/custom.el")
(load custom-file 'noerror)
Add a log message that gives us an indication on how long it took to load the config.
(let ((startup-time
(float-time (time-subtract (current-time) emacs-start-time))))
(message "Loaded configuration in %.3fs" startup-time))
The following Local Variables
block sets up an on save hook that automatically tangles
this file so that init.el
is always in sync with the latest.