diff options
Diffstat (limited to 'init.el')
| -rw-r--r-- | init.el | 2109 |
1 files changed, 2079 insertions, 30 deletions
@@ -1,34 +1,2083 @@ -;; This file is but bootstrapping. My real configuration file is -;; located at "jakob.org". +;;; init.el -*- lexical-binding: t; -*- +;;; +;;; Author: Jakob L. Kreuze <zerodaysfordays@sdf.org> +;;; URL: https://git.sr.ht/~jakob/.emacs.d/ +;;; +;;; What follows is my personal configuration for GNU Emacs. Alas, what once was +;;; a literate configuration has turned into a modularized set of Elisp files. I +;;; have a number of reasons for this. +;;; +;;; - There was a considerable amount of friction in changing my configuration. +;;; There are a few things that could remedy this, namely the suggestion of +;;; using poly-org as suggested in [1], but my biggest grievance is putting +;;; documentation between snippets of code. There were a couple of answers to +;;; the Emacs StackExchange question I asked on this [2], but the only +;;; suggestion that remotely close to what I was looking for was to use +;;; <<noweb>> syntax. Too much overhead for my tastes; I have to make a new +;;; named block every time I want to document part of a package configuration? +;;; Color me unexcited. Configuration reloading was also a massive pain, as +;;; some s-exps were split across blocks and I couldn't evaluate them with +;;; C-M-x. +;;; +;;; - My current method for tangling doesn't support byte compilation. The byte +;;; compiler will emit warnings for problematic Elisp. I can lint an Elisp +;;; file. With the way I was tangling, any errors in my code would be a +;;; pleasant surprise for the next time I tried to start Emacs. Furthermore, +;;; if I want to reap the benefits of using GCCEmacs [3], I'd better be +;;; compiling my sources. This is another grievance that could have been +;;; remedied with literate-elisp as suggested in [1], but I've decided it's +;;; too much work. I like the idea of being able to byte-compile with the +;;; native Emacs machinery better. +;;; +;;; - It's difficult to split up a literate configuration into multiple files, +;;; which is something I'd like to do for reasons of loading efficiency and +;;; organization. I think there's a reason that other sufficiently built-up +;;; configurations of note, such as Doom [4] and Purcell's [5], are written in +;;; plain ol' Elisp, leaving documentation to another file if stuctured text +;;; is really necessary. +;;; +;;; And, really, links were the only the only rich text I was using in the Org +;;; version. If I can survive writing emails without Org-style links, I can +;;; survive writing Elisp without Org-style links. I still love Org, but for +;;; right now, I'm not going to adopt it for anything outside of my personal +;;; information management workflow. Two conversations that influenced this +;;; decision and are worth documenting are [6] and [7]. +;;; +;;; Also worth noting is that there is no namespace (i.e. =jakob/= or =my-=) for +;;; functions defined in this file. I'm happy to use a consistent namespacing +;;; for packages I plan to publish on Melpa, but I don't see the point in a +;;; configuration file. +;;; +;;; [1]: https://www.reddit.com/r/emacs/comments/gfiadj/thoughts_on_having_a_literate_config/fpxqmf8/ +;;; [2]: https://emacs.stackexchange.com/questions/57608/split-code-across-multiple-src-blocks +;;; [3]: http://akrl.sdf.org/gccemacs.html +;;; [4]: https://github.com/hlissner/doom-emacs +;;; [5]: https://github.com/purcell/emacs.d +;;; [6]: https://www.reddit.com/r/emacs/comments/gfiadj/thoughts_on_having_a_literate_config/ +;;; [7]: https://www.colinmclear.net/posts/emacs-configuration/ -(defun eval-document (path) - "Tangles and evaluates all Emacs lisp code in the org file -located at PATH." - (let* ((body-list ()) - (src-block-regexp (concat - ;; (1) indentation (2) lang - "^\\([ \t]*\\)#\\+begin_src[ \t]+\\([^ \f\t\n\r\v]+\\)[ \t]*" - ;; (3) switches - "\\([^\":\n]*\"[^\"\n*]*\"[^\":\n]*\\|[^\":\n]*\\)" - ;; (4) header arguments - "\\([^\n]*\\)\n" - ;; (5) body - "\\([^\000]*?\n\\)??[ \t]*#\\+end_src"))) + + +;; straight.el is my package manager of choice. Here's the necessary bootstrap +;; code for that. + +(defvar bootstrap-version) +(let ((bootstrap-file + (expand-file-name "straight/repos/straight.el/bootstrap.el" user-emacs-directory)) + (bootstrap-version 5)) + (unless (file-exists-p bootstrap-file) + (with-current-buffer + (url-retrieve-synchronously + "https://raw.githubusercontent.com/raxod502/straight.el/develop/install.el" + 'silent 'inhibit-cookies) + (goto-char (point-max)) + (eval-print-last-sexp))) + (load bootstrap-file nil 'nomessage)) + +;; As for ensuring that the packages listed in this configuration are installed, +;; I prefer use-package. `straight-use-package-by-default' ensures that packages +;; are registered and built without reqiring an explicit `:straight t'. + +(straight-use-package 'use-package) +(setq straight-use-package-by-default t) + +;; `bind-key' is necessary for `:bind' in `use-package' declarations, and I +;; actually use it quite a bit in this configuration. + +(use-package bind-key) +(require 'bind-key) + +;; A handful of the Elisp snippets in this file depend on third-party libraries +;; which aren't loaded by default. + +;;; TODO + +;; Bind some keys for working with this configuration file. + +(defun config-goto () + "Switch to a buffer visiting the config file." + (interactive) + (find-file "~/.emacs.d/init.el")) + +(bind-key "C-c e" #'config-goto) + + +;;; +;;; The Core of GNU Emacs +;;; + +;; Having Emacs open to `*scratch*' feels as though it's inviting me to punch +;; out some Lisp forms and evaluate them. I like to pretend that my computers +;; are Lisp machines. + +;; TODO: Replace with dashboard.el? + +(setq inhibit-startup-message t + inhibit-startup-buffer-menu t + initial-scratch-message nil + initial-buffer-choice nil) + +;; A recurring theme in this configuration file is changing where Emacs packages +;; drop their various state files. 'no-littering.el' is a package that handles +;; this by creating 'var/' and 'etc/' directories in the Emacs directory, but +;; it's implemented in a way that pollutes the global namespace with variables +;; for packages that I don't use, and would require a significant amount of +;; configuration on my part anyway, so I've decided to do this manually, setting +;; relevant variables to subdirectories of XDG_CACHE_HOME and XDG_DATA_HOME. + +;; Save backup files to one directory instead of making a mess of the filesystem. + +(setq backup-directory-alist `(("." . "~/.cache/emacs"))) + +;; `auto-save', in addition to the actual files it saves, maintains another file +;; listing the files it's currently taking care of. + +(setq auto-save-list-file-prefix "~/.cache/emacs/auto-save-list/.saves-") + +;; Disable the creation of locking symlinks in the current directory. This is a +;; very opinionated choice, and probably isn't a good idea for most. + +(setq create-lockfiles nil) + +;; I don't use 'custom.el' to set variables, but a few of the packages I use do. +;; This snippet ensures that a massacre is not made of my init.el. + +(setq custom-file "~/.emacs.d/custom.el") +(when (file-exists-p custom-file) + (load custom-file)) + +;; I don't really care about persisting minibuffer history between sessions. +;; Even if I disable `savehist-mode', it still writes to disk, so the least I +;; can do is move the history file to somewhere that isn't .emacs.d. + +(savehist-mode -1) +(setq savehist-file "~/.cache/emacs/history") + +(use-package tramp + :straight nil + :init + (setq tramp-backup-directory-alist backup-directory-alist) + (setq-default tramp-persistency-file-name "~/.cache/emacs/tramp")) + +(use-package nsm + :straight nil + :init + (setq nsm-settings-file "~/.cache/emacs/network-security.data")) + +;; Use a consistent confirmation dialog of "y or n". + +(fset 'yes-or-no-p 'y-or-n-p) + +;; Automatically revert buffers and dired listings when something on disk +;; changes. + +(use-package autorevert + :config + (global-auto-revert-mode 1) + (setq global-auto-revert-non-file-buffers t + auto-revert-verbose nil)) + +;; "Command attempted to use minibuffer while in minibuffer" gets old fast. + +(setq enable-recursive-minibuffers t) + +;; Select help windows when I pop them so that I can kill them with <q>. + +(setq help-window-select t) + +;; Most *NIX tools work best when files are terminated with a newline. + +(setq require-final-newline t) + +;; Sentences should be separated by a single space. Treat two sentences as such +;; when filling. + +(setq sentence-end-double-space nil) + +;; I use the tab key, but I generally prefer space characters to tabstops and +;; like a four-character width for indentation. + +(setq-default indent-tabs-mode nil + tab-width 4) + +;; Modern conventions state that 80 characters is the standard width. + +(setq-default fill-column 80) + +;; When killing, add whatever's in the system clipboard if it isn't there +;; already, so I can get it back with <M-y>. + +(setq save-interprogram-paste-before-kill t) + +;; Enable the default-disabled functions that I use. + +(put 'downcase-region 'disabled nil) +(put 'upcase-region 'disabled nil) + +;; Enable useful visual queues. + +(column-number-mode t) + +;; Unbind <C-z> and <C-x C-z> so that I'm not accidentally calling =suspend-frame=. + +(global-unset-key (kbd "C-z")) +(global-unset-key (kbd "C-x C-z")) + +;; Replace `kill-word', `backward-kill-word', and `kill-visual-line' with +;; equivalents that do not copy the region into the kill ring. + +(defun delete-word (arg) + "Delete characters forward until encountering the end of a word. +With argument ARG, do this that many times." + (interactive "p") + (delete-region (point) (progn (forward-word arg) (point)))) + +(defun backward-delete-word (arg) + "Delete characters backward until encountering the beginning of a word. +With argument ARG, do this that many times." + (interactive "p") + (delete-word (- arg))) + +(defun delete-visual-line (&optional arg) + "Delete the rest of the visual line. +With prefix argument ARG, delete that many visual lines from point. +If ARG is negative, delete visual lines backward. +If ARG is zero, delete the text before point on the current visual +line." + (interactive "P") + ;; Like in `kill-line', it's better to move point to the other end + ;; of the kill before killing. + (let ((opoint (point)) + (delete-whole-line (and kill-whole-line (bolp)))) + (if arg + (vertical-motion (prefix-numeric-value arg)) + (end-of-visual-line 1) + (if (= (point) opoint) + (vertical-motion 1) + ;; Skip any trailing whitespace at the end of the visual line. + ;; We used to do this only if `show-trailing-whitespace' is + ;; nil, but that's wrong; the correct thing would be to check + ;; whether the trailing whitespace is highlighted. But, it's + ;; OK to just do this unconditionally. + (skip-chars-forward " \t"))) + (delete-region opoint (if (and delete-whole-line (= (following-char) ?\n)) + (1+ (point)) + (point))))) + +(bind-key "M-d" #'delete-word) +(bind-key "M-DEL" #'backward-delete-word) +(bind-key "C-k" #'delete-visual-line) + +;; I use <M-v> and <M-V> for movement, mostly, so having <prior> and <next> +;; bound to the same functions is a little redundant. What I prefer is to bind +;; them to functions for scolling by one line at a time instead. Sometimes +;; useful when perusing IRC logs. + +(define-minor-mode baby-scroll-mode + "Toggle `baby-scroll-mode'. +Minor mode which remaps <next> and <prior> to less extreme +scrolling functions." + :global t + nil "Baby Scroll" '(([next] . scroll-up-line) + ([prior] . scroll-down-line))) + +(baby-scroll-mode) + +;; Don't do jerky jumps when the cursor reaches the end of the window. Instead, +;; just scroll by one line. + +(setq scroll-conservatively most-positive-fixnum) + + + +;;; +;;; Appearance +;;; + +;; Use soft wrapping instead of "scrolling" when overflowing the width of a frame. + +(global-visual-line-mode t) + +;; The menu bar can be accessed with <M-`> if I really need it, so I don't see +;; the point in having it take up the space at the top. The rest of these rarely +;; see use from me, and I'd prefer to disable them and have access to the extra +;; screen real estate. + +(menu-bar-mode -1) +(scroll-bar-mode -1) +(tool-bar-mode -1) +(fringe-mode -1) + +;; Hacking is a lot more serene when visual stimulus is kept to a minimum. + +(blink-cursor-mode 0) + +;; Enable `prettify-symbols' globally. + +(global-prettify-symbols-mode t) + +;; This is snippet was taken verbatim from Emacs StackExchange [1] to enable +;; `prettify-symbols in Org source code blocks. Sadly, I can't think of a way to +;; implement this as advice. +;; +;; [1]: https://emacs.stackexchange.com/questions/17283/is-it-possible-to-get-prettified-symbols-in-org-mode-source-blocks + +(defun org-src-font-lock-fontify-block (lang start end) + "Fontify code block. +This function is called by emacs automatic fontification, as long +as `org-src-fontify-natively' is non-nil." + (let ((lang-mode (org-src--get-lang-mode lang))) + (when (fboundp lang-mode) + (let ((string (buffer-substring-no-properties start end)) + (modified (buffer-modified-p)) + (org-buffer (current-buffer)) pos next) + (remove-text-properties start end '(face nil)) + (with-current-buffer + (get-buffer-create + (concat " org-src-fontification:" (symbol-name lang-mode))) + (delete-region (point-min) (point-max)) + (insert string " ") ;; so there's a final property change + (unless (eq major-mode lang-mode) (funcall lang-mode)) + ;; Avoid `font-lock-ensure', which does not display fonts in + ;; source block. + (font-lock-fontify-buffer) + (setq pos (point-min)) + (while (setq next (next-single-property-change pos 'face)) + (put-text-property + (+ start (1- pos)) (1- (+ start next)) 'face + (get-text-property pos 'face) org-buffer) + (setq pos next)) + ;; Addition: also copy 'composition info for prettified symbols + (setq pos (point-min)) + (while (setq next (next-single-property-change pos 'composition)) + (put-text-property + (+ start (1- pos)) (1- (+ start next)) 'composition + (get-text-property pos 'composition) org-buffer) + (setq pos next)) + ;; End addition + ) + (add-text-properties + start end + '(font-lock-fontified t fontified t font-lock-multiline t)) + (set-buffer-modified-p modified))))) + +;; Although I'm usually not a fan of treating non-printable ASCII like this, the +;; lines act as sort more of a cue for me to use <C-x [> and <C-x ]> than a +;; plain ol' '^L'. + +(use-package page-break-lines + :demand t + :config + (global-page-break-lines-mode)) + + +;;; +;;; Theme +;;; + +;; This is my custom theme, the code for which is very loosely adapted from +;; moe-theme.el [1], but most of the palette has been altered to emulate the +;; appearance of my old setup of Emacs in a terminal emulator. +;; +;; [1]: https://github.com/kuanyui/moe-theme.el + +(deftheme chayote + "Inspired by the appearance of Emacs in my terminal emulator.") + +;; The colors have been extracted into a variable, `palette', and a macro, +;; `with-palette', to provide access to the theme's color definitions without +;; significantly polluting the global namespace. To help me in modifying the +;; theme, I've defined a function similar to `list-colors-display', but for +;; showing the colors in the palette I've defined. + +(defvar palette + '((yellow-1 "#ffd787") (yellow-2 "#ffd700") (yellow-3 "#c4a000") + (yellow-3-5 "#aaaa11") (yellow-4 "#875f00") + + (orange-1 "#ffaf87") (orange-2 "#ff8700") (orange-3 "#ff5d17") + (orange-4 "#d75f00") (orange-5 "#af5f00") + + (magenta-1 "#ff7bbb") (magenta-2 "#ff4ea3") (magenta-3 "#ff1f8b") + + (green-1 "#87ff87") (green-2 "#a1db00") (green-3 "#5faf00") + (green-4 "#008700") (green-5 "#005f00") + + (cyan-1 "#afd7d7") (cyan-2 "#87ffd7") (cyan-3 "#00d7af") + (cyan-4 "#00ac8a") (cyan-5 "#5faf87") (cyan-6 "#005f5f") + (cyan-7 "#236f73") + + (blue-1 "#1fb6d3") (blue-2 "#1f5bff") (blue-3 "#005f87") + (blue-4 "#5f7ab1") (blue-5 "#0000af") (blue-6 "#00005f") + + (purple-1 "#d18aff") (purple-2 "#af5fff") (purple-3 "#9a08ff") + (purple-4 "#6c0099") + + (red-1 "#ef2929") (red-2 "#dd0000") (red-3 "#a40000") + (red-4 "#5f0000") + + (white-1 "#f0e9dc") (white-2 "#c6c6c6") (white-3 "#b2b2b2") + + (black-1 "#a8a8a8") (black-2 "#8a8a8a") (black-2-5 "#6c6c6c") + (black-3 "#4e4e4e") (black-4 "#3a3a3a") (black-5 "#303539") + (black-6 "#000000") + + (LIGHT_BG "#fdfde7") (white-0 "#eeeeee") + + (green-02 "#5fd700") (green-01 "#d7ff00") (green-0 "#d7ff5f") + (green-00 "#d7ff87") + + (cyan-0 "#d7ffd7") + + (blue-01 "#c3c9f8") (blue-0 "#afd7ff") (blue-00 "#d7d7ff") + + (yellow-0 "#ffff87") (yellow-00 "#ffffaf") + + (purple-0 "#af87ff") (purple-00 "#e6a8df") + + (red-0 "#ff4b4b") (red-00 "#ffafaf") + + (magenta-0 "#ffafd7") (magenta-00 "#ffd7ff") + + (orange-0 "#ffaf87") (orange-00 "#ffd787") (orange-000 "#ffd7af") + + (linum-dark "#87875f") (linum-light "#d7d7af")) + "The palette of colors in use with my 'chayote' theme.") + +(defmacro with-palette (&rest form) + "Evaluate `FORM' in a `let' containing the theme's colors." + `(let ((class '((class color) (min-colors 89))) + ,@palette) + ,@form)) + +(defun display-palette () + "Display names of colors in `PALETTE', and show what they look like." + (interactive) + (with-help-window "*Chayote Colors*" + (with-current-buffer standard-output + (erase-buffer) + (dolist (color palette) + (let* ((name (symbol-name (first color))) + (value (second color))) + (put-text-property 0 (length name) + 'font-lock-face `(:foreground ,value) + name) + (insert name) + (insert (propertize " " 'display '(space :align-to (- right 9)))) + (insert " ") + (put-text-property 0 (length value) + 'font-lock-face `(:background ,value) + value) + (insert value) + (terpri))) + (set-buffer-modified-p nil) + (setq truncate-lines t)))) + +(with-palette + (custom-theme-set-faces + 'chayote + `(default ((,class (:foreground ,white-1 :background ,black-5 :family "Terminus" :height 90)))) + `(italic ((,class (:italic t)))) + `(cursor ((,class (:background ,white-0)))) + `(variable-pitch ((,class (:inherit default)))) + + `(link ((,class (:foreground ,blue-1 :underline t)))) + + ;; Highlighting faces + `(highlight ((,class (:background ,black-3)))) + `(hl-line ((,class (:background ,black-4)))) + `(highlight-symbol-face ((,class (:background ,green-5)))) + `(region ((,class (:background ,blue-4)))) + `(secondary-selection ((,class (:background ,blue-3 :foreground ,white-0)))) + `(isearch ((,class (:foreground ,white-1 :background ,orange-3)))) + `(lazy-highlight ((,class (:background ,magenta-3 :foreground ,white-1)))) + `(trailing-whitespace ((,class (:background ,red-3)))) + `(show-paren-match ((,class (:background ,blue-3 :foreground nil)))) + `(header-line ((,class (:background ,blue-3 :foreground ,white-0)))) + `(help-argument-name ((,class (:foreground ,magenta-1 :italic t)))) + `(eldoc-highlight-function-argument ((,class (:foreground ,green-01 :bold t :underline t :background ,green-5)))) + + ;; Mode line & frames' faces + `(mode-line ((,class (:box nil :background ,white-1 :foreground ,black-5)))) + `(mode-line-inactive ((,class (:background ,white-3 :foreground ,black-5)))) + `(mode-line-buffer-id ((,class (:box nil :foreground ,black-5 :background nil)))) + `(vertical-border ((,class (:foreground ,black-3 :background ,black-3)))) + + ;; Escape and prompt faces + `(minibuffer-prompt ((,class (:foreground ,blue-1)))) + `(escape-glyph ((,class (:foreground ,yellow-3)))) + `(error ((,class (:foreground ,red-0)))) + `(warning ((,class (:foreground ,orange-1)))) + `(success ((,class (:foreground ,green-1)))) + + ;; Font lock faces + `(font-lock-builtin-face ((,class (:foreground ,cyan-1)))) + `(font-lock-comment-delimiter-face ((,class (:inherit font-lock-comment-face)))) + `(font-lock-comment-face ((,class (:foreground ,orange-2)))) + `(font-lock-constant-face ((,class (:foreground ,cyan-2)))) + `(font-lock-doc-face ((,class (:inherit font-lock-string-face)))) + `(font-lock-doc-string-face ((,class (:inherit font-lock-string-face)))) + `(font-lock-function-name-face ((,class (:foreground ,blue-0)))) + `(font-lock-keyword-face ((,class (:foreground ,blue-1)))) + `(font-lock-negation-char-face ((,class (:foreground ,red-0)))) + `(font-lock-preprocessor-face ((,class (:inherit font-lock-builtin-face)))) + `(font-lock-regexp-grouping-backslash ((,class (:foreground ,yellow-1)))) + `(font-lock-regexp-grouping-construct ((,class (:foreground ,purple-1)))) + `(font-lock-string-face ((,class (:foreground ,orange-1)))) + `(font-lock-type-face ((,class (:foreground ,green-1)))) + `(font-lock-variable-name-face ((,class (:foreground ,yellow-1)))) + `(font-lock-warning-face ((,class (:weight bold :foreground ,red-2)))) + + ;; Info + `(Info-quoted ((,class (:inherit default)))) + + ;; company + `(company-tooltip ((,class (:background ,black-4)))) + `(company-scrollbar-bg ((,class (:background ,black-4)))) + `(company-scrollbar-fg ((,class (:background ,orange-2)))) + `(company-tooltip-selection ((,class (:background ,black-3)))) + `(company-tooltip-common ((,class (:foreground ,blue-1)))) + `(company-tooltip-annotation ((,class (:foreground ,cyan-1)))) + + ;; AUCTeX + `(font-latex-sedate-face ((,class (:inherit font-lock-keyword-face)))) + + ;; Gnus + `(gnus-group-mail-3 ((,class (:foreground ,green-2)))) + + ;; org-mode + `(org-document-info-keyword ((,class (:foreground ,red-0)))) + `(org-document-title ((,class (:inherit org-document-info-keyword)))) + `(org-document-info ((,class (:inherit org-document-info-keyword)))) + `(org-meta-line ((,class (:inherit org-document-info-keyword)))) + `(org-archived ((,class (:slant italic)))) + `(org-checkbox ((,class (:background ,white-2 :foreground ,black-3 + :box (:line-width 1 :style released-button))))) + `(org-date ((,class (:foreground ,green-3 :underline t)))) + `(org-done ((,class (:foreground ,green-4 :background ,green-00 + :box (:line-width 1 :style none))))) + `(org-todo ((,class (:foreground ,red-3 :background ,orange-0 + :box (:line-width 1 :style none))))) + `(org-level-1 ((,class (:foreground ,purple-00)))) + `(org-level-2 ((,class (:foreground ,blue-0)))) + `(org-level-3 ((,class (:foreground ,cyan-3)))) + `(org-level-4 ((,class (:foreground ,green-02)))) + `(org-level-5 ((,class (:foreground ,red-2)))) + `(org-level-6 ((,class (:foreground ,orange-2)))) + `(org-level-7 ((,class (:foreground ,magenta-2)))) + `(org-level-8 ((,class (:foreground ,yellow-2)))) + `(org-tag ((,class (:background ,black-1 :foreground ,white-0)))) + `(org-column ((,class (:background ,yellow-00 :foreground ,black-3)))) + `(org-column-title ((,class (:background ,blue-0 :foreground ,black-5 :underline t)))) + `(org-agenda-structure ((,class (:foreground ,cyan-4 :background nil :bold nil)))) + `(org-deadline-announce ((,class (:foreground ,red-3)))) + `(org-scheduled ((,class (:foreground ,black-2)))) + `(org-scheduled-previously ((,class (:foreground ,red-1)))) + `(org-scheduled-today ((,class (:foreground ,blue-2)))) + `(org-special-keyword ((,class (:foreground ,green-3)))) + `(org-table ((,class (:background ,black-4 :foreground ,white-3)))) + ;; `(org-time-grid ((,class (:foreground ,white-4)))) + `(org-upcoming-deadline ((,class (:foreground ,red-3)))) + ;; `(org-warning ((,class (:foreground ,white-0 :background ,red-3)))) + `(org-warning ((,class (:foreground ,white-0 :background ,red-4)))) + `(org-formula ((,class (:foreground ,purple-2)))) + `(org-headline-done ((,class (:foreground ,green-2)))) + `(org-hide ((,class (:foreground ,LIGHT_BG)))) + `(org-code ((,class (:foreground ,blue-2 :background ,white-1)))) + ;; `(org-link ((,class (:foreground ,blue-2 :underline ,blue-2)))) + `(org-link ((,class (:inherit link)))) + `(org-footnote ((,class (:foreground ,magenta-3)))) + `(org-ellipsis ((,class (:foreground ,red-2)))) + `(org-agenda-clocking ((,class (:foreground ,blue-3 :background ,blue-0)))) + `(org-agenda-date ((,class (:foreground ,blue-1 :background nil :underline nil)))) + `(org-agenda-date-weekend ((,class (:foreground ,purple-2 :background nil :slant italic :bold nil)))) + `(org-agenda-date-today ((,class (:foreground ,blue-0 :background nil :underline t)))) + `(org-agenda-column-dateline ((,class (:foreground ,white-0 :background ,yellow-0 :underline t)))) + `(org-agenda-todo ((,class (:foreground ,white-0 :background ,red-2)))) + `(org-agenda-done ((,class (:foreground ,green-3 :background ,nil)))) + `(org-agenda-dimmed-todo-face ((,class (:foreground ,white-0 :background ,red-2)))) + `(org-priority ((,class (:foreground ,red-1 :background ,nil)))) + `(org-block ((,class (:foreground ,white-1 :background ,black-4)))) + `(org-block-begin-line ((,class (:foreground ,black-3)))) + `(org-block-end-line ((,class (:foreground ,black-3)))) + `(org-block-background ((,class (:background ,white-1)))) + ;; `(org-block-begin-line ((,class (:foreground ,blue-3 :background ,blue-0)))) + ;; `(org-block-end-line ((,class (:foreground ,white-2 :background ,white-1)))) + `(org-quote ((,class (:foreground ,blue-3 :background ,blue-00)))) + `(org-mode-line-clock ((,class (:foreground ,blue-3 :background ,blue-0 :bold t)))) + `(org-mode-line-clock-overrun ((,class (:foreground ,white-0 :background ,red-1 :bold t)))) + ;; `(org-verbatim ((,class (:foreground ,blue-3 :background ,blue-00 :bold nil)))) + `(org-verbatim ((,class (:foreground ,blue-1 :background ,black-4)))) + + ;; erc + `(erc-input-face ((,class (:foreground ,white-1)))) + `(erc-button ((,class (:inherit link)))) + + ;; smartparens + `(sp-pair-overlay-face ((,class (:inherit default))))) + + (custom-theme-set-variables + 'chayote + `(ansi-color-names-vector [,black-5 ,red-0 ,green-0 ,yellow-1 + ,blue-1 ,purple-1 ,blue-0 ,white-1]))) + + +;;; +;;; Modeline +;;; + +;; My implementation of a clock for the modeline. The modeline is refreshed by a +;; timer to ensure that it doesn't go too stale, as it could theoretically go +;; several minutes without being updated. + +(defun mode-line-time () + (format-time-string "%Y-%m-%d %H:%M")) + +(let ((timer (run-at-time t 60.0 #'ignore))) + (timer-set-function timer + (lambda () + (force-mode-line-update)))) + +;; For some time, I was using my own implementation of a modeline battery +;; monitor. However, the constant calls to `battery-linux-sysfs' were +;; interfering with the way that EXWM handles input focus, and I've since moved +;; towards reusing `display-battery-mode'. Because strings in `mode-line-format' +;; can contain control sequences, the percent symbol has to be escaped twice -- +;; once for `battery-format', and once for `mode-line-format'. +;; +;; Obviously, my tower lacks a battery, so I only enable this on my laptop. + +(when (string= (system-name) "Upsilon") + (require 'battery) + (display-battery-mode +1) + (setq battery-mode-line-format "%L: %t, %p%%%%")) + +(defun maybe-elide (s &optional max-length) + "Truncate `S' to `MAX-LENGTH'. +Return a string derived from `S', possible elided, that is at +most 1MAX-LENGTH characters long." + (let ((max-length (or max-length 30))) + (if (> (length s) max-length) + (concat (substring s 0 (- max-length 4)) " ...") + s))) + +(defun escape-mode-line-construct (s) + "Sanitize `S' for use in `mode-line-format'. +Ensure that no part of S will be interpreted as a modeline format +specifier." + (replace-regexp-in-string (regexp-quote "%") "%%" s t t)) + +(defun mode-line-render (left right) + "Return a string of `window-width' length containing LEFT, and +RIGHT aligned respectively." + (let* ((right (format-mode-line right)) + (left (maybe-elide (format-mode-line left) + (- (window-width) (length right) 1))) + (padding (- (window-width) (length left) (length right) 1)) + (padding (if (wholenump padding) (make-string padding ?\ )))) + (if padding + (escape-mode-line-construct (concat " " left padding right)) + (concat " " left right)))) + +(setq-default mode-line-format + '((:eval + (mode-line-render + (cond ((eq major-mode 'exwm-mode) + (concat "%b: " exwm-title)) + ((eq major-mode 'pdf-view-mode) + (concat "%b [" + (number-to-string (pdf-view-current-page)) + "/" + (number-to-string (pdf-cache-number-of-pages)) + "]")) + (t + (concat "%b [%m] [%*] Ln: %l/" + (number-to-string (1+ (count-lines 1 (point-max)))) + " Col: %c"))) + (cond ((string= (system-name) "Upsilon") + (format "%s | %s" + (mode-line-time) + battery-mode-line-string)) + (t (mode-line-time))))))) + + +;;; +;;; Ivy +;;; + +;; As suggested by a friend [1], I tried out `ivy' as a replacement for `ido.' +;; It was love at first sight. It's snappy, and works even better than `ido' +;; after setting `exwm-workspace-show-all-buffers' and +;; `exwm-layout-show-all-buffers'. +;; +;; [1]: https://icosahedron.website/@zge/101330910258980695 + +(use-package ivy + ;; Configure how the =completing-read= is displayed. + :init + (setq ivy-height 5 ivy-count-format "") + + ;; Enable Ivy globally. + :config + (ivy-mode 1)) + +;; `counsel' is a nice Ivy-based frontend for a couple of interactive commands. + +(use-package counsel + :demand t + :bind + ("C-x b" . counsel-switch-buffer)) + +;; Use Swiper instead of isearch. + +;; TODO: Should I re-enable this? + +;; (use-package swiper +;; :bind +;; ("C-r" . swiper) +;; ("C-s" . swiper)) + + +;;; +;;; Imenu +;;; + +(use-package imenu + :bind ("C-c s" . imenu)) + + +;;; +;;; Yasnippet +;;; + +;; Load all snippets when `yas-minor-mode' is enabled. + +(use-package yasnippet + :demand t + :hook + (yas-minor-mode . yas-reload-all)) + + +;;; +;;; Avy +;;; + +;; Set up keybindings for Avy. + +(use-package avy + :bind + ("M-s" . avy-goto-char)) + +;; Install a link-selection package for Avy, and create a binding for `org-mode'. + +;; TODO: Should I re-enable this? + +;; (use-package ace-link +;; :after org +;; :init (ace-link-setup-default) +;; :bind (:map org-mode-map +;; ("C-c C-o" . ace-link-org))) + + + +;;; +;;; pass +;;; + +;; TODO: Replace with pass.el? + +;; Here's a snippet for interactively pulling a password from password-store, so +;; I'm not spawning a terminal every time I need to invoke my password manager. + +(defvar password-store-directory "~/.password-store" + "Directory containing passwords managed by `pass'.") + +(defun pass-password-files () + "Returns the paths of every password managed by `pass'." + (let ((prefix-length (1+ (length (expand-file-name password-store-directory)))) + (suffix-length (length ".gpg"))) + (map 'list + #'(lambda (path) (cl-subseq path prefix-length (- suffix-length))) + (directory-files-recursively password-store-directory ".gpg$")))) + +(defun pass () + "Prompt for a password and copy it to the clipboard. +Passwords are taken from `password-store-directory'." + (interactive) + (let* ((account (completing-read "Account: " (pass-password-files))) + (path (concat password-store-directory "/" account ".gpg"))) (with-temp-buffer (insert-file-contents path) - (goto-char (point-min)) - (while (re-search-forward src-block-regexp nil t) - (let ((lang (match-string 2)) - (args (match-string 4)) - (body (match-string 5))) - (when (and (or (string= lang "emacs-lisp") (string= lang "elisp")) - (not (string-match-p ":tangle\\s-+no" args))) - (add-to-list 'body-list body))))) - (let ((buffer (generate-new-buffer "*jakob-tangle-temporary*"))) - (set-buffer buffer) - (apply 'insert (reverse body-list)) - (eval-buffer) - (kill-buffer))) - (message "Successfully evaluated %s" path)) + (clipboard-kill-ring-save 1 (1- (buffer-end 1)))) + (message "Password for %s copied to clipboard." account))) + + +;;; +;;; 8-ball +;;; + +;; A little snippet for getting an answer to a dumb question. Responses were +;; lifted from Wikipedia's page [1] about the toy. +;; +;; [1]: https://en.wikipedia.org/wiki/Magic_8-Ball + +(defun consult () + "Consult the magic 8-ball. +Interactive function to consult the magic 8-ball for answers to +very important questions." + (interactive) + (let ((responses (list "It is certain." "It is decidedly so." + "Without a doubt." "Yes - definitely." + "You may rely on it." "As I see it, yes." + "Most likely." "Outlook good." "Yes." + "Signs point to yes." "Reply hazy, try again." + "Ask again later." "Better not tell you now." + "Cannot predict now." "Concentrate and ask again." + "Don't count on it." "My reply is no." + "My sources say no." "Outlook not so good." + "Very doubtful."))) + (message (elt responses (mod (random) (length responses)))))) + + +;;; +;;; Window Management +;;; + +;; Yes, I use Emacs as my window manager. You should, too! + +(use-package exwm + :config + (exwm-enable) + + ;; I find that four workspaces is enough to start out with, and I like being + ;; able to use my X windows across workspaces. + :init + (setq exwm-workspace-number 4 + exwm-workspace-show-all-buffers t + exwm-layout-show-all-buffers t) + + ;; "Global key bindings" in EXWM work essentially anywhere, including buffers + ;; that are currently in char mode. I've taken this as an opportunity to + ;; incorporate windmove to my workflow, now that I have the super key at my + ;; disposal. + :init + (require 'windmove) + + ;; Define some interactive functions for dealing with a multi-head setup. + (when (string= (system-name) "Epsilon") + (defun my-windmove-right () + (interactive) + (if (windmove-find-other-window 'right) + (windmove-right) + (if (= exwm-workspace-current-index 1) + (exwm-workspace-switch 0) + (exwm-workspace-switch 1)))) + (defun my-windmove-left () + (interactive) + (if (windmove-find-other-window 'left) + (windmove-left) + (if (= exwm-workspace-current-index 1) + (exwm-workspace-switch 0) + (exwm-workspace-switch 1))))) + + ;; And if we haven't defined those functions because we aren't on a multi-head + ;; setup, define aliases to the underlying windmove functions. + (when (and (not (fboundp #'my-windmove-left)) + (not (fboundp #'my-windmove-right))) + (defalias #'my-windmove-left #'windmove-left) + (defalias #'my-windmove-right #'windmove-right)) + + (setq exwm-input-global-keys + `(([?\s-r] . exwm-reset) + ([?\s-a] . my-windmove-left) + ([?\s-d] . my-windmove-right) + ([?\s-w] . windmove-up) + ([?\s-s] . windmove-down) + ([?\s-p] . (lambda (command) + (interactive (list (read-shell-command "$ "))) + (start-process-shell-command command nil command))) + ([?\s-X] . (lambda () + (interactive) + (start-process "" nil "slock"))) + ([?\s-f] . (lambda () + (interactive) + (start-process "" nil "firefox"))) + (,(kbd "s-<return>") . (lambda () + (interactive) + (start-process "" nil "alacritty"))) + + ;; Keys are offset such that <s-1> corresponds to the 0th workspace. + ,@(mapcar (lambda (i) + `(,(kbd (format "s-%d" i)) . + (lambda () + (interactive) + (exwm-workspace-switch-create ,(- i 1))))) + (number-sequence 1 9)))) + + ;; Ah, yes. Simulation keys, the reason I decided to look at EXWM in the first + ;; place. + :init + (setq exwm-input-simulation-keys + '(([?\C-b] . [left]) + ([?\M-b] . [C-left]) + ([?\C-f] . [right]) + ([?\M-f] . [C-right]) + ([?\C-p] . [up]) + ([?\C-n] . [down]) + ([?\C-a] . [home]) + ([?\C-e] . [end]) + ([?\M-v] . [prior]) + ([?\C-v] . [next]) + ([?\C-d] . [delete]) + ([?\C-k] . [S-end delete]) + + ([?\C-w] . [?\C-x]) + ([?\M-w] . [?\C-c]) + ([?\C-y] . [?\C-v]) + + ([?\C-s] . [?\C-f]))) + + ;; The tab management functionality of Firefox's Saka Key extension is + ;; limited, so I'm using simulation keys instead. I've also disabled + ;; simulation keys for alacritty because it doesn't play nicely with readline. + :hook + (exwm-manage-finish . (lambda () + (when (and exwm-class-name + (string= exwm-class-name "Firefox")) + (exwm-input-set-local-simulation-keys + (concatenate 'list + exwm-input-simulation-keys + '(([?\C-q] . [?\C-w]))))) + (when (and exwm-class-name + (string= exwm-class-name "Alacritty")) + (exwm-input-set-local-simulation-keys nil)))) + + ;; EXWM's buffers are named '*EXWM*' upon creation, which makes switching + ;; between windows a pain. This snippet renames buffers when their class or + ;; title changes. + :hook + (exwm-update-class . (lambda () + (exwm-workspace-rename-buffer exwm-class-name))) + (exwm-update-title . (lambda () + (when (not exwm-instance-name) + (exwm-workspace-rename-buffer exwm-title)))) + + + ;; Start the Emacs daemon so that 'emacsclient' can be used from the command + ;; line to pop a new buffer for editing. + :config + (server-start) + + ;; Configure the resolution of my monitors and let EXWM know about them. + :config + (require 'exwm-randr) + (defun hdmi-out () + (and (string= (system-name) "Upsilon") + (save-window-excursion + (with-temp-buffer + (shell-command "xrandr" (current-buffer)) + (cl-search "HDMI-1 connected" (buffer-string)))))) + (setq exwm-randr-workspace-monitor-plist + (if (string= (system-name) "Epsilon") + '(0 "DVI-D-0" 1 "HDMI-A-3") + '(0 (if (hdmi-out) "HDMI-1" "eDP-1")))) + (add-hook 'exwm-randr-screen-change-hook + (lambda () + (start-process-shell-command + "xrandr" nil + (if (string= (system-name) "Epsilon") + "xrandr --output DVI-D-0 --primary --left-of HDMI-A-0" + (if (hdmi-out) + "xrandr --output HDMI-1 --mode 1920x1080 --output eDP-1 --off" + "xrandr --output eDP-1 --mode 1920x1080"))))) + (exwm-randr-enable)) + +;; Some keybindings for buffer-move that are analagous to my keybindings for +;; windmove above. + +(use-package buffer-move + :bind + ("M-s-w" . buf-move-up) + ("M-s-a" . buf-move-left) + ("M-s-s" . buf-move-down) + ("M-s-d" . buf-move-right)) + +;; EXWM has issues popping any sort of pinentry window, so I just let Emacs do +;; pinentry's job. + +(use-package epa + :straight nil + :config + (setq epa-pinentry-mode 'loopback)) + +;; And on that note, allow 'gpg-agent' to query Emacs for my passphrase. + +(use-package pinentry + :demand t + :config + (setenv "INSIDE_EMACS" (format "%s,comint" emacs-version)) + (pinentry-start)) + + +;;; +;;; System +;;; + +;; Set up keybindings for calling 'ag'. + +(use-package ag + :bind + ("C-c k" . counsel-ag)) + +;; Hide the targets of symbolic links in `dired' listings. This is a visual +;; preference of mine. + +(use-package dired + :straight nil + :init + (setq-default dired-listing-switches "-alL")) + +(use-package dired-rmjunk + ;; Set up a keybinding for invoking `dired-rmjunk' from `dired-mode'. + :bind + (:map dired-mode-map + ("r" . dired-rmjunk)) + + ;; Implement my customizations from =rmshit.py=. + :init + (setq dired-rmjunk-patterns + '(".recently-used" + ".local/share/recently-used.xbel" + ".thumbnails" + ".gconfd" + ".gconf" + ".local/share/gegl-0.2" + ".gstreamer-0.10" + ".esd_auth" + ".config/enchant" + ".parallel" + ".dbus" + "ca2" + "ca2~" + ".nv/" + ".npm/" + ".java/" + ".oracle_jre_usage/" + ".jssc/" + ".tox/" + ".pylint.d/" + ".guile_history" + ".sqlite_history" + ".python_history" + ".hy-history" + ".lesshst" + ".audacity-data" + ".wget-hsts" + ".scapy_history" + ".gdb_history" + ".binwalk" + ".lyrics" + ".slime-history.eld" + ".w3m"))) + + +;;; +;;; Documents +;;; + +;; Ensure that my Aspell "personal dictionary" is included when invoking ispell. + +(setq ispell-personal-dictionary "~/.aspell.en.pws") + +(use-package org + :straight org-plus-contrib + + ;; File system of hygene. + :init + (setq-default org-id-locations-file "~/.cache/emacs/org-id-locations" + org-latex-preview-ltxpng-directory "~/.cache/emacs/ltximg/" + org-preview-latex-image-directory "~/.cache/emacs/ltximg/") + + ;; General appearance and quality-of-life settings. + :init + (setq org-return-follows-link t + org-startup-folded nil + org-cycle-separator-lines -1 + org-startup-with-inline-images t + org-hide-emphasis-markers t + org-tags-column 100 + org-yank-folded-subtrees nil + org-link-elisp-confirm-function nil) + + ;; Keep everything in column zero, instead of indenting according to the depth + ;; of the preceding headline. + + :init + (setq org-adapt-indentation nil) + + ;; Have <tab> indent according to the indentation rules of the corresponding + ;; mode in an =org-src= block, and start indentation on column 0. Taken from + ;; [1] and [2]. + ;; + ;; [1]: https://stackoverflow.com/questions/15773354/indent-code-in-org-babel-src-blocks + ;; [2]: https://emacs.stackexchange.com/questions/9472/org-mode-source-block-doesnt-respect-parent-buffer-indentation + :init + (setq org-src-tab-acts-natively t + org-edit-src-content-indentation 0) + + ;; Reuse the current window when I punch in <C-c '>, rather than mess up my + ;; window configuration. + :init + (setq org-src-window-setup 'current-window) + + ;; Enable Geiser evaluation for Scheme blocks. + :config + (org-babel-do-load-languages + 'org-babel-load-languages + '((scheme . t))) + + ;; Scale up embedded LaTeX. The default size is unreadable. + :config + (setq org-format-latex-options + (plist-put org-format-latex-options :scale 1.5)) + + ;; Don't interpret text following underscores as subscript text unless it is + ;; enclosed in braces. + :init + (setq org-use-sub-superscripts "{}") + + ;; Enable Yasnippet and Flyspell. + :hook + (org-mode . yas-minor-mode-on) + (org-mode . flyspell-mode) + + ;; Replace my Imenu binding with `org-goto,' as it allows me to select parent + ;; headings. + :init + (setq org-goto-interface 'outline-path-completionp) + (setq org-outline-path-complete-in-steps nil) + + :bind + (:map org-mode-map ("C-c s" . org-goto)) + + ;; Global keybindings for my Org workflow. + :bind + ("C-c a" . (lambda () (interactive) (org-agenda nil "d"))) + ("C-c c" . (lambda () (interactive) (org-capture nil "c"))) + ("C-c t" . (lambda () (interactive) (org-capture nil "t"))) + + ;; Use Counsel's functionality for adding tags. + :bind + (:map org-mode-map ("C-c C-q" . counsel-org-tag)) + + ;; I maintain a personal wiki using Org and I like to link between pages. This + ;; is just a convenience link type, rooted at the base of my notes folder so + ;; that I can easily refer to other pages from subdirectories of + ;; `notes-directory'. + :config + (defvar notes-directory "~/Notes" + "Where all the pages of my 'personal wiki' are stored.") + + (defun org-notes-follow (link) + "Open LINK, an Org link from a notes buffer. +Visits LINK, or if LINK names a subdirectory of +`notes-directory', the 'Index.org' in LINK." + (let ((directory-p (file-directory-p (expand-file-name link notes-directory)))) + (find-file (expand-file-name (if directory-p "Index.org" (concat link ".org")) + notes-directory)))) + + (org-link-set-parameters "note" :follow #'org-notes-follow) + + ;; Code for interactively navigating the personal wiki. + :config + (defvar notes-directory-ignore + '("." ".." ".git" ".gitignore" ".strfolder" "Drill" "GTD") + "Files and directories in `notes-directory' to exclude from + wiki interactions.") + + (defun notes-file-is-page (filename) + "Returns whether or not FILENAME names a page rather than a category." + (string= (cl-subseq filename (- (length filename) 4)) ".org")) + + (defun notes-file-should-ignore (filename) + "Returns whether or not a file in `notes-directory' should be ignored. + Members of `notes-directory-ignore' are ignored." + (member filename notes-directory-ignore)) + + (defun notes-hierarchy-paths (&optional category) + "Returns page and category paths relative to `notes-directory'. + The resultant list does not include files that satisfy the + `notes-file-should-ignore' predicate." + (let* ((path (if category + (concat notes-directory "/" category) + notes-directory)) + (contents (cl-remove-if #'notes-file-should-ignore + (directory-files path))) + (pages (cl-remove-if-not #'notes-file-is-page contents)) + (categories (cl-remove-if-not #'(lambda (subcategory) + (file-directory-p (concat path "/" subcategory))) + contents)) + (categories (if category + (map 'list + #'(lambda (subcategory) (concat category "/" subcategory)) + categories) + categories))) + (append (if category + (map 'list #'(lambda (page) (concat category "/" page)) pages) + pages) + categories + (apply #'append (map 'list #'notes-hierarchy-paths categories))))) + + (defun notes-hierarchy () + "Returns every page and category in `notes-directory'. + The result contains lists of the form (NAME PATH IS-PAGE-P), and + does not include files that satisfy the + `notes-file-should-ignore' predicate." + (map 'list + #'(lambda (path) (list + (cond ((cl-search "Index.org" path) + (let ((name (cl-subseq path 0 -9))) + (if (string= name "") + "/" + name))) + ((notes-file-is-page path) (cl-subseq path 0 -4)) + (t path)) + (concat notes-directory "/" path) + (notes-file-is-page path))) + (notes-hierarchy-paths))) + + (defun notes-page-goto () + "Interactively find a page in `notes-directory'." + (interactive) + (let* ((hierarchy (notes-hierarchy)) + (hierarchy (cl-remove-if-not #'third hierarchy)) + (name (ivy-read "Find file: " (map 'list #'first hierarchy))) + (item (cl-find-if #'(lambda (item) (string= name (first item))) hierarchy))) + (if item + (find-file (second item)) + (let ((path (concat notes-directory "/" name ".org"))) + (find-file path))))) + + ;; Set up a key binding for quickly querying the wiki. + :bind + ("C-c w" . notes-page-goto) + + ;; Low-friction means of adding things to my GTD inbox. + :config + (setq org-capture-templates + `(("t" "TODO" entry (file "~/Notes/GTD/Inbox.org") + "* TODO %?\n"))) + + + ;; A rather convoluted agenda setup to show me: + ;; - The tasks I've marked as needing to be done today. + ;; - The inbox items I need to process. + ;; - My daily habits (implemented as recurring Org TODO's). + ;; - A view of deadlines and scheduled items for the week ahead. + :init + (setq org-agenda-files '("~/Notes/GTD/Habits.org" + "~/Notes/GTD/Inbox.org" + "~/Notes/GTD/Actionable.org")) + + (let ((entry-done + '(lambda () (org-agenda-skip-entry-if 'todo 'done))) + + (subtree-is-habit + '(lambda () + (let ((subtree-end (save-excursion (org-end-of-subtree t)))) + (if (string= (org-entry-get nil "STYLE") "habit") + subtree-end + nil)))) + + (subtree-is-not-habit + '(lambda () + (let ((subtree-end (save-excursion (org-end-of-subtree t)))) + (if (not (string= (org-entry-get nil "STYLE") "habit")) + subtree-end + nil)))) + + (subtree-is-not-inbox + '(lambda () + (let ((subtree-end (save-excursion (org-end-of-subtree t)))) + (if (not (cl-search "Inbox.org" (buffer-name))) + subtree-end + nil))))) + + (setq org-agenda-custom-commands + `(("d" "Daily" + ((tags "PRIORITY=\"A\"" + ((org-agenda-skip-function ,entry-done) + (org-agenda-overriding-header "Tasks for Today:"))) + (alltodo "" + ((org-agenda-skip-function ,subtree-is-not-inbox) + (org-agenda-overriding-header "Inbox:"))) + (agenda "" + ((org-agenda-span 'day) + (org-agenda-skip-function ,subtree-is-not-habit) + (org-deadline-warning-days 0) + (org-agenda-overriding-header "Habits:"))) + (agenda "" + ((org-agenda-span 'week) + (org-agenda-skip-function ,subtree-is-habit) + (org-agenda-overriding-header "Week Ahead:")))) + ((org-agenda-compact-blocks t))))))) + +(use-package org-drill + ;; org-drill in upstream Org is unmaintained, and as described in [1], no + ;; longer works with the latest version of Org. I'm using this fork [2]. + ;; + ;; [1]: https://bitbucket.org/eeeickythump/org-drill/issues/62/org-drill-doesnt-work-with-org-mode-92 + ;; [2]: https://bitbucket.org/eeeickythump/org-drill/issues/63/maintainership + :straight (org-drill :type git :host github :repo "hakanserce/org-drill") + + ;; Update LaTeX fragments when showing them to me. + :init + (with-eval-after-load 'org-drill + (defun org-drill--show-latex-fragments () + (org-latex-preview '(16)))) + + ;; Scan the directory of the current buffer's file for other items, rather + ;; than just the current buffer. This is so that I can spread my cards across + ;; multiple files and still be tested on all of them when I run `org-drill'. + ;; The idea of combining topic during review sessions is suggested in Michael + ;; Nielsen's article [1]. + ;; + ;; [1]: http://augmentingcognition.com/ltm.html + :init + (setq org-drill-scope 'directory) + + ;; Use the SM-2 algorithm rather than the default SM-5 algorithm. I have faith + ;; in Anki's analysis of SM-5 [1] in the form that it is available. + ;; + ;; [1]: https://apps.ankiweb.net/docs/manual.html#what-about-sm-5 + :init + (setq org-drill-spaced-repetition-algorithm 'sm2) + + ;; Shut up the nagging "are you studying enough!?" message if I don't get + ;; enough items right. It's unhelpful and discourages me from being honest + ;; with the SRS algorithm. + :init + (setq org-drill-forgetting-index 100) + + ;; Set up a keybinding for getting quickly starting a drill session. + :bind + ("<f11>" . (lambda () + (interactive) + (let* ((drill-directory (concat notes-directory "/" "Drill")) + (path (-first #'(lambda (path) (s-ends-with-p ".org" path)) + (directory-files drill-directory t)))) + (find-file path) + (org-drill))))) + +;; The org-download package is incredible for quickly pull images into Org +;; documents. + +(use-package org-download + ;; Configure it to use 'scrot' and the 'org-attach' machinery. + :init + (setq org-download-screenshot-method "scrot -s %s") + (setq org-download-method 'attach)) + +;; When exporting Org as HTML, include syntax highlighting. + +(use-package htmlize + :config + (setq org-html-htmlize-output-type 'css)) + + +;;; +;;; PDF +;;; + +;; `doc-view' is unbearable for reading PDF's of any considerable size and +;; `pdf-tools' brings a number of significant improvements, so I can justify not +;; using the built-in solution. Even though I can run MuPDF in an EXWM buffer, +;; the integration in terms of searching and viewing attachments that comes with +;; pdf-tools makes for a much more pleasant experience. + +(use-package pdf-tools + :demand t + :config + (pdf-loader-install)) + + +;;; +;;; IRC +;;; + +;; Functions for connecting to and authenticating with my IRC bouncer. + +(defun read-bouncer-password () + "Pull the password for my bouncer from authinfo" + (let ((auth (auth-source-search :host "jakob.space" :user "jakob" :port 6697))) + (unless auth + (error "No auth entry for bouncer.")) + (let ((secretf (plist-get (car auth) :secret))) + (unless secretf + (error "No secret for bouncer's auth entry.")) + (funcall secretf)))) + +(defvar *bouncer-port-alist* '(("freenode" . 6697) + ("tildetown" . 6671))) + +(defun connect-bouncer (server) + "Connect to `SERVER' on the bouncer and rename the buffer" + (let* ((port (cdr (assoc server *bouncer-port-alist*))) + (buf (erc-tls :server "jakob.space" + :port port + :password (read-bouncer-password)))) + (erc-with-buffer (buf (rename-buffer server))))) + +;; Interactive function for connecting to all of my bouncer's IRC servers. + +(defvar irc-autoconnect-networks '("freenode" "tildetown") + "List of networks to automatically connect to when 'irc' is invoked.") + +(defun irc () + "Connect to every network in `irc-autoconnect-networks'." + (interactive) + (dolist (network irc-autoconnect-networks) + (connect-bouncer network))) + +(defun irc-quit () + "Disconnects from and kills all buffers associated with ERC." + (interactive) + (let ((server-buffers (cl-remove-if-not #'erc-server-buffer-p (erc-buffer-list)))) + (dolist (buffer server-buffers) + (erc-with-buffer (buffer) + (erc-quit-server (funcall erc-quit-reason nil)))))) + +(use-package erc + :straight nil + + ;; Only load the modules I actually use. This list deviates from the default + ;; by including truncate, but omitting autojoin. + :config + (setq erc-modules '(button completion fill + irccontrols list match + menu move-to-prompt netsplit + networks noncommands readonly + ring stamp track truncate)) + (erc-update-modules) + + ;; Configure personal details. + :init + (setq erc-nick '("jakob" "tsarfox") + erc-user-full-name "Jakob <http://jakob.space>" + erc-quit-reason (lambda (reason) + (or reason (concat "Program received signal " + "SIGSEGV, Segmentation fault.")))) + + ;; Display the current nick in the prompt. + :init + (setq erc-prompt (lambda () (format "%s>" (erc-current-nick)))) + + ;; Center everything. + :init + (setq erc-fill-function 'erc-fill-static + erc-fill-column 100 + erc-fill-static-center 22) + + ;; Enable MIRC and control sequences so I can partake in figlet spam, + ;; objectively the best part of the IRC experience. + :init + (setq erc-interpret-controls-p t + erc-interpret-mirc-color t) + + ;; Enable timestamps, and show them to the left of the messages. + :init + (setq erc-hide-timestamps nil + erc-insert-timestamp-function 'erc-insert-timestamp-left) + + ;; Although 'log' is omitted from `erc-modules', I still explicitly disable + ;; logging. I can just save the buffer if I want to save a conversation. + :init + (setq erc-enable-logging nil + erc-save-buffer-on-part nil) + + ;; Similarly, 'autowaway' is omitted from `erc-modules', but I'm disabling it + ;; anyway. + :init + (setq erc-auto-set-away nil + erc-auto-discard-away nil) + + ;; Kill buffers when I disconnect. + :init + (setq erc-kill-buffer-on-part t + erc-kill-queries-on-quit t + erc-kill-server-buffer-on-quit t) + + ;; Don't get in my way by popping new windows when someone queries me, and + ;; don't assault me with windows when I connect to my bouncer. + :init + (setq erc-auto-query 'bury + erc-join-buffer 'bury) + + ;; Show CTCP requests. Not for any reason in particular, I just like seeing + ;; when people ping me. + :init + (setq erc-paranoid t) + + ;; Hide JOIN/PART/QUIT messages from idlers. + :init + (setq erc-lurker-hide-list '("JOIN" "PART" "QUIT") + erc-lurker-threshold-time 3600) + + ;; Configure `erc-track' to ignore the less interesting message types. + :init + (setq erc-track-exclude-server-buffer t + erc-track-exclude-types '("JOIN" "NICK" "PART" "QUIT" "MODE" + "324" "329" "332" "333" "353" "477")) + + ;; Enable channel tracking. + :config + (erc-track-minor-mode 1) + (erc-track-mode 1) + + ;; Move to the prompt as soon as I begin typing. I've configured the buffer + ;; text preceding it to be read-only, anyway. + :config + (erc-move-to-prompt-enable) + + ;; Bind a key for resetting =erc-track=, since I tend to get absolutely + ;; bombarded upon reconnecting. + :bind + (:map erc-mode-map + ("C-c f" . (lambda () + (interactive) + (setq erc-modified-channels-alist nil) + (erc-modified-channels-update)))) + + ;; Let `erc-fill' handle filling. + :hook + (erc-mode . (lambda () (auto-fill-mode 0)))) + +;; Configure 'erc-hl-nicks' to show names using pretty colors. + +(use-package erc-hl-nicks + :after erc + :config + (erc-hl-nicks-enable)) + +;; Display a bar delimiting read and unread messages. Adapted from [1]. +;; +;; [1]: https://www.emacswiki.org/emacs/ErcBar + +(defvar erc-bar-overlay nil + "Overlay used to set bar") + +(defvar erc-bar-threshold 1 + "The minimum number of messages required for the bar to be +displayed.") + +(defun erc-bar-move-back (n) + "Moves back n message lines. Ignores wrapping, and server +messages." + (interactive "nHow many lines ? ") + (re-search-backward "^.*<.*>" nil t n)) + +(defun erc-bar-update-overlay () + "Update the overlay for current buffer, based on the content of +erc-modified-channels-alist. Should be executed on window +change." + (interactive) + (let* ((info (assq (current-buffer) erc-modified-channels-alist)) + (count (cadr info))) + (if (and info (> count erc-bar-threshold)) + (save-excursion + (goto-char (point-max)) + (when (erc-bar-move-back count) + (let ((inhibit-field-text-motion t)) + (move-overlay erc-bar-overlay + (line-beginning-position) + (line-end-position) + (current-buffer))))) + (delete-overlay erc-bar-overlay)))) + +(with-eval-after-load 'erc + ;; Run the hook before 'erc-modified-channels-update + (defadvice erc-track-mode (after erc-bar-setup-hook + (&rest args) activate) + (remove-hook 'window-configuration-change-hook 'erc-bar-update-overlay) + (add-hook 'window-configuration-change-hook 'erc-bar-update-overlay)) + + (add-hook 'erc-send-completed-hook (lambda (str) (erc-bar-update-overlay)))) + +;; Display the bar as a highlight on the most recently unread message. + +(with-palette + (setq erc-bar-overlay (make-overlay 0 0)) + (overlay-put erc-bar-overlay 'face `(:background ,black-2))) + + +;;; +;;; News +;;; + +;; Most of my Gnus configuration is in gnus.el, which is not under version +;; control because it contains a list of my accounts on various email servers. + +;; Stop Gnus from making a mess of my home directory. + +(setq gnus-init-file "~/.emacs.d/gnus.el" + gnus-startup-file "~/.local/share/newsrc" + gnus-read-newsrc-file nil + gnus-save-newsrc-file nil + gnus-directory "~/.local/share/gnus/news/" + message-directory "~/.local/share/gnus/mail") + +;; Sort articles such that newer items are at the top of the summary buffer. + +(setq gnus-thread-sort-functions '(gnus-thread-sort-by-most-recent-date)) + +;; Enable Flyspell so I don't make a fool of myself when composing messages. + +(add-hook 'message-mode-hook #'flyspell-mode) + +;; Use w3m to display HTML emails. Adapted from [1]. +;; +;; [1]: https://norman.walsh.name/2011/03/10/w3m + +(use-package w3m + :demand t + :config + (setq gnus-mime-display-multipart-related-as-mixed nil + mm-text-html-renderer #'w3m + mm-inline-text-html-with-images t + mm-inline-text-html-with-w3m-keymap nil + w3m-default-display-inline-images t)) + +;; Encrypt emails such that we can read them as well. + +(setq mml-secure-openpgp-encrypt-to-self t) + + +;;; +;;; Tunes +;;; + +(use-package mpdel + :demand t + ;; Use a more sane prefix key for interacting with 'mpdel'. + :init + (setq mpdel-prefix-key (kbd "C-c z")) + + ;; Start `mpdel-mode', and while we're at it, start the daemon, too. + :config + (shell-command "ps -e | grep -q mpd || mpd") + (mpdel-mode)) + + +;;; +;;; Programming +;;; + +(use-package dumb-jump + ;; Replace `xref' keybindings instead of using the default ones, since these + ;; will get overwritten if I'm using a proper tag indexer anyway. + :config + (define-key dumb-jump-mode-map (kbd "C-M-g") nil) + (define-key dumb-jump-mode-map (kbd "C-M-p") nil) + (define-key dumb-jump-mode-map (kbd "C-M-q") nil) + (define-key dumb-jump-mode-map (kbd "M-.") #'dumb-jump-go) + (define-key dumb-jump-mode-map (kbd "M-,") #'dumb-jump-back) + (dumb-jump-mode)) + +(use-package company + ;; Start suggesting completions (almost) as soon as possible. + :config + (setq company-idle-delay 0.1 + company-minimum-prefix-length 3) + + ;; Use standard movement keys for selecting entries from the completion popup + ;; instead of those used for minibuffer next/previous. + :bind + (:map company-active-map + ("M-n" . nil) + ("M-p" . nil) + ("C-n" . company-select-next) + ("C-p" . company-select-previous)) + + ;; And have <tab> responsible for completion rather than <ret>, so that + ;; company isn't quite as in-the-way. + :bind + (:map company-active-map + ("TAB" . company-complete-selection) + ("<tab>" . company-complete-selection) + ("RET" . nil) + ("<return>" . nil))) + +(use-package compile + :straight nil + :bind + ("<f12>" . compile) + + ;; Taken from EmacsWiki [1]. The compilation command can be changed by invoking + ;; compile with a prefix argument, so I don't see the advantage of popping a + ;; prompt every time. + ;; + ;; [1]: https://www.emacswiki.org/emacs/CompileCommand + :init + (setq compilation-read-command nil) + + ;; Hide the compilation buffer upon successful compilation. I typically don't + ;; care about the output unless there were errors. + :config + (add-hook 'compilation-finish-functions + (lambda (buf strg) + (let ((win (get-buffer-window buf 'visible))) + (when win (delete-window win)))))) + +(use-package flycheck) + +(use-package magit + :bind + ("C-c g" . magit-status)) + +;; After several failed attempts at getting into Smartparens, I've finally +;; developed a taste for it. Rommel was not lying when he said that "[i]t's like +;; having cybernetic limbs—it makes you jump higher and punch harder." + +(use-package smartparens + ;; Use the default Smartparens configuration as a base. + :config + (require 'smartparens-config) + + ;; Enable the Smartparens-flavored replacement for `show-paren-mode'. + :config + (show-smartparens-global-mode t) + + ;; Enable Smartparens in all modes derived from `prog-mode' or `comint-mode', + ;; the latter hopefully covering all REPL cases. + :hook + (prog-mode . turn-on-smartparens-mode) + (comint-mode . turn-on-smartparens-mode) + + ;; Don't treat '`' as a pair marker when editing [[*Myrddin][Myrddin]] source code. + :config + (sp-local-pair 'myrddin-mode "`" nil :actions nil) + + ;; Rebind the default keys for manipulating sexps to the alternatives + ;; suggested in Emacs and Pairs [1]. + ;; + ;; [1]: https://ebzzry.io/en/emacs-pairs/ + :bind + (:map smartparens-mode-map + ("C-M-a" . sp-beginning-of-sexp) + ("C-M-e" . sp-end-of-sexp) + ("C-M-d" . sp-down-sexp) + ("C-M-u" . sp-backward-up-sexp) + ("C-M-f" . sp-forward-sexp) + ("C-M-b" . sp-backward-sexp) + ("C-M-n" . sp-next-sexp) + ("C-M-p" . sp-previous-sexp) + + ("M-[" . sp-backward-unwrap-sexp) + ("M-]" . sp-unwrap-sexp) + ("C-<right>" . sp-forward-slurp-sexp) + ("C-<left>" . sp-backward-slurp-sexp) + ("M-<right>" . sp-foward-barf-sexp) + ("M-<right>" . sp-forward-barf-sexp) + + ("C-M-t" . sp-transpose-sexp) + ("C-M-k" . sp-kill-sexp) + ("M-k" . sp-backward-kill-sexp)) + + ;; The following mask `split-line' and `completion-at-point', respectively, + ;; but I have yet to use either of these, so I don't mind using the keys for + ;; Smartparens. I use "o" as a mnemonic for "out", and "i" is a mnemonic for + ;; "in". + :bind + (:map smartparens-mode-map + ("C-M-o" . sp-up-sexp) + ("C-M-i" . sp-backward-down-sexp))) + +(use-package lsp-ui + ;; The only reason I'm using `lsp-ui' is for integration with Flycheck. The + ;; features like lenses don't interest me. + :init + (setq lsp-ui-sideline-enable nil + lsp-ui-peek-enable nil + lsp-ui-doc-enable nil)) + +;; Disable the default machinery in `lsp-mode'. The reason I'm using the package +;; in the first place is for its ability to integrate with the other +;; programming-related packages I use. + +(use-package company-lsp + :commands company-lsp + :init + (setq company-lsp-cache-candidates 'auto)) + +(use-package lsp-mode + :init + (setq lsp-prefer-flymake nil + lsp-enable-xref t) + + ;; Filesystem hygene. + :init + (setq lsp-session-file "~/.cache/emacs/.lsp-session-v1" + lsp-auto-guess-root t) + + ;; Use `xref' keybindings instead of the default ones. + :bind + (:map lsp-mode-map + ("M-." . lsp-find-definition) + ("M-," . pop-tag-mark))) + + +;;; +;;; Rust +;;; + +;; Enable flycheck. + +(use-package flycheck-rust + :after rust-mode + :hook + (flycheck-mode . flycheck-rust-setup) + (rust-mode . flycheck-mode)) + +;; Enable company, ElDoc, and racer. + +(use-package racer + :init + (setq racer-cmd "~/.cargo/bin/racer") + (setq racer-rust-src-path "~/Code/rust/src") + + :hook + (racer-mode . eldoc-mode) + (racer-mode . company-mode)) + +(use-package rust-mode + :hook + (rust-mode . racer-mode) + + ;; Run 'rustfmt' automatically. + :init + (setq rust-format-on-save t)) + +;; Enable keybindings for working with Cargo. + +(use-package cargo + :hook (rust-mode . cargo-minor-mode)) + + +;;; +;;; C +;;; + + +(use-package cc-mode + :straight nil + + ;; Generally, I adhere to the Linux kernel coding style [1]. + ;; + ;; [1]: https://www.kernel.org/doc/html/v4.10/process/coding-style.html + :hook + (c-mode . (lambda () + (setq indent-tabs-mode t) + (c-set-style "linux"))) + + ;; Enable company, Flycheck, and ElDoc, integration for all of which is + ;; provided by `irony-mode'. + :hook + (c-mode . company-mode) + (c-mode . flycheck-mode) + (c-mode . eldoc-mode)) + +(use-package irony + :hook + (c-mode . irony-mode) + (irony-mode . irony-cdb-autosetup-compile-options)) + +(use-package company-irony + :config + (add-to-list 'company-backend 'company-irony)) + +(use-package flycheck-irony + :hook + (flycheck-mode . flycheck-irony-setup)) + +(use-package irony-eldoc + :hook + (irony-mode . irony-eldoc)) + + +;;; +;;; C++ +;;; + +(use-package cc-mode + :straight nil + ;; Even though the Linux kernel coding style [1] was written to address C, I + ;; feel its viewpoints on indentation apply equally as well to C++. + ;; + ;; [1]: https://www.kernel.org/doc/html/v4.10/process/coding-style.html + :hook + (c++-mode . (lambda () + (setq indent-tabs-mode t) + (c-set-style "linux"))) + + ;; Enable company, Flycheck, and ElDoc, integration for all of which is + ;; provided by `irony-mode'. + :hook + (c++-mode . company-mode) + (c++-mode . flycheck-mode) + (c++-mode . eldoc-mode)) + +(use-package irony + :hook + (c++-mode . irony-mode)) + + +;;; +;;; Myrddin +;;; + +(use-package myrddin-mode + ;; Indent with tabs. + :hook (myrddin-mode . (lambda () (setq-local indent-tabs-mode t)))) + + +;;; +;;; Emacs Lisp +;;; + +(use-package elisp-mode + :straight nil + ;; Enable company for both `lisp-interaction-mode' and `emacs-lisp-mode'. + :hook + (lisp-interaction-mode . company-mode) + (emacs-lisp-mode . company-mode) + + ;; Avoid omitting information when displaying deeply-nested lists. + :init + (setq eval-expression-print-level nil)) + +;; Helpful is a wonderful improvement upon the vanilla `describe-.*' functions, +;; capable of showing source code, keybindings, symbol properties, etc. + +(use-package helpful + :bind + ("C-h f" . helpful-callable) + ("C-h v" . helpful-variable) + ("C-h k" . helpful-key) + ("C-h C" . helpful-command) + ("C-c C-d" . helpful-at-point)) + + +;;; +;;; Common Lisp +;;; + +;; SLIME is my preferred REPL for working with Common Lisp, + +(use-package slime + ;; and SBCL is my preferred implementation. + :config + (setq inferior-lisp-program "sbcl") + + ;; Enable Smartparens and Company in the SLIME REPL. + :hook + (slime-repl-mode . (lambda () + (smartparens-mode +1) + (company-mode +1))) + + ;; Enable the SLIME backend for company. + :config + (setq slime-contribs '(slime-fancy slime-company))) + +(use-package slime-company + :after company + :hook + (lisp-mode . (lambda () (company-mode +1))) + :config + (add-to-list 'company-backends 'company-slime)) + + +;;; +;;; Scheme +;;; + +;; Geiser is my preferred REPL for working with Scheme. + +(use-package geiser + ;; Set up Geiser for all of the Scheme implementations I regularly use. + :init + (setq geiser-active-implementations '(chicken guile racket)) + (setq geiser-default-implementation 'guile) + (setenv "CHICKEN_DOC_REPOSITORY" "/home/jakob/.local/share/chicken/doc/") + + ;; Filesystem hygene. + :init + (setq geiser-repl-history-filename "~/.cache/emacs/geiser_history")) + + +;;; +;;; Python +;;; + +;; Set up company. + +(use-package company-jedi + :after company + + :hook + (python-mode . company-mode) + (python-mode . jedi:setup) + + :init + (setq jedi:complete-on-dot t) + (setq jedi:tooltip-method nil) + + :config + (add-to-list 'company-backends 'company-jedi)) + + +;;; +;;; Hy +;;; + +;; Set up company. + +(use-package hy-mode + :hook + (hy-mode . company-mode) + (hy-mode . jedi-mode) + + ;; `hy-mode' doesn't create an autoload for the command to start a Hy REPL, so + ;; we'll declare it here. This lets me M-x run-hy before `hy-mode' loaded. + :commands run-hy + + ;; Enable `hy-mode' in files ending with ".hy" + :mode "\\.hy\\'") + + +;;; +;;; Javascript +;;; + +(use-package js + :straight nil + + ;; Set up `js-mode' to interact with a LSP daemon. + :requires lsp-mode + :hook (js-mode . lsp) + :config + (lsp-register-client + (make-lsp-client :new-connection (lsp-stdio-connection "~/.bin/js-ts-stdio") + :major-modes '(js-mode) + :server-id 'js-ts-stdio)) + +;; I have yet to find a style guide I agree with, but one thing common to most +;; of them that I _do_ agree with is that Javascript is far more readable with +;; an indentation width of 2. + :init + (setq-default js-indent-level 2)) + + +;;; +;;; Nim +;;; -(eval-document "~/.emacs.d/jakob.org") +(use-package nim-mode) |