Skip to content

alcarney/emacs.d

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

53 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Emacs Configuration

Setup

Installing Emacs

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

Config Start

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

Packaging

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

Use Package

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

Misc Settings

Backups

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)

Encoding

(set-language-environment "UTF-8")
(set-default-coding-systems 'utf-8)

Long Lines

By default, truncate long lines

(setq-default truncate-lines t)

Whitespace

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

UI

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)

Buffers

IBuffer

(use-package ibuffer
  :bind (("C-x C-b" . ibuffer))
  :config
  <<ibuffer-config>>)

Window Placement

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)

Completion

IComplete

(use-package icomplete
  :init
  (fido-mode 1))

Fonts

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)

Icons

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

Line Numbers

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

Modeline

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

Tab Bar

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

Theme

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

Windows

<<window-functions>>

(use-package window
  :init
  <<window-config>>
  <<window-placement>>
  :bind (("<f8>" . window-toggle-side-windows)))

Placement

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)

Select By Major Mode

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

Display in Panel

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

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

Display In Top Window

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

Programs

Dired

<<dired-functions>>

(use-package dired
  :bind (("C-x d" . me/dired-open-directory))
  :hook (dired-mode . me/dired-mode-tweaks))

Open dired directory

A quick command that opens the default-directory in dired

(defun me/dired-open-directory ()
  (interactive)
  (dired default-directory))

Tweaks

A collection of tweaks to apply to new dired buffers

(defun me/dired-mode-tweaks ()
  (dired-hide-details-mode))

Window Placement

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

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

Startup

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

Feeds

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.

URLTags
https://pointieststick.com/feed/linux kde
https://blog.gtk.org/rsslinux gtk
https://blogs.gnome.org/shell-dev/rsslinux gnome
https://sachachua.com/blog/category/emacs/feed/emacs
https://www.youtube.com/feeds/videos.xml?channel_id=UC5Qw7uMu8_QMJHBw3mQJ62wlinux 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))

Window Placement

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)

Git

Git Gutter

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

Magit

(use-package magit
  :bind (("C-x g" . magit-status)))

LSP Mode

(use-package lsp-mode
  :ensure t
  :init
  (setq lsp-keymap-prefix "C-c l")
  :hook ((python-mode . lsp))
  :commands lsp)

Org Mode

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

Org Agenda

(setq org-agenda-files (list org-directory))

Org Babel

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

Org Capture

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

TODOs and Habits

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

Tweaks

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 Management

<<project-functions>>

<<project-packages>>

(use-package project
  :bind (("C-x p f" . project-find-file)
         ("C-x p s" . me/project-search)))

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

Check for a prefix argument and prompt for the filename pattern to search on?

Additional Packages

rg is an Emacs frontend to ripgrep.

(use-package rg
  :ensure t)

Programming

C

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

Start Debugging

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

Stop Debugging

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

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

Tweaks to apply when opening Python files

(defun me/python-mode-tweaks ()
  (setq-local fill-column 88))

Opening a Python REPL

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

Flake8

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

Additional Packages

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

Mermaid

Mermaid Diagrams

(use-package mermaid-mode
  :ensure t
  :config
  (setq mermaid-mmdc-location mmdc-path))

ob-mermaid

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

Prose

Markdown

<<markdown-functions>>

(use-package markdown-mode
  :ensure t
  :hook (gfm-mode . me/gfm-mode-tweaks)
  :mode (("\\.md\\'" . gfm-mode)
         ("\\.markdown\\'" . gfm-mode)))

Tweaks

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

Finishing Up

Custom

Ensure that anything set through custom is saved to a separate file

(setq custom-file "~/.emacs.d/custom.el")
(load custom-file 'noerror)

Startup Time

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

Auto Tangling

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.

Releases

No releases published

Packages

No packages published