This is a configuration for Emacs based on the speech dispatcher software, which is a utility for providing speech output for blind and visually impaired persons. Additionally, Braille support is provided with the brltty software.
In order for this configuration to work, you will need to have several things installed on your system:
- Emacs
- speechd-dispatcher
- brltty
If you are reading this file in Emacs, you will have discovered that it is written in org-mode and the configuration itself is tangled from here.
Here are some things to note:
- interact with the source blocks by pressing C-‘, which puts you into a special edit mode
- The sections of the document are folded, so you will need to press TAB on the section you want to see the children of.
This repository is now hosted on Github, so you can submit pull requests and file issues there.
There are a few things that one might want to do before Emacs starts loading the initialization file, such as customizing the initial frame, increasing the garbage collection threshhold, etc.
For our purposes, we don’t really need to have the toolbars or menu bars active by default. If you should want to use something that is provided in the menu bars, you can get at them with F10.
;; -*- lexical-binding: t; -*-
(setq inhibit-startup-message t
toolbar-mode nil
menu-bar-mode nil)
(push '(tool-bar-lines . 0) default-frame-alist)
(push '(menu-bar-lines . 0) default-frame-alist)
(push '(vertical-scroll-bars) default-frame-alist)
(setq load-prefer-newer 'noninteractive)
Here is where the bulk of the initialization takes place, in a file called init.el.
There are many ways in which to customize Emacs:
- some prefer to modularize the configuration
- Some like to have a literate config
- Or maybe have one giant configuration file.
The initial configuration will be a literate config. In so doing, we can both provide the code that Emacs Will run in addition to providing further user-facing documentation.
In Emacs 29, there exists a setopt macro that works like setq, but is meant for user-customizeable variables.
It replaces general-setq, which aims to be a similar interface.
(unless (fboundp 'setopt)
(require 'wid-edit)
(defmacro setopt (&rest pairs)
"Set VARIABLE/VALUE pairs, and return the final VALUE.
This is like `setq', but is meant for user options instead of
plain variables. This means that `setopt' will execute any
`custom-set' form associated with VARIABLE.
\(fn [VARIABLE VALUE]...)"
(declare (debug setq))
(unless (zerop (mod (length pairs) 2))
(error "PAIRS must have an even number of variable/value members"))
(let ((expr nil))
(while pairs
(unless (symbolp (car pairs))
(error "Attempting to set a non-symbol: %s" (car pairs)))
(push `(setopt--set ',(car pairs) ,(cadr pairs))
expr)
(setq pairs (cddr pairs)))
(macroexp-progn (nreverse expr))))
(defun setopt--set (variable value)
(custom-load-symbol variable)
;; Check that the type is correct.
(when-let ((type (get variable 'custom-type)))
(unless (widget-apply (widget-convert type) :match value)
(warn "Value `%S' does not match type %s" value type)))
(put variable 'custom-check-value (list value))
(funcall (or (get variable 'custom-set) #'set-default) variable value)))
;; -*- lexical-binding: t; -*-
(setopt package-quickstart-file (expand-file-name "var/package-quickstart.el" user-emacs-directory)
package-quickstart t)
There are three package sources that we use by default:
- Melpa
- The place that has the most packages, which I have been using for years now.
- ELPA
- The GNU packages.
- non-GNU ELPA
- The repository for packages that don’t quite meet the FSF guidelines.
(require 'package)
(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/"))
(customize-set-variable 'package-archive-priorities '(("melpa" . 10) ("gnu" . 9) ("nongnu" . 8)))
use-package is a configuration macro that allows for easy installation, customization, and further configuration of packages.
As of Emacs 29, this will be built into Emacs core, so installing it will not be necessary.
(unless (and (version< emacs-version "29.0") (package-installed-p 'use-package))
(package-install 'use-package))
(setopt use-package-compute-statistics t
use-package-always-demand t)
(require 'use-package)
By default, Emacs will pollute your configuration directory with various auto-save, cache and configuration files. The aim of the no littering package is to keep the configuration directory relatively clean, by putting persistant information into the var subdirectory and configuration files in etc.
(use-package no-littering
:ensure t
:hook (after-init . (lambda () (load custom-file)))
:custom
(auto-save-name-transforms `((".*" ,(no-littering-expand-var-file-name "auto-save/") t)))
(custom-file (no-littering-expand-etc-file-name "custom.el")))
The configuration of speechd-el is quite immense, so this section is meant more as a guide as to how to do it rather than something that is set in stone; feel free to tweak things to your liking.
(use-package speechd-el
:ensure t
:custom
(speechd-speak-whole-line t)
(speechd-speak-echo nil)
(speechd-speak-read-command-keys nil)
(speechd-voices '((nil
(rate . 100)
(output-module . "espeak-ng"))))
:config
(speechd-speak))
As you input keys into Emacs, which-key will interactively filter the currently available options and present the commands and their keybindings. As it is right now, which-key will wait one second before printing its output to the minibuffer for speechd-el to read. If you should do something that prints out a large keymap, i.e. ctl-x, it will barf everything out in one go; I need to figure out a solution for this as yet.
(use-package which-key
:ensure t
:custom
(which-key-idle-delay 1.0)
(which-key-compute-remaps t)
(which-key-popup-type 'minibuffer)
(which-key-show-transient-maps t)
(which-key-mode t))
General is a nifty utility for easily defining keybindings, as well as having some useful utilities for running code after initialization of Emacs, after the first GUI and/or TTY frame is created, and setting custom user options.
(use-package general
:ensure t
:config
(general-auto-unbind-keys)
(general-evil-setup t)
;; Create the leader definer
(general-create-definer mapleader
:keymaps 'override
:prefix "SPC"
:global-prefix "C-SPC"
:states '(normal visual insert emacs))
(mapleader
"b" '(:ignore t :which-key "Buffer operations")
"bn" '(next-buffer :which-key "Go to the next buffer.")
"bp" '(previous-buffer :which-key "Go to the previous buffer.")
"bb" '(switch-to-buffer :which-key "Switch to a buffer.")
"bj" '(:ignore t :which-key "Jumping to specific buffers.")
"bjd" '(dired-jump :which-key "Go to a dired window.")
"bjm" '((lambda () (interactive) (switch-to-buffer (messages-buffer))) :which-key "Jump to messages buffer.")
"bjw" '((lambda () (interactive) (switch-to-buffer "*Warnings*")) :which-key "Go to the warnings buffer.")
"bjs" '((lambda () (interactive) (switch-to-buffer "*scratch*")) :which-key "Switch to the scratch buffer.")
"bi" '(ibuffer :which-key "Interactive buffer management through the Ibuffer interface.")
"br" '(revert-buffer :which-key "Revert the buffer to how it is on disk.")
;; Customization
"c" '(:ignore t :which-key "Customization interface.")
"cc" '(customize :which-key "Open the customization index.")
"cf" '(customize-face :which-key "Customize a face.")
"cg" '(customize-group :which-key "Customize a specific group.")
"cv" '(customize-variable :which-key "Customize a variable in the custom interface.")
"cV" '(customize-set-variable :which-key "Expert level variable setting, sans interface.")
"f" '(:ignore t :which-key "File operations.")
"fs" '(save-buffer :which-key "Save your currently opened file.")
"ff" '(find-file :which-key "Find a file.")
"fd" '(dired :which-key "Open a dired buffer.")
"fr" '(recentf-open-files :which-key "Open a recent file.")
;; Frame operations
"F" '(:ignore t "Frames.")
"Fd" '(delete-frame :which-key "Delete this frame.")
"Fo" '(other-frame :which-key "Go to another frame.")
"h" '(:ignore t :which-key "Help")
"hd" '(:ignore t :which-key "Describe parts of Emacs.")
"hdb" '(describe-bindings :which-key "Describe the keybindings that are in effect right now.")
"hdB" '(general-describe-keybindings :which-key "Get a list of the key bindings that are in effect via General.")
"hdf" '(describe-function :which-key "Describe a function.")
"hdF" '(describe-face :which-key "Describe a face.")
"hdk" '(describe-key :which-key "Describe what a key is bound to.")
"hdp" '(describe-package :which-key "Describe a package.")
"hdv" '(describe-variable :which-key "Describe a variable.")
;; Info manuals
"hi" '(:ignore t :which-key "Info")
"hia" '(info-apropos :which-key "Search the info database.")
"hii" '(info-index :which-key "Open the info index.")
"him" '(info-display-manual :which-key "Open a specific info manual."))
(general-create-definer maplocal
:keymaps 'override
:prefix ","
:global-prefix "SPC m"
:states '(normal visual)))
Evil is a system for providing vim-like keybindings in Emacs.
Our preferred undo system for Evil mode.
(use-package undo-tree
:custom
(global-undo-tree-mode t)
:ensure t)
(use-package evil
:ensure t
:custom
(evil-echo-state nil)
(evil-want-integration t)
(evil-want-c-i-jump nil)
(evil-want-keybind nil)
(evil-undo-system 'undo-tree)
:preface
(defun sonarmacs--evil-state-change-notify ()
(when (and evil-next-state evil-previous-state (not (eq evil-previous-state evil-next-state)))
(speechd-say-text (format "Changing state from %s to %s." evil-previous-state evil-next-state) :priority 'important)))
:hook ((evil-insert-state-exit evil-normal-state-exit evil-motion-state-exit evil-operator-state-exit evil-replace-state-exit evil-visual-state-exit evil-emacs-state-exit) . sonarmacs--evil-state-change-notify)
:general
(mapleader
"bd" '(evil-delete-buffer :which-key "Kill the current buffer."))
(:states '(normal visual insert operator replace motion)
speechd-speak-prefix speechd-speak-mode-map)
(speechd-speak-mode-map
"e" 'evil-scroll-line-down)
:config
(evil-mode t)
(speechd-speak--command-feedback (evil-next-line evil-previous-line evil-next-visual-line evil-previous-visual-line evil-beginning-of-line) after (speechd-speak-read-line (not speechd-speak-whole-line)))
(speechd-speak--command-feedback (evil-forward-paragraph evil-backward-paragraph) after
(speechd-speak-read-paragraph))
(speechd-speak--command-feedback (evil-forward-word-begin evil-backward-word-begin evil-backward-word-end evil-forward-word-end) after (speechd-speak-read-word))
(speechd-speak--command-feedback (evil-backward-char) after (speechd-speak-read-char (following-char)))
(speechd-speak--command-feedback (evil-forward-char) after (speechd-speak-read-char (preceding-char))))
For the mass evilification of modes.
(use-package evil-collection
:after evil
:ensure t
:config
(evil-collection-init))
Magit is the really handy git porcilin for Emacs, which I have used for years.
(use-package transient
:ensure t
:custom
(transient-show-popup t)
(transient-enable-popup-navigation t)
(transient-force-single-column t)
:config
(speechd-speak--command-feedback (transient-forward-button transient-backward-button) after
(with-current-buffer (window-buffer transient--window)
;; Get at the button to speak.
(when-let ((button (button-at (point)))
(start (button-start button))
(end (button-end button)))
(speechd-speak-read-region start end)))))
(use-package magit
:custom
(magit-delete-by-moving-to-trash nil)
:general
(mapleader
"g" '(:ignore t :which-key "Git operations.")
"gg" '(magit-status :which-key "Status of the project.")
"gs" '(magit-stage-file :which-key "Stage a file."))
:config
(magit-add-section-hook 'magit-status-sections-hook 'magit-insert-modules 'magit-insert-stashes))
Various git services, such as Github, Gitlab, and SourceHut.
(use-package forge
:after magit
:ensure t
:general
(mapleader
"gf" '(forge-dispatch :which-key "Forge dispatch map.")))
For going back through previous revisions of a file.
(use-package git-timemachine
:ensure t
:general
(mapleader
"tg" '(git-timemachine-toggle :which-key "Toggle the time machine on or off.")))
(use-package magit-gitflow
:ensure t
:hook (magit-mode . turn-on-magit-gitflow))
Tell Emacs who you are, as well as some other things.
(setopt user-full-name "Hunter Jozwiak"
user-mail-address "[email protected]"
user-login-name "sektor")
When Emacs pops up a dialog for serious actions, such as deleting a file, it will expect an answer that is either yes or no, instead of the y or n prompt. The use-short-answers allows those prompts to be answered with either y or n.
(setopt use-short-answers t)
(setopt auto-revert-interval 0.1
global-auto-revert-mode t)
(setopt savehist-mode t)
(setopt recentf-mode t)
(setopt winner-mode t)
(mapleader
"w" '(:ignore t :which-key "Window operations.")
"wl" 'windmove-right
"wh" 'windmove-left
"wk" 'windmove-up
"wj" 'windmove-down
"wr" 'winner-redo
"wu" 'winner-undo
"wo" 'other-window)
(use-package orderless
:ensure t
:custom
(completion-styles '(orderless basic))
(completion-category-overrides '((file (styles . (partial-completion))))))
(use-package vertico
:ensure t
:custom
(vertico-count 20)
(vertico-cycle t)
(vertico-mode t)
:general
(vertico-map
"C-j" 'vertico-next
"C-K" 'vertico-previous)
:preface
(defvar-local spoken-index -1 "Indes of the last spoken candidate")
(defvar-local last-spoken-candidate nil "Last spoken candidate.")
:config
(speechd-speak--command-feedback-region (vertico-insert))
(speechd-speak--defadvice vertico--exhibit after
(let ((new-cand
(substring (vertico--candidate)
(if (>= vertico--index 0)
(length vertico--base)
0)))
(to-speak nil))
(unless (equal last-spoken-candidate new-cand)
(push new-cand to-speak)
(when (or (equal vertico--index spoken-index)
(and (not (equal vertico--index -1))
(equal spoken-index -1)))
(push "candidate" to-speak)))
(when (and (not vertico--allow-prompt)
(equal last-spoken-candidate nil))
(push "first candidate" to-speak))
(when to-speak
(speechd-say-text (mapconcat 'identity to-speak " ")))
(setq-local
last-spoken-candidate new-cand
spoken-index vertico--index))))
(use-package vertico-directory
:after vertico
:general
(vertico-map
"C-h" 'vertico-directory-up
"C-l" 'vertico-directory-down))
For useful annotations, not sure exactly how useful this will be.
(use-package marginalia
:ensure t
:custom
(marginalia-mode t))
(use-package consult
:ensure t)
(use-package embark
:ensure t
:general
([remap describe-bindings] 'embark-bindings
"C-." 'embark-act)
:custom
(prefix-help-command #'embark-prefix-help-command))
(use-package embark-consult
:ensure t
:after embark consult
:hook (embark-collect-mode . consult-preview-at-point-mode))
(use-package corfu
:ensure t
:custom
(corfu-auto t)
(corfu-auto-delay 0.0)
(corfu-cycle t)
(corfu-echo-documentation 1)
(global-corfu-mode t)
:preface
(defvar-local corfu--last-spoken nil "The last spoken candidate.")
(defvar-local corfu--last-spoken-index nil "Index into the last spoken candidate.")
:config
(eldoc-add-command #'corfu-insert)
(speechd-speak--defadvice corfu--exhibit after
(let ((new-cand
(substring (nth corfu--index corfu--candidates)
(if (>= corfu--index 0)
(length corfu--base)
0)))
(to-speak nil))
(unless (equal corfu--last-spoken new-cand)
(push new-cand to-speak)
(when (or (equal corfu--index corfu--last-spoken-index)
(and (not (equal corfu--index -1))
(equal corfu--last-spoken-index -1)))
(push "candidate" to-speak)))
(when (equal corfu--last-spoken nil)
(push "first candidate" to-speak))
(when to-speak
(speechd-say-text (mapconcat 'identity to-speak " ")))
(setq-local
corfu--last-spoken new-cand
corfu--last-spoken-index corfu--index))))
(use-package cape
:ensure t
:config
(add-to-list 'completion-at-point-functions #'cape-file)
(add-to-list 'completion-at-point-functions #'cape-dabbrev)
(advice-add 'pcomplete-completions-at-point :around #'cape-wrap-silent)
(advice-add 'pcomplete-completions-at-point :around #'cape-wrap-purify))
I don’t really care for tabs, so let’s turn it off.
(setopt indent-tabs-mode nil)
By default, Eldoc is really spammy. Let’s have it write to a buffer instead, and read from that buffer when things are changed.
(use-package eldoc
:init
;; Bookkeeping
(defvar sonarmacs--last-spoken-eldoc-message nil "The last documentation that we spoke.")
(defun sonarmacs--speak-eldoc (docs interactive)
"Speak the eldoc documentation from the buffer.
If the documentation strings are the same as before, i.e., the symbol has not changed, do not respeak them; the user can go back and view the buffer if they like."
(when (and eldoc--doc-buffer (buffer-live-p eldoc--doc-buffer))
(with-current-buffer eldoc--doc-buffer
(unless (equal sonarmacs--last-spoken-eldoc-message eldoc--doc-buffer-docs)
(speechd-say-text (buffer-string) :priority 'important)
(setq sonarmacs--last-spoken-eldoc-message docs)))))
:ghook
('eldoc-display-functions (list #'sonarmacs--speak-eldoc #'eldoc-display-in-buffer))
:custom
(eldoc-echo-area-prefer-doc-buffer t)
:config
(remove-hook 'eldoc-display-functions #'eldoc-display-in-echo-area))
This will be built into Emacs as of version 29, but we can take advantage of it for now by ensuring it is fetched from the package interface.
In short, eglot is a bridge from Emacs to many LSP servers (rust-analyzer, Solargraph, etc). It aims to be lightweight and make use of Emacs internals (eldoc, flymake, xref, and friends).
(unless (and (version< emacs-version "29.0") (package-installed-p 'eglot))
(package-install 'eglot))
(use-package eglot
:custom
(eglot-autoshutdown t)
:general
(maplocal
:states 'normal
:keymaps 'eglot-mode-map
"l" '(:ignore t :which-key "LSP.")
"la" '(:ignore t :which-key "LSP actions.")
"laa" '(eglot-code-actions :which-key "Code actions.")
"lae" 'eglot-code-action-extract
"laf" '(eglot-format :which-key "Format the highlighted region.")
"laF" '(eglot-format-buffer :which-key "Format the current buffer.")
"lai" 'eglot-code-action-inline
"lao" '(eglot-code-action-organize-imports :which-key "Organize your imports.")
"laq" 'eglot-code-action-quickfix
"lar" '(eglot-rename :which-key "Rename the symbol under point.")
"laR" 'eglot-code-action-rewrite
"lr" '(eglot-reconnect :which-key "Reconnect to the LSP server.")))