silly business

Prologue

This is my Emacs configuration. It is written in Org Mode format, which means that I can display a static representation here, but the source repository and document (plain text view), are interactive when opened in Emacs.

It follows the concept of "literate programming" and both defines my Emacs configuration (as well as a few other, related things) and includes my notes about why I made those changes, and what I was doing at the time, as well as whatever other commentary I felt like including at the time (related or otherwise).

At least, that's the goal. In reality, it's a messy living document that I use to configure Emacs and to keep track of what I've done. I don't always take the best of notes, but it is sufficient for me to keep moving forward. If you search around, you may find ideas and code that you can repurpose for your own uses.

Entrypoint

The source code below is extracted to init.el by calling M-x org-babel-tangle. The rest of this file is extracted to readme.el by this entrypoint in init.el. This allows me to only maintain readme.org as it will be re-extracted at startup every time. If this whole file is tangled to init.el by init.el, then a bootstrapping problem is introduced. So this part remains static, and the rest of the config can live in its Org file.

  (setq dotfiles-dir
        (file-name-directory
         (or (buffer-file-name) load-file-name)))

  (let* ((org-dir (expand-file-name
                   "lisp" (expand-file-name
                           "org" (expand-file-name
                                  "src" dotfiles-dir))))
         (org-contrib-dir (expand-file-name
                           "lisp" (expand-file-name
                                   "contrib" (expand-file-name
                                              ".." org-dir))))
         (load-path (append (list org-dir org-contrib-dir)
                            (or load-path nil))))
    ;; load up Org-mode and Org-babel
    (require 'ob-tangle))

  ;; load up all literate org-mode files in this directory
  (mapc #'org-babel-load-file (directory-files dotfiles-dir t "\\.org$"))

Customize

Emacs provides a menu-based customization interface that makes configuration files like this one entirely optional, and sometimes Emacs prompts the user for things and saves their preferences to a "custom file." By default, that file is this file, but the auto-generated code is nasty, disposable, and almost always specific to the system where I've made some interactive choice – for instance to trust local variables set in the header of a file like this one – and after a long time I've realized it's too troublesome to check in those changes. So this setting tells Customize to write those settings to their own file, and this file is ignored in .gitignore.

  (setq custom-file (expand-file-name "custom.el" user-emacs-directory))
  (load custom-file)

Package Manager Bootstrap

After tangling the source files and loading init.el, the first thing that must be done is to prepare to manage third party packages, because my config is built on top of the work of many third party packages. I like to install and manage all of the packages I use as part of my configuration so that it can be duplicated across computers (more or less) and managed with git, so I use use-package to ensure that packages are installed from my configuration file.

Bootstrap sets up the ELPA, Melpa, and Org Mode repositories, sets up the package manager, configures use-package and installs a few extra packages that acoutrement use-package and will be used heavily throughout. It used to install use-package itself, however, it has since been upstreamed and that step has been removed. πŸŽ‰

  (require 'package)
  (setq package-archives '(("gnu" . "https://elpa.gnu.org/packages/")
                           ("melpa" . "https://melpa.org/packages/")
                           ("org" . "http://orgmode.org/elpa/")))
  (package-initialize)

  ;; set ensure to be the default
  (require 'use-package-ensure)
  (setq use-package-always-ensure t)

  ;; these go in bootstrap because packages installed
  ;; with use-package use :diminish and :delight
  (use-package diminish)
  (use-package delight)

Once this is done I need to install and configure any third party packages that are used in many modes throughout Emacs. Some of these modes fundamentally change the Emacs experience and need to be present before everything can be configured.

Fundamental Package Installation and Configuration

First I need to install packages with a large effect and on which other packages are likely to depend. These are packages essential to my workflow. Configuration here should be config that must run early, before variables are set or language-related packages, which will likely rely on these being set.

Icons

Treemacs and Doom themes both rely upon all-the-icons to look nice

  (use-package all-the-icons)

Along the way nerd-icons also gets installed. On first run or after clearing out elpa/, need to run the following:

M-x nerd-icons-install-fonts
M-x all-the-icons-install-fonts

This installs the actual fonts and only needs to be called once. Maybe I'll automate it someday.

Treemacs

Treemacs provides a file browser on the left hand side of Emacs that I have grown to really like. It's great for exploring unfamiliar projects and modules. It's installed early because many things have integrations with it, including some themes.

  (use-package treemacs
    :defer t
    )

  (setq treemacs-no-png-images nil)

  (use-package treemacs-evil
    :after (treemacs evil))

  (use-package treemacs-projectile
    :after (treemacs projectile))

  (use-package treemacs-magit
  :after (treemacs magit))

Theme

I'm mainly using the Doom Emacs theme pack. I think they're really nice to look at, especially with solaire-mode.

Theme packs

Doom
  (use-package doom-themes
    :config
    ;; Global settings (defaults)
    (setq doom-themes-enable-bold t    ; if nil, bold is universally disabled
          doom-themes-enable-italic t
          ) ; if nil, italics is universally disabled

    ;; Corrects (and improves) org-mode's native fontification.
    ;; TODO is this still relevant when also using org-modern? or do
    ;; they just conflict?
    (doom-themes-org-config)
    )
ef-themes

Protesilaos Stavrou has a nice theme pack too:

  (use-package ef-themes)

Default theme

I prefer to load a theme per-system, but it's nice to have it documented here. Add a line like the following to the appropriate file in local/

;;  (load-theme 'ef-reverie)

Navigation and Completion, and the Minibuffer

The next few packages work closely together to enhance some of the core functionality of Emacs related to navigation, buffer management, and running commands.

Consult (commands to list, search, and preview files and buffers in the minibuffer)

Consult adds search and navigation commands that build upon the built-in completing-read

    (use-package consult)

Marginalia (more metadata in completions and the minibuffer)

Marginalia enhances the same native Emacs search interface with extra information about whatever is being displayed. It's used by both Vertico and Consult to display extra information about the actions they offer.

  ;; Enable rich annotations using the Marginalia package
  (use-package marginalia
    ;; Bind `marginalia-cycle' locally in the minibuffer.  To make the binding
    ;; available in the *Completions* buffer, add it to the
    ;; `completion-list-mode-map'.
    :bind (:map minibuffer-local-map
                ("M-A" . marginalia-cycle))

    ;; The :init section is always executed.
    :init

    ;; Marginalia must be activated in the :init section of use-package such that
    ;; the mode gets enabled right away. Note that this forces loading the
    ;; package.
    (marginalia-mode))

  ;; enhance marginalia with icons
  (use-package nerd-icons-completion
    :config
    (nerd-icons-completion-mode))

Orderless (better interactive matching)

Orderless allows pattern matching to be "better." With the default configuration, which is what I have below, the main obvious difference from vanilla Emacs is that now matching works anywhere in the target string and not just the beginning. That's a big win. This is applied everywhere Emacs does matching.

    (use-package orderless
      :ensure t
      :custom
      (completion-styles '(orderless basic))
      (completion-category-overrides '((file (styles basic partial-completion)))))

Embark (contextual actions)

Embark allows you to call commands on whatever the cursor is on (thing "at-point") and shows stuff that is relevant to the context. It has some integrations with consult that seem very powerful and I don't fully understand them yet, but I'm adding them in here so I can figure them out. Lots of searching and matching goodness for working across many files and buffers, I think.

    (use-package embark)
    (use-package embark-consult)

Vertico (minibuffer behavior)

Finally, Vertico makes M-x more featureful, and allows me to display command history when it is invoked. I map M-x to SPC SPC due to my historical use of Spacemacs, and Vertico keeps Emacs feeling like home for someone used to Helm.

Below is, actually, the default config. I didn't write any of this. It's kind of wild.

  ;; Enable vertico
  (use-package vertico
    :custom
    ;; (vertico-scroll-margin 0) ;; Different scroll margin
     (vertico-count 20) ;; Show more candidates
    ;; (vertico-resize t) ;; Grow and shrink the Vertico minibuffer
     (vertico-cycle t) ;; Enable cycling for `vertico-next/previous'
    :init
    (vertico-mode))

  ;; Persist history over Emacs restarts. Vertico sorts by history position.
  (use-package savehist
    :init
    (savehist-mode))

Tab Completion

Corfu handles tab completion outside of the minibuffer, and allows multiple terms separated by spaces, using the rules from completing-read – in this case, what I've defined in the Orderless section above.

  (use-package corfu
    ;; Optional customizations
    ;; :custom
    ;; (corfu-cycle t)                ;; Enable cycling for `corfu-next/previous'
    ;; (corfu-quit-at-boundary nil)   ;; Never quit at completion boundary
    ;; (corfu-quit-no-match nil)      ;; Never quit, even if there is no match
    ;; (corfu-preview-current nil)    ;; Disable current candidate preview
    ;; (corfu-preselect 'prompt)      ;; Preselect the prompt
    ;; (corfu-on-exact-match nil)     ;; Configure handling of exact matches

    ;; Enable Corfu only for certain modes. See also `global-corfu-modes'.
    ;; :hook ((prog-mode . corfu-mode)
    ;;        (shell-mode . corfu-mode)
    ;;        (eshell-mode . corfu-mode))
    :bind
    ;; Configure SPC for separator insertion
    (:map corfu-map ("SPC" . corfu-insert-separator))

    ;; Recommended: Enable Corfu globally.  This is recommended since Dabbrev can
    ;; be used globally (M-/).  See also the customization variable
    ;; `global-corfu-modes' to exclude certain modes.
    :init
    (global-corfu-mode))

Global Configuration

Below is some final, global configuration related to Vertico and Corfu & configure how completion and the minibuffer work.

  ;; A few more useful configurations...
  ;; Support opening new minibuffers from inside existing minibuffers.
  (setq enable-recursive-minibuffers t)

  ;; Hide commands in M-x which do not work in the current mode.  Vertico
  ;; commands are hidden in normal buffers. 
  (setq read-extended-command-predicate #'command-completion-default-include-p)

  ;; Enable indentation+completion using the TAB key.
  ;; `completion-at-point' is often bound to M-TAB.
  (setq tab-always-indent 'complete)

  ;; Emacs 30 and newer: Disable Ispell completion function.
  ;; Try `cape-dict' as an alternative.
  ;; (text-mode-ispell-word-completion nil)

  ;; Add prompt indicator to `completing-read-multiple'.
  ;; We display [CRM<separator>], e.g., [CRM,] if the separator is a comma.
  (defun crm-indicator (args)
    (cons (format "[CRM%s] %s"
                  (replace-regexp-in-string
                   "\\`\\[.*?]\\*\\|\\[.*?]\\*\\'" ""
                   crm-separator)
                  (car args))
          (cdr args)))
  (advice-add #'completing-read-multiple :filter-args #'crm-indicator)

  ;; Do not allow the cursor in the minibuffer prompt
  (setq minibuffer-prompt-properties
        '(read-only t cursor-intangible t face minibuffer-prompt))
  (add-hook 'minibuffer-setup-hook #'cursor-intangible-mode)

Solaire Mode

Also some visual candy that makes "real" buffers more visible by changing the background color slightly vs e.g. compilation or magit buffers

  (use-package solaire-mode)

  ;; treemacs got redefined as a normal window at some point
  (push '(treemacs-window-background-face . solaire-default-face) solaire-mode-remap-alist)
  (push '(treemacs-hl-line-face . solaire-hl-line-face) solaire-mode-remap-alist)

  (solaire-global-mode +1)

Doom Modeline

The Doom Emacs project also provides a fancy modeline to go along with their themes.

  (use-package doom-modeline
    :config       (doom-modeline-def-modeline 'main
                    '(bar matches buffer-info remote-host buffer-position parrot selection-info)
                    '(misc-info minor-modes input-method buffer-encoding major-mode process vcs "  "))
    :hook (after-init . doom-modeline-mode))

Emoji πŸ™

Provided by emojify. Run emojify-download-emoji

    ;; πŸ™Œ Emoji! πŸ™Œ
    (use-package emojify
      :config
      (setq emojify-download-emojis-p t)
      (emojify-set-emoji-styles '(unicode))
      (add-hook 'after-init-hook #'global-emojify-mode))

Configure Recent File Tracking

Emacs comes with recentf-mode which helps me remember what I was doing after I restart my session.

  ;; recent files mode
  (recentf-mode 1)
  (setq recentf-max-menu-items 25)
  (setq recentf-max-saved-items 25)

  ;; ignore the elpa directory
  (add-to-list 'recentf-exclude
               "elpa/*")

Install and Configure Projectile

projectile is a fantastic package that provides all kinds of project context-aware functions for things like:

  • running grep, but only inside the project
  • compiling the project from the project root without doing anything
  • find files within the project, again without having to do anything extra

It's great, it gets installed early, can't live without it. πŸ’˜ projectile

  (use-package projectile
    :delight)
  (use-package treemacs-projectile)
  (projectile-mode +1)

Install and Configure Evil Mode

evil-mode fundamentally changes Emacs so that while editing all of the modes and keybindings from vim are present. It's controversial but I think modal editing is brilliant and have been using vim bindings for twenty-odd years now. No going back.

  (defun setup-evil ()
    "Install and configure evil-mode and related bindings."
    (use-package evil
      :init
      (setq evil-want-keybinding nil)
      (setq evil-want-integration t)
      :config
      (evil-mode 1))

    (use-package evil-collection
      :after evil
      :config
      ;; don't let evil-collection manage go-mode
      ;; it is overriding gd
      (setq evil-collection-mode-list (delq 'go-mode evil-collection-mode-list))
      (evil-collection-init))

    ;; the evil-collection overrides the worktree binding :(
    ;; in magit
    (general-define-key
     :states 'normal
     :keymaps 'magit-status-mode-map
     "Z" 'magit-worktree)

    (general-define-key
     :states 'normal
     "RET" 'embark-act
     )

    (general-define-key
     :states 'normal
     :keymaps 'prog-mode-map
     "gd" 'evil-goto-definition
     )

    ;; add fd as a remap for esc
    (use-package evil-escape
      :delight)
    (evil-escape-mode 1)

    (use-package evil-surround
      :config
      (global-evil-surround-mode 1))
    (use-package evil-snipe)
    (evil-snipe-override-mode +1)
    ;; and disable in specific modes (an example below)
    ;; (push 'python-mode evil-snipe-disabled-modes)

    (use-package undo-tree
      :config
      (global-undo-tree-mode)
      (evil-set-undo-system 'undo-tree)
      (setq undo-tree-history-directory-alist '(("." . "~/.emacs.d/undo"))))

    ;; add some advice to undo-tree-save-history to suppress messages
    ;; when it saves its backup files
    (defun quiet-undo-tree-save-history (undo-tree-save-history &rest args)
      (let ((message-log-max nil)
            (inhibit-message t))
        (apply undo-tree-save-history args)))

    (advice-add 'undo-tree-save-history :around 'quiet-undo-tree-save-history)

    (setq-default evil-escape-key-sequence "fd")

    ;; unbind RET since it does the same thing as j and in some
    ;; modes RET is used for other things, and evil conflicts
    (with-eval-after-load 'evil-maps
      (define-key evil-motion-state-map (kbd "RET") nil))
    )

Install and Configure Keybindings Helper

General provides more consistent and convenient keybindings, especially with evil-mode.

It's mostly used below in the global keybindings section.

    (use-package general
      :init
      (setup-evil)
      :config
      (general-evil-setup))

Install and Configure Magit

Magit is an incredible integrated git UI for Emacs.

    (use-package magit)
    ;; disable the default emacs vc because git is all I use,
    ;; for I am a simple man
    (setq vc-handled-backends nil)

Allow magit to interact with git forges, like Github and Gitlab

  (use-package forge
    :after magit)

Install and Configure git-timemachine

git-timeline lets you step through the history of a file.

  (use-package git-timemachine)

  ;; This lets git-timemachine's bindings take precedence over evils'
  ;; (got lucky and happened to find this while looking for the package name, ha!)
  ;; @see https://bitbucket.org/lyro/evil/issue/511/let-certain-minor-modes-key-bindings
  (eval-after-load 'git-timemachine
    '(progn
       (evil-make-overriding-map git-timemachine-mode-map 'normal)
       ;; force update evil keymaps after git-timemachine-mode loaded
       (add-hook 'git-timemachine-mode-hook #'evil-normalize-keymaps)))

Install and Configure which-key

It can be difficult to to remember and discover all of the available shortcuts in Emacs, so which-key pops up a special buffer to show you available shortcuts whenever you pause in the middle of a keyboard shortcut for more than a few seconds. It's really lovely.

    (use-package which-key
      :delight
      :init
      (which-key-mode)
      (which-key-setup-minibuffer))

Set up pass for secrets handling

  (use-package pass)

Handle "fancy" output in compilation buffer

The external package fancy-compilation-mode handles colorization and "clever" use of ANSI to create progress bars and stupid shit like that, which show up in things like npm output and Docker output when BuildKit is set to NORMAL. You can, of course, set the BuildKit output style to PLAIN, but sometimes you're eg editing a file where NORMAL is hard-coded in the Makefile target you want to run when using compilation-mode and fighting project defaults isn't what you want to spend your time on.

   (use-package fancy-compilation
    :commands (fancy-compilation-mode))

  (with-eval-after-load 'compile
    (fancy-compilation-mode))

I don't like how fancy-compilation-mode overrides colors by default, but luckily this can be disabled.

  (setq fancy-compilation-override-colors nil)

Configure the Startup Splashscreen

Following Spacemacs's style, I use the emacs-dashboard project and all-the-icons to provide an aesthetically pleasing splash screen with useful links to recently used files on launch.

Actually, looking at the project page, the icons don't seem to be working for me. Maybe I need to enable them. I'll investigate later.

  ;; first disable the default startup screen
  (setq inhibit-startup-screen t)
  (use-package dashboard
    :config
    (dashboard-setup-startup-hook)
    (setq dashboard-startup-banner 'logo)
    (setq dashboard-center-content t)
    (setq dashboard-items '((recents  . 5)
                            (bookmarks . 5)
                            (projects . 5))
          )
    )

  (setq dashboard-set-footer nil)

Install templating tool and default snippets

YASnippet is really cool and allow fast insertion of boilerplate using templates. I've been meaning to use this more. Here are the YASnippet docs.

  (use-package yasnippet
    :delight
    :config
    (use-package yasnippet-snippets))

Enable yas-mode everywhere

  (yas-global-mode 1)

Extra Packages

Packages with a smaller effect on the experience.

prism colors by indent level

It takes over the color theme and I don't know if I want it on all the time but it's interesting and I want to have it installed so that I can turn it on in certain situations, like editing highly nested YAML, where it might be invaluable. If I can remember to use it :)

  (use-package prism)

git-gutter shows unstaged changes in the gutter

  (use-package git-gutter
      :delight
      :config
      (global-git-gutter-mode +1))

Highlight the current line

I like to highlight the current line so that it is easy to identify where my cursor is.

  (global-hl-line-mode)
  (setq global-hl-line-sticky-flag t)

Rainbow delimiters make it easier to identify matching parentheses

  (use-package rainbow-delimiters
    :config
    ;; set up rainbow delimiters for Emacs lisp
    (add-hook 'emacs-lisp-mode-hook #'rainbow-delimiters-mode)
    ;; and sql mode too, it's useful there
    (add-hook 'sql-mode-hook #'rainbow-delimiters-mode)
    )

restart-emacs does what it says on the tin

  (use-package restart-emacs)

s is a string manipulation utility

I use this for a trim() function far down below. I think it gets pulled in as a dependency anyway, but in any case it provides a bunch of helper functions and stuff. Docs are here.

  (use-package s)

a systemd file mode

Just provides syntax highlighting in .unit files.

  (use-package systemd)

Install and Configure Flycheck for Linting

Flycheck is an on-the-fly checker that hooks into most language backends.

  ;; linter
  (use-package flycheck
    :delight
    ;; enable it everywhere
    :init (global-flycheck-mode))

  (add-hook 'flycheck-error-list-mode-hook
            'visual-line-mode)

Install exec-path-from-shell to manage the PATH

exec-path-from-shell mirrors PATH in zsh or Bash in macOS or Linux into Emacs so that the PATH in the shell and the PATH when calling commands from Emacs are the same.

  (use-package exec-path-from-shell
    :config
    (exec-path-from-shell-initialize))

ace-window provides an ace-jump experience for switching windows

  (use-package ace-window)

Install a mode for drawing indentation guides

This mode adds subtle coloration to indentation whitespace for whitespace-delimited languages like YAML where sometimes it can be difficult to see the nesting level of a given headline in deeply-nested configuration.

  (use-package highlight-indent-guides)

Quick buffer switcher

PC style quick buffer switcher for Emacs

This switches Emacs buffers according to most-recently-used/least-recently-used order using C-tab and C-S-tab keys. It is similar to window or tab switchers that are available in PC desktop environments or applications.

Bound by default to C-<TAB> and C-S-<TAB>, I have decided that these are sane defaults. Just install this and turn it on.

  (use-package pc-bufsw)
  (pc-bufsw)

Writeable grep mode with ack

Writable grep mode allows you to edit the results from running grep on a project and easily save changes back to all of the original files

  (use-package ack)
  (use-package ag)
  (use-package wgrep-ack)

Better help buffers

  (use-package helpful)
  (global-set-key (kbd "C-h f") #'helpful-callable)
  (global-set-key (kbd "C-h v") #'helpful-variable)
  (global-set-key (kbd "C-h k") #'helpful-key)

Quickly jump around buffers

  (use-package ace-jump-mode)

Dumb jump

Dumb jump provides an interface to grep that does a pretty good job of finding definitions when a smarter backend like LSP is not available. This registers it as a backend for XREF.

  (use-package dumb-jump)
  (add-hook 'xref-backend-functions #'dumb-jump-xref-activate)
  (setq xref-show-definitions-function #'xref-show-definitions-completing-read)

Kubernetes Mode

Provides an interactive Kubernetes Mode inspired by magit. Since magit is one of my favorite tools, I have to try out the Kubernetes mode as well.

  (use-package kubernetes
  :ensure t
  :commands (kubernetes-overview))
  ;; add this config if I experience issues with Emacs locking up
  ;;:config
  ;;(setq kubernetes-poll-frequency 3600
   ;;     kubernetes-redraw-frequency 3600))

I need the evil compatiblity mode, too, because I run evil.

  (use-package kubernetes-evil
    :after kubernetes)

multiple cursors

  (use-package evil-mc)

elfeed

  (use-package elfeed)

Font

The FiraCode font is a programming-focused font with ligatures that looks nice and has a open license so I'm standardizing my editor configuration on that font

FiraCode Font Installation Script

Installing fonts is always a pain so I'm going to use a variation of the installation script that the FireCode devs provide under their manual installation guide. This should be Linux-distribution agnostic, even though the font can be installed as a system package with on all of my systems on 2022-02-19 Sat with just

sudo apt install fonts-firacode

because I don't intend to use Ubuntu as my only system forever. I just happen to be on Ubuntu on 2022-02-19 Sat.

But first, I want to be able to run this script every time Emacs starts, but only have the script actually do anything if the font is not already installed.

This guard will check to see if there's any font with 'fira' in it (case insensitive) and if so, just exits the script. This will happen on most executions.

  set -eo pipefail
  [[ $(fc-list | grep -i fira) != "" ]] && exit 0

Now here's the standard installation script

  fonts_dir="${HOME}/.local/share/fonts"
  if [ ! -d "${fonts_dir}" ]; then
      mkdir -p "${fonts_dir}"
  fi

  version=5.2
  zip=Fira_Code_v${version}.zip
  curl --fail --location --show-error https://github.com/tonsky/FiraCode/releases/download/${version}/${zip} --output ${zip}
  unzip -o -q -d ${fonts_dir} ${zip}
  rm ${zip}

  # for now we need the Symbols font, too
  zip=FiraCode-Regular-Symbol.zip
  curl --fail --location --show-error https://github.com/tonsky/FiraCode/files/412440/${zip} --output ${zip}
  unzip -o -q -d ${fonts_dir} ${zip}
  rm ${zip}

  fc-cache -f

This installation script was sourced from https://github.com/tonsky/FiraCode/wiki/Linux-instructions#installing-with-a-package-manager

Enable FiraCode Font

Calling the script from above will install the font

  (shell-command "chmod +x ~/.emacs.d/install-firacode-font.bash")
  (shell-command "~/.emacs.d/install-firacode-font.bash")

Enable it

  (add-to-list 'default-frame-alist '(font . "Fira Code-10"))
  (set-frame-font "Fira Code-10" nil t)

Configure FiraCode special features

FiraCode offers ligatures for programming symbols, which is cool.

  (use-package ligature
    :load-path "./vendor/"
    :config
    ;; Enable the "www" ligature in every possible major mode
    (ligature-set-ligatures 't '("www"))
    ;; Enable traditional ligature support in eww-mode, if the
    ;; `variable-pitch' face supports it
    (ligature-set-ligatures 'eww-mode '("ff" "fi" "ffi"))

    ;; ;; Enable ligatures in programming modes                                                           
    (ligature-set-ligatures 'prog-mode '("www" "**" "***" "**/" "*>" "*/" "\\\\" "\\\\\\" "{-"
                                         ":::" ":=" "!!" "!=" "!==" "-}" "----" "-->" "->" "->>"
                                         "-<" "-<<" "-~" "#{" "#[" "##" "###" "####" "#(" "#?" "#_"
                                         "#_(" ".-" ".=" ".." "..<" "..." "?=" "??" ";;" "/*" "/**"
                                         "/=" "/==" "/>" "//" "///" "&&" "||" "||=" "|=" "|>" "^=" "$>"
                                         "++" "+++" "+>" "=:=" "==" "===" "==>" "=>" "=>>" "<="
                                         "=<<" "=/=" ">-" ">=" ">=>" ">>" ">>-" ">>=" ">>>" "<*"
                                         "<*>" "<|" "<|>" "<$" "<$>" "<!--" "<-" "<--" "<->" "<+"
                                         "<+>" "<=" "<==" "<=>" "<=<" "<>" "<<" "<<-" "<<=" "<<<"
                                         "<~" "<~~" "</" "</>" "~@" "~-" "~>" "~~" "~~>" "%%"))

    ;; disabled combinations that could be ligatures
    ;;  "::"

   (global-ligature-mode 't))

Language Configuration

This section contains all of the IDE-like features in my configuration.

YAML

  (use-package yaml-mode)
  (add-hook 'yaml-mode-hook 'highlight-indent-guides-mode)
  ;;(add-hook 'yaml-mode-hook 'origami-mode)

  (general-define-key
   :states  'normal
   :keymaps 'yaml-mode-map
   "zo"     'origami-open-node-recursively
   "zO"     'origami-open-all-nodes
   "zc"     'origami-close-node-recursively)

Rego

whatever that is

  (use-package rego-mode)

Markdown

  (use-package markdown-mode
    :ensure t
    :mode (("README\\.md\\'" . gfm-mode)
           ("\\.md\\'" . gfm-mode)
           ("\\.markdown\\'" . gfm-mode)))

    ;; show code blocks w/ monospace font
    (add-hook 'markdown-mode-hook 'visual-line-mode)
    (add-hook 'markdown-mode-hook 'variable-pitch-mode)
    (add-hook 'markdown-mode-hook
              '(lambda ()
                 (set-face-attribute 'markdown-code-face nil :inherit 'fixed-pitch)
                 (set-face-attribute 'markdown-pre-face nil :inherit 'fixed-pitch)))

  ;; this can go here because it affects Markdown's live preview mode
  ;; but I should consider putting it somewhere more general maybe?
  (add-hook 'eww-mode-hook 'visual-line-mode)

Docker

  (use-package dockerfile-mode)
  (add-to-list 'auto-mode-alist '("Dockerfile\\'" . dockerfile-mode))
  (put 'dockerfile-image-name 'safe-local-variable #'stringp)

Python

auto-virtualenv looks in $WORKON_HOME for virtualenvs, and then I can run M-x pyvenv-workon RET project RET to choose my virtualenv for project, found in $WORKON_HOME, or a symlink anyway.

  (use-package auto-virtualenv)
  (add-hook 'python-mode-hook 'auto-virtualenv-set-virtualenv)
  (setenv "WORKON_HOME" "~/.virtualenvs")

So the convention for use is:

  1. Create a virtualenv as usual for the project
  2. Symlink it inside ~/.virtualenvs
  3. M-x pyvenv-workon

Go

Go is my primary language so it's my most dynamic and complicated configuration, however it degrades gracefully so if not everything is installed, the rest of it still works.

Dependencies

Go support requires some dependencies. I will try to list them all here. Stuff I have installed has some overlap because of the in-progress move to LSP, but I'll prune it later.

$ go get https://github.com/golang/lint
  • golangci-lint is a meta linter that calls a bunch of 3rd party linters (configurable) and replaces the old one that used to freeze my computer. go-metalinter, I think, is what it was called. Anyway, it used to crash my computer and apparently that was a common experience. Anyway golangci-lint must be installed independently, too:
# install it into ./bin/
$ curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v1.23.6

Initial Setup

  (use-package go-mode
    :hook ((go-mode . yas-minor-mode)
           (go-mode . eglot-ensure))
    :config
    ;; fixes ctrl-o after goto-definition by telling evil that godef-jump jumps
    (evil-add-command-properties #'godef-jump :jump t))


  ;; enable golangci-lint to work with flycheck
  (use-package flycheck-golangci-lint
    :hook (go-mode . flycheck-golangci-lint-setup))

Eglot Config

Since Go has auto formatting and imports management as a first-party feature I like to enable that as an automatic step before save in Emacs so that I do not have to remember to remove unwantetd imports, or to add new ones, or to format my code, literally ever. I am totally pampered by this state of affairs and Go is my bae for having all of these features.

  ;; https://github.com/joaotavora/eglot/issues/574#issuecomment-1401023985
  (defun my-eglot-organize-imports () (interactive)
         (eglot-code-actions nil nil "source.organizeImports" t))

  (defun install-my-eglot-organize-imports () 
    (add-hook 'before-save-hook 'my-eglot-organize-imports nil t)
    (add-hook 'before-save-hook 'eglot-format-buffer nil t))

  (add-hook 'go-mode-hook #'install-my-eglot-organize-imports)

The Go Emacs docs suggest using this snippet which I think might help with some freezing I've been seeing when stepping into previously unseen library code

  (defun project-find-go-module (dir)
    (when-let ((root (locate-dominating-file dir "go.mod")))
      (cons 'go-module root)))

  (cl-defmethod project-root ((project (head go-module)))
    (cdr project))

  (add-hook 'project-find-functions #'project-find-go-module)

Preliminary testing suggests it might do the trick

Package and Configuration for Executing Tests

  (use-package gotest)
  (advice-add 'go-test-current-project :before #'projectile-save-project-buffers)
  (advice-add 'go-test-current-test :before #'projectile-save-project-buffers)
  (add-hook 'go-test-mode-hook 'visual-line-mode)

REPL

Gore provides a REPL and gorepl-mode lets you use it from Emacs. In order to use the REPL from Emacs, you must first install Gore:

go get -u github.com/motemen/gore/cmd/gore

Gore also uses gocode for code completion, so install that (even though Emacs uses go-pls for the same).

go get -u github.com/mdempsky/gocode

Once that's done gorepl-mode is ready to be installed:

  (use-package gorepl-mode)

Mode-Specific Keybindings

  (general-define-key
   :states  'normal
   :keymaps 'go-mode-map
   ",a"     'go-import-add
   ",tp"    'go-test-current-project
   ",tt"    'go-test-current-test
   ",tf"    'go-test-current-file

   ;; using the ,c namespace for repl and debug stuff to follow the C-c
   ;; convention found in other places in Emacs
   ",cc"     'dap-debug
   ",cr"     'gorepl-run
   ",cg"     'gorepl-run-load-current-file
   ",cx"     'gorepl-eval-region
   ",cl"     'gorepl-eval-line

   ",x"      'eglot-code-actions
   ",n"      'go-rename
    )

  (autoload 'go-mode "go-mode" nil t)
  (add-to-list 'auto-mode-alist '("\\.go\\'" . go-mode))

Hooks

  ;; sets the visual tab width to 2 spaces per tab in Go buffers
  (add-hook 'go-mode-hook (lambda ()
                            (set (make-local-variable 'tab-width) 2)))

Rust

To install the Rust language server:

  1. Install rustup.
  2. Run rustup component add rls rust-analysis rust-src.
  (use-package rust-mode
    :mode (("\\.rs$" . rust-mode)))

Web

After some amount of searching and fumbling about I have discovered web-mode which appears to be the one-stop-shop solution for all of your HTML and browser-related needs. It handles a whole slew of web-related languages and templating formats and plays nicely with LSP. It's also the only package that I could find that supported .tsx files at all.

So yay for web-mode!

    (use-package web-mode
      :mode (("\\.html$" . web-mode)
             ("\\.html.tmpl$" . web-mode)
             ("\\.js$"   . web-mode)
             ("\\.jsx$"  . web-mode)
             ("\\.ts$"   . web-mode)
             ("\\.tsx$"  . web-mode)
             ("\\.css$"  . web-mode)
             ("\\.svelte$" . web-mode))
      :config
      (setq web-mode-enable-css-colorization t)
      (setq web-mode-enable-auto-pairing t)
      (setq web-mode-enable-auto-quoting nil))

enable jsx mode for all .js and .jsx files

If working on projects that do not use JSX, might need to move this to a project-specific config somewhere.

For now though, this is sufficient for me

  (setq web-mode-content-types-alist
        '(("jsx" . "\\.js[x]?\\'")))

Thanks to https://prathamesh.tech/2015/06/20/configuring-web-mode-with-jsx/

Setting highlighting for special template modes

  ;; web-mode can provide syntax highlighting for many template
  ;; engines, but it can't detect the right one if the template uses a generic ending.
  ;; If a project uses a generic ending for its templates, such
  ;; as .html, add it below. It would be more elegant to handle this by
  ;; setting this variable in .dir-locals.el for each project,
  ;; unfortunately due to this https://github.com/fxbois//issues/799 that
  ;; is not possible :(

  ;;(setq web-mode-engines-alist '(
  ;;        ("go" . ".*example_project_dir/.*\\.html\\'")
          ;; add more projects here..
  ;;        ))

JSON

  (use-package json-mode
    :mode (("\\.json$" . json-mode ))
    )

  (add-hook 'json-mode-hook 'highlight-indent-guides-mode)
Default Keybindings
    C-c C-f: format the region/buffer with json-reformat (https://github.com/gongo/json-reformat)
    C-c C-p: display a path to the object at point with json-snatcher (https://github.com/Sterlingg/json-snatcher)
    C-c P: copy a path to the object at point to the kill ring with json-snatcher (https://github.com/Sterlingg/json-snatcher)
    C-c C-t: Toggle between true and false at point
    C-c C-k: Replace the sexp at point with null
    C-c C-i: Increment the number at point
    C-c C-d: Decrement the number at point

Fish

  (use-package fish-mode)

Salt

  (use-package salt-mode)
  (add-hook 'salt-mode-hook
          (lambda ()
              (flyspell-mode 1)))

  (add-hook 'salt-mode-hook 'highlight-indent-guides-mode)

  (general-define-key
   :states  'normal
   :keymaps 'sh-mode-map
   ",c" (general-simulate-key "C-x h C-M-x")
   )

Elixir

  (use-package elixir-mode)

  ;; Create a buffer-local hook to run elixir-format on save, only when we enable elixir-mode.
  (add-hook 'elixir-mode-hook
            (lambda () (add-hook 'before-save-hook 'elixir-format nil t)))

SQL

Indent SQL

SQL support is pretty good out of the box but Emacs strangely doesn't indent SQL by default. This package fixes that.

  (use-package sql-indent)

Use rainbow delimeters in SQL

  (add-hook 'sql-mode-hook #'rainbow-delimiters-mode)

Emacs Lisp

I don't have any custom configuration for Emacs Lisp yet, but I am going to use this space to collect tools and resources that might become useful in the future, and which I may install.

A collection of development modes and utilities

Racket

Funny the twists of fate that bring us back to where we started. My interest in Emacs stemmed originally from an interest in Racket, and my inability to get vim to format Racket code appropriately. I never did wind up learning Racket, but I guess I might now, for entirely different reasons

;; (use-package racket-mode)

Adaptive Wrap and Visual Line Mode

Here I've done some black magic fuckery for a few modes. Heathens in modern languages and also some other prose modes don't wrap their long lines at 80 characters like God intended so instead of using visual-column-mode which I think does something similar but probably would've been easier, I've defined an abomination of a combination of visual-line-mode (built-in) and adaptive-wrap-prefix-mode to dynamically (visually) wrap and indent long lines in languages like Go with no line length limit so they look nice on my screen at any window width and don't change the underlying file β€” and it's actually pretty cool.

  (use-package adaptive-wrap
    :config
    (setq-default adaptive-wrap-extra-indent 2)
    (defun adaptive-and-visual-line-mode (hook)
      (add-hook hook (lambda ()
                        (progn
                          (visual-line-mode)
                          (adaptive-wrap-prefix-mode)))))

    (mapc 'adaptive-and-visual-line-mode
          (list
           'markdown-mode
           'go-mode-hook
           'sql-mode-hook
           'js2-mode-hook
           'yaml-mode-hook
           'rjsx-mode-hook))

    (add-hook 'compilation-mode-hook
              #'adaptive-wrap-prefix-mode)
    (setq compilation-scroll-output t))

Global Keybindings

Helper Functions

  (defun find-initfile ()
    "Open main config file."
    (interactive)
    (find-file "~/.emacs.d/readme.org"))

  (defun find-initfile-other-frame ()
    "Open main config file in a new frame."
    (interactive)
    (find-file-other-frame "~/.emacs.d/readme.org"))

  (defun close-client-frame ()
    "Exit emacsclient."
    (interactive)
    (server-edit "Done"))

  (defun last-window ()
    "Switch to the last window."
    (interactive)
    (other-window -1 t))

  (defun toggle-line-numbers-rel-abs ()
    "Toggles line numbers between relative and absolute numbering"
    (interactive)
    (if (equal display-line-numbers-type 'relative)
        (setq display-line-numbers-type 'absolute)
      (setq display-line-numbers-type 'relative))
    (if (equal display-line-numbers-mode t)
        (progn
          (display-line-numbers-mode -1)
          (display-line-numbers-mode))))

  (defun random-theme (light-theme-list dark-theme-list)
    "Choose a random theme from the appropriate list based on the current time"
    (let* ((now (decode-time))
           (themes (if (and (>= (nth 2 now) 10) (< (nth 2 now) 15))
                       light-theme-list
                     dark-theme-list)))
      (nth (random (length themes)) themes)))

  (defun load-next-favorite-theme ()
    "Switch to a random theme appropriate for the current time."
    (interactive)
    (let ((theme (random-theme light-theme-list dark-theme-list)))
      (load-theme theme t)
      (message "Switched to theme: %s" theme)))

Main Global Keymap

These are all under SPACE, following the Spacemacs pattern. Yeah, my configuration is a little of Spacemacs, a little of Doom, and a little of whatever I feel inspired by.

These keybindings are probably the most opinionated part of my configuration. They're shortcuts I can remember, logically or not.

  ;; define the spacebar as the global leader key, following the
  ;; Spacemacs pattern, which I've been using since 2014
  (general-create-definer my-leader-def
    :prefix "SPC")

  ;; define SPC m for minor mode keys, even though I use , sometimes
  (general-create-definer my-local-leader-def
    :prefix "SPC m")

  ;; global keybindings with LEADER
  (my-leader-def 'normal 'override
    "aa"     'ace-jump-mode
    "ag"     'org-agenda
    "TAB"    #'switch-to-prev-buffer
    "bb"     'consult-buffer
    "bl"     'ibuffer
    "bs"     'consult-buffer-other-window
    "bR"     'revert-buffer
    "bd"     'evil-delete-buffer
    "ds"     (defun ian-desktop-save ()
               (interactive)
               (desktop-save "~/desktop-saves"))
    "dr"     (defun ian-desktop-read ()
               (interactive)
               (desktop-read "~/desktop-saves"))
    "cc"     'projectile-compile-project

    "ec"     'flycheck-clear
    "el"     'flycheck-list-errors
    "en"     'flycheck-next-error
    "ep"     'flycheck-previous-error
    "Fm"     'make-frame
    "ff"     'find-file
    "Ff"     'toggle-frame-fullscreen
    "fd"     'dired
    "fr"     'consult-recent-file
    "fed"    'find-initfile
    "feD"    'find-initfile-other-frame
    "gb"     'magit-blame
    "gl"     'consult-line
    "gs"     'magit-status
    "gg"     'magit
    "gt"     'git-timemachine
    "gd"     'magit-diff
    "gi"     'consult-imenu
    "go"     'browse-at-remote
    "gptm"   'gptel-menu
    "gptc"   'gptel
    "jj"     'bookmark-jump
    "js"     'bookmark-set
    "jo"     'org-babel-tangle-jump-to-org

    "ks"     'kagi-fastgpt-shell
    "kp"     'kagi-fastgpt-prompt
    "kf"     'kagi-proofread
    "kr"     'kagi-summarize-region
    "kb"     'kagi-summarize-buffer
    "ku"     'kagi-summarize-url
    "kt"     'kagi-translate

    "ic"     'insert-char
    "is"     'yas-insert-snippet
    "n"      '(:keymap narrow-map)
    "oo"     'browse-url-at-point
    "p"      'projectile-command-map
    "p!"     'projectile-run-async-shell-command-in-root
    "ps"     'consult-git-grep
    "si"     'yas-insert-snippet
    "sn"     'yas-new-snippet
    "qq"     'save-buffers-kill-terminal
    "qr"     'restart-emacs
    "qz"     'delete-frame
    "ta"     'treemacs-add-project-to-workspace
    "thi"    (defun ian-theme-information ()
               "Display the last applied theme."
               (interactive)
               (let ((last-theme (car (reverse custom-enabled-themes))))
                 (if last-theme
                     (message "Last applied theme: %s" last-theme)
                   (message "No themes are currently enabled."))))
    "thc"    'consult-theme
    "tm"     'toggle-menu-bar-mode-from-frame
    "thn"    'load-next-favorite-theme
    "tnn"    'display-line-numbers-mode
    "tnt"    'toggle-line-numbers-rel-abs
    "tr"     'treemacs-select-window
    "ts"     'toggle-scroll-bar
    "tt"     'toggle-transparency
    "tp"     (defun ian-toggle-prism ()
               (interactive)
               (prism-mode 'toggle))
    "tw"     'whitespace-mode
    "w-"     'split-window-below
    "w/"     'split-window-right
    "wb"     'last-window
    "wj"     'evil-window-down
    "wk"     'evil-window-up
    "wh"     'evil-window-left
    "wl"     'evil-window-right
    "wd"     'delete-window
    "wD"     'delete-other-windows
    "ww"     'ace-window
    "wo"     'other-window
    "w="     'balance-windows
    "W"      '(:keymap evil-window-map)
    "SPC"    'execute-extended-command
    )

  ;; global VISUAL mode map
  (general-vmap
    ";" 'comment-or-uncomment-region)

  ;; top right button on my trackball is equivalent to click (select) +
  ;; RET (open) on files in Treemacs
  (general-define-key
   :keymaps 'treemacs-mode-map
   "<mouse-8>" 'treemacs-RET-action)

Org Mode Settings

Some default evil bindings

  (use-package evil-org)

Image drag-and-drop for org-mode

  (use-package org-download)

Install some tools for archiving web content into Org

  (use-package org-web-tools)

Fontify the whole line for headings (with a background color)

  (setq org-fontify-whole-heading-line t)

disable the default editing window layout

instead, just replace the current window with the editing one..

indent and wrap long lines

  (add-hook 'org-mode-hook 'org-indent-mode)
  (add-hook 'org-mode-hook 'visual-line-mode)

Enable execution of languages from Babel

  (org-babel-do-load-languages 'org-babel-load-languages
                               '(
                                 (sql . t)
                                 (python . t)
                                 (shell . t)
                                 )
                               )

set Org-specific keybindings

  (my-local-leader-def
    :states  'normal
    :keymaps 'org-mode-map
    "y"      'org-store-link
    "i"      'org-toggle-inline-images
    "p"      'org-insert-link
    "x"      'org-babel-execute-src-block
    "s"      'org-insert-structure-template
    "e"      'org-edit-src-code
    "t"      'org-babel-tangle
    "o"      'org-export-dispatch
    "TAB"    'org-toggle-heading
    )

  (general-define-key
   :states  'normal
   :keymaps 'org-mode-map
   "TAB"    'evil-toggle-fold)

Export Settings

GitHub-flavored markdown

  (use-package ox-gfm)

HTMLize

htmlize prints the current buffer or file, as it would appear in Emacs, but in HTML! It's super cool and a comment

  (use-package htmlize)

enable markdown export

  (eval-after-load "org"
    (progn
      '(require 'ox-md nil t)
      '(require 'ox-gfm nil t)))

explicitly set utf-8 output (apparently)

  (setq org-export-coding-system 'utf-8)

custom todo states

 (setq org-todo-keywords
       '((sequence "TODO(t)"     "|" "IN PROGRESS(p)" "|" "DONE(d)" "|" "STUCK(s)" "|" "WAITING(w)")
         (sequence "OPEN(o)" "|" "INVESTIGATE(v)" "|" "IMPLEMENT(i)" "|" "REVIEW(r)" "|" "MERGED(m)" "|" "RELEASED(d)" "|" "ABANDONED(a)")
         (sequence "QUESTION(q)" "|" "ANSWERED(a)")))

TODO Faces

  (setq org-todo-keyword-faces
        '(("IN PROGRESS" . org-warning) ("STUCK" . org-done)
          ("WAITING" . org-warning)))

Org-Protocol

Org-Protocol is super cool! It enables things like bookmarklets to bookmark things to Org files!

  ;; enable org-protocol
  (require 'org-protocol)

enter follows links.. how was this not a default?

  (setq org-return-follows-link  t)

Use a variable-pitch font in Org-Mode

Org is mostly prose and prose should be read in a variable-pitch font where possible. This changes fonts in Org to be variable-pitch where it makes sense

  (add-hook 'org-mode-hook 'variable-pitch-mode)

Inside of code blocks I want a fixed-pitch font

  (defun ian-org-fixed-pitch ()
    "Fix fixed pitch text in Org Mode"
    (set-face-attribute 'org-table nil :inherit 'fixed-pitch)
    (set-face-attribute 'org-block nil :inherit 'fixed-pitch))

  (add-hook 'org-mode-hook 'ian-org-fixed-pitch)

Useful anchors in HTML export

This is taken from github.com/alphapapa's Unpackaged.el collection, unmodified.

  (eval-when-compile
    (require 'easy-mmode)
    (require 'ox))

  (define-minor-mode unpackaged/org-export-html-with-useful-ids-mode
    "Attempt to export Org as HTML with useful link IDs.
  Instead of random IDs like \"#orga1b2c3\", use heading titles,
  made unique when necessary."
    :global t
    (if unpackaged/org-export-html-with-useful-ids-mode
        (advice-add #'org-export-get-reference :override #'unpackaged/org-export-get-reference)
      (advice-remove #'org-export-get-reference #'unpackaged/org-export-get-reference)))

  (defun unpackaged/org-export-get-reference (datum info)
    "Like `org-export-get-reference', except uses heading titles instead of random numbers."
    (let ((cache (plist-get info :internal-references)))
      (or (car (rassq datum cache))
          (let* ((crossrefs (plist-get info :crossrefs))
                 (cells (org-export-search-cells datum))
                 ;; Preserve any pre-existing association between
                 ;; a search cell and a reference, i.e., when some
                 ;; previously published document referenced a location
                 ;; within current file (see
                 ;; `org-publish-resolve-external-link').
                 ;;
                 ;; However, there is no guarantee that search cells are
                 ;; unique, e.g., there might be duplicate custom ID or
                 ;; two headings with the same title in the file.
                 ;;
                 ;; As a consequence, before re-using any reference to
                 ;; an element or object, we check that it doesn't refer
                 ;; to a previous element or object.
                 (new (or (cl-some
                           (lambda (cell)
                             (let ((stored (cdr (assoc cell crossrefs))))
                               (when stored
                                 (let ((old (org-export-format-reference stored)))
                                   (and (not (assoc old cache)) stored)))))
                           cells)
                          (when (org-element-property :raw-value datum)
                            ;; Heading with a title
                            (unpackaged/org-export-new-title-reference datum cache))
                          ;; NOTE: This probably breaks some Org Export
                          ;; feature, but if it does what I need, fine.
                          (org-export-format-reference
                           (org-export-new-reference cache))))
                 (reference-string new))
            ;; Cache contains both data already associated to
            ;; a reference and in-use internal references, so as to make
            ;; unique references.
            (dolist (cell cells) (push (cons cell new) cache))
            ;; Retain a direct association between reference string and
            ;; DATUM since (1) not every object or element can be given
            ;; a search cell (2) it permits quick lookup.
            (push (cons reference-string datum) cache)
            (plist-put info :internal-references cache)
            reference-string))))

  (defun unpackaged/org-export-new-title-reference (datum cache)
    "Return new reference for DATUM that is unique in CACHE."
    (cl-macrolet ((inc-suffixf (place)
                               `(progn
                                  (string-match (rx bos
                                                    (minimal-match (group (1+ anything)))
                                                    (optional "--" (group (1+ digit)))
                                                    eos)
                                                ,place)
                                  ;; HACK: `s1' instead of a gensym.
                                  (-let* (((s1 suffix) (list (match-string 1 ,place)
                                                             (match-string 2 ,place)))
                                          (suffix (if suffix
                                                      (string-to-number suffix)
                                                    0)))
                                    (setf ,place (format "%s--%s" s1 (cl-incf suffix)))))))
      (let* ((title (org-element-property :raw-value datum))
             (ref (url-hexify-string (substring-no-properties title)))
             (parent (org-element-property :parent datum)))
        (while (--any (equal ref (car it))
                      cache)
          ;; Title not unique: make it so.
          (if parent
              ;; Append ancestor title.
              (setf title (concat (org-element-property :raw-value parent)
                                  "--" title)
                    ref (url-hexify-string (substring-no-properties title))
                    parent (org-element-property :parent parent))
            ;; No more ancestors: add and increment a number.
            (inc-suffixf ref)))
        ref)))

  (add-hook 'org-mode-hook 'unpackaged/org-export-html-with-useful-ids-mode)

Disable pretty entities

I find superscripts, subscripts, etc, are less common than verbatim underscores and such so I am changing the default for this setting

  (setq org-pretty-entities nil)

Using eglot with Org

eglot and Babel seems to still be a work in progress as of <2024-11-16 Sat>.

The code snippet below, from this comment on the thread about adding support for Org Babel, seems like it could be a temporary solution as support seems to be slow in coming, but I haven't tried it out yet.

I'm storing it here as an example for when I'm ready – I happened upon this while searching for something else and don't want to lose it, but I'm not ready for it right this moment either.

(require 'eglot)

(defun sloth/org-babel-edit-prep (info)
  (setq buffer-file-name (or (alist-get :file (caddr info))
                             "org-src-babel-tmp"))
  (eglot-ensure))

(advice-add 'org-edit-src-code
            :before (defun sloth/org-edit-src-code/before (&rest args)
                      (when-let* ((element (org-element-at-point))
                                  (type (org-element-type element))
                                  (lang (org-element-property :language element))
                                  (mode (org-src-get-lang-mode lang))
                                  ((eglot--lookup-mode mode))
                                  (edit-pre (intern
                                             (format "org-babel-edit-prep:%s" lang))))
                        (if (fboundp edit-pre)
                            (advice-add edit-pre :after #'sloth/org-babel-edit-prep)
                          (fset edit-pre #'sloth/org-babel-edit-prep)))))

Define how org-edit-src behaves

Do M-x describe-variable RET org-src-window-setup to see the options

  (setq org-src-window-setup 'other-frame)

Miscellaneous standalone global configuration changes

Start server

  (server-start)

Opening the Remote Repo in the Browser from Emacs

https://github.com/rmuslimov/browse-at-remote

  (use-package browse-at-remote)

Opening Sources in Emacs from the Browser

https://orgmode.org/worg/org-contrib/org-protocol.html

First use this .desktop file to register a handler for the new protocol scheme:

  [Desktop Entry]
  Name=org-protocol
  Comment=Intercept calls from emacsclient to trigger custom actions
  Categories=Other;
  Keywords=org-protocol;
  Icon=emacs
  Type=Application
  Exec=org-protocol %u
  #Exec=emacsclient -- %u
  Terminal=false
  StartupWMClass=Emacs
  MimeType=x-scheme-handler/org-protocol;

After tangling that file to its destination, run the following command to update the database:

update-desktop-database ~/.local/share/applications/

Add the custom org-protocol script to intercept calls from the browser, do any necessary pre-processing, and hand off the corrected input to emacsclient:

  # for some reason the bookmarklet strips a colon, so use sed to remove
  # the botched prefix and rebuild it correctly
  emacsclient -- org-protocol://open-source://$(echo "$@" | sed 's#org-protocol://open-source//##g') | tee /tmp/xdg-emacsclient
  # that's probably a useless call to echo but whatever

For now this is extremely rudimentary and I will improve it as needed.

Manual Steps:

  1. The first time, add a button in the browser by creating a bookmarklet containing the following target:
Source Code
;; Defined in /usr/local/share/emacs/29.1/lisp/org/org-protocol.el.gz
(defun org-protocol-open-source (fname)
  "Process an org-protocol://open-source?url= style URL with FNAME.

Change a filename by mapping URLs to local filenames as set
in `org-protocol-project-alist'.

The location for a browser's bookmark should look like this:

  javascript:location.href = \\='org-protocol://open-source?\\=' +
        new URLSearchParams({url: location.href})
  1. Add an entry to org-protocol-project-alist, defined in the local machine's hostname-specific config found in local/. An example can be found on the Worg page above, but here it is again for easy reference:
(setq org-protocol-project-alist
      '(("Worg"
         :base-url "https://orgmode.org/worg/"
         :working-directory "/home/user/worg/"
         :online-suffix ".html"
         :working-suffix ".org")
        ("My local Org-notes"
         :base-url "http://localhost/org/"
         :working-directory "/home/user/org/"
         :online-suffix ".php"
         :working-suffix ".org")))

N.B. this code block does not get tangled into init.el.

TODO automate the cloning of unknown repos and addition to this list

I want to be able to press the button on new repos that I haven't cloned yet, and have them dumped to a sane location and then added to the list and opened.

TODO test a new bookmarklet with the quoted syntax from the Org-Protocol docs

also, I stumbled on this in the docs and it looks interesting and relevant:

;;   5.) Optionally add custom sub-protocols and handlers:
;;
;;       (setq org-protocol-protocol-alist
;;             '(("my-protocol"
;;                :protocol "my-protocol"
;;                :function my-protocol-handler-function)))
;;
;;       A "sub-protocol" will be found in URLs like this:
;;
;;           org-protocol://sub-protocol://data
;;
;; If it works, you can now setup other applications for using this feature.

TRAMP settings

Only one setting at the moment: use ssh instead of scp when accessing files with ssh: schemes

  (setq tramp-default-method "ssh")

Disable most warnings

Honestly I'm not good enough at Emacs to make sense of most of them anyway

(setq warning-minimum-level :emergency)

Theme Switching Helpers

Save the current theme to a global variable so it can be referenced later

  (defun load-theme--save-new-theme (theme &rest args)
    (setq ian-current-theme theme))
  (advice-add 'load-theme :before #'load-theme--save-new-theme)

Line Numbers in Programming Buffers

  (add-hook 'prog-mode-hook 'display-line-numbers-mode)
  (setq display-line-numbers-type 'relative)

Transparency toggle

I definitely lifted this from somewhere but failed to document where I got it :\ Probably from Spacemacs. Thanks, Spacemacs.

      (defun toggle-transparency ()
        (interactive)
        (let ((alpha (frame-parameter nil 'alpha)))
          (set-frame-parameter
           nil 'alpha
           (if (eql (cond ((numberp alpha) alpha)
                          ((numberp (cdr alpha)) (cdr alpha))
                          ;; Also handle undocumented (<active> <inactive>) form.
                          ((numberp (cadr alpha)) (cadr alpha)))
                    100)
               '95 '(100 . 100)))))

Switch to last buffer

This one lifted from https://emacsredux.com/blog/2013/04/28/switch-to-previous-buffer/

TODO: Make this behave like alt-tab in Windows, but for buffers. I think hycontrol may come in handy (Hyperbole).

    (defun er-switch-to-previous-buffer ()
      (concat
        "Switch to previously open buffer."
        "Repeated invocations toggle between the two most recently open buffers.")
        (interactive)
        (switch-to-buffer (other-buffer (current-buffer) 1)))

Fix Home/End keys

Emacs has weird behavior by default for Home and End and this change makes the behavior "normal" again.

      (global-set-key (kbd "<home>") 'move-beginning-of-line)
      (global-set-key (kbd "<end>") 'move-end-of-line)

Customize the frame (OS window) title

Taken from StackOverflow, at least for now, which does 90% of what I want and can serve as a future reference of how to customize this aspect of Emacs. This displays the file name and major mode in the OS title bar. Will have to find the documentation that defines the format string passed to frame-title-format at some point.

(setq-default frame-title-format '("%f [%m]"))

Tweak align-regexp

Configure align-regexp to use spaces instead of tabs. This is mostly for this file. When my keybindings are in two columns and M-x align-regexp uses tabs, the columns look aligned in Emacs but unaligned on GitHub. Using spaces faces this. This snippet effects that change.

Lifted from StackOverflow:

https://stackoverflow.com/questions/22710040/emacs-align-regexp-with-spaces-instead-of-tabs

      (defadvice align-regexp (around align-regexp-with-spaces activate)
        (let ((indent-tabs-mode nil))
          ad-do-it))

Configure automatic backup/recovery files

I don't like how Emacs puts temp files in the same directory as the file, as this litters the current working directory and makes git branches dirty. These are some tweaks to store those files in /tmp.

  (setq make-backup-files nil)
  (setq backup-directory-alist `((".*" . "/tmp/.emacs-saves")))
  (setq backup-by-copying t)
  (setq delete-old-versions t)

Autosave

Automatically saves the file when it's been idle for 5 minutes.

  ;; autosave
  (setq auto-save-visited-interval 300)
  (auto-save-visited-mode
   :diminish
   )

Default window size

Just a bigger size that I prefer..

  (add-to-list 'default-frame-alist '(width . 128))
  (add-to-list 'default-frame-alist '(height . 60))

Unclutter global modeline

Some global minor modes put themselves in the modeline and it gets noisy, so remove them from the modeline.

  ;; hide some modes that are everywhere
  (diminish 'eldoc-mode)
  (diminish 'undo-tree-mode)
  (diminish 'auto-revert-mode)
  (diminish 'evil-collection-unimpaired-mode)
  (diminish 'yas-minor-mode-major-mode)

Less annoying bell

Flashes the modeline foreground instead of whatever the horrible default behavior was (I don't even remember).

  (setq ring-bell-function
        (lambda ()
          (let ((orig-fg (face-foreground 'mode-line)))
            ;; change the flash color here
            ;; overrides themes :P
            ;; guess that's one way to do it
            (set-face-foreground 'mode-line "#F2804F")
            (run-with-idle-timer 0.1 nil
                                 (lambda (fg) (set-face-foreground 'mode-line fg))
                                 orig-fg))))

(from Emacs wiki)

Remove toolbar, scrollbars, and menu

Removes the toolbar and menu bar (file menu, etc) in Emacs because I just use M-x for everything.

  (when (fboundp 'menu-bar-mode) (menu-bar-mode -1))
  (when (fboundp 'tool-bar-mode) (tool-bar-mode -1))
  (scroll-bar-mode -1)
  (defun my/disable-scroll-bars (frame)
    (modify-frame-parameters frame
                             '((vertical-scroll-bars . nil)
                               (horizontal-scroll-bars . nil))))
  (add-hook 'after-make-frame-functions 'my/disable-scroll-bars)

Enable context menu on right click

  (context-menu-mode t)

Enable the mouse in the terminal

  (xterm-mouse-mode 1)

Disable "nice" names in Customize

I prefer that Customize display the names of variables that I can change in this file, rather than the human-readable names for people who customize their Emacs through M-x customize

  (setq custom-unlispify-tag-names nil)

Don't require a final newline

Very occasionally this causes problems and it's not something that I actually care about. To be honest I do not know why Emacs has a default behavior where it adds a newline to the end of the file on save.

  (setq require-final-newline nil)

Caps lock mode

For those of us who did away with the caps lock button but write SQL sometimes

  (use-package caps-lock)

Allow swapping windows with ctrl + shift + left-click-drag

  (defvar window-swap-origin nil)

  (defun window-swap-start (event)
    "Start swapping windows using mouse events."
    (interactive "e")
    (setq window-swap-origin (posn-window (event-start event))))

  (defun window-swap-end (event)
    "End swapping windows using mouse events."
    (interactive "e")
    (let ((origin window-swap-origin)
          (target (posn-window (event-end event))))
      (window-swap-states origin target))
    (setq window-swap-origin nil))

  (global-set-key (kbd "<C-S-mouse-1>") 'window-swap-start)
  (global-set-key (kbd "<C-S-drag-mouse-1>") 'window-swap-end)

Kagi integration

I love Kagi and even if it costs a few cents per query I would like to have it accessible from Emacs. Uses API key stored in ~/.secret.el~ as configured in the "load secrets" section above

Basic config

  (use-package kagi
    :custom
    (kagi-api-token  (password-store-get "kagi-token"))

    ;; Universal Summarizer settings
    (kagi-summarizer-default-language "EN")
    (kagi-summarizer-cache t))

Org Babel Support

Kagi FastGPT is also supported in Org Babel blocks, which will be nice if I ever use it and want to capture the resposnes alongside notes

  (use-package ob-kagi-fastgpt
    :ensure nil  ; provided by the kagi package
    :after org
    :config
    (ob-kagi-fastgpt-setup))

Then create a source block with 'language' β€˜kagi-fastgpt’:

  Can Kagi FastGPT be used in Org mode?

LLM integration

  (use-package gptel)

  (setq
   gptel-model 'llama3.2:latest
   gptel-backend (gptel-make-ollama "Ollama"
                   :host "localhost:11434" 
                   :stream t
                   :models '((mistral:latest)
                             (llama3.2:latest))))

  (gptel-make-kagi "Kagi"
    :key (password-store-get "kagi-token"))

Emacs Everywhere

Sadly this only works in X11 but there's a long Wayland support issue, and it looks like a lot of progress has been made! So hopefully this will get updated to work in Wayland before I upgrade to the next LTS.. whenever I do that, lol.

  (use-package emacs-everywhere)

Confirm before exit

<2024-11-16 Sat>: Don't know why I didn't do this sooner! With my muscle memory for :wq I close by mistake Emacs constantly – especially since I've been using vim bindings now for multiple decades and I use emacsclient heavily, so a lot of the time I actually do wish to call evil-exit… just not on that last frame!

  (setq confirm-kill-emacs 'yes-or-no-p)

Start a new blog post

I used Kagi FastGPT to generate about half of this. It taught me about read-string and replace-regexp-in-string and wrote the little regexp for me. I tweaked the output to put the blog in the right place and open the new file once it's created. I guess it was nice to have some of it generated.

  (defun silly-business/new-blog-post ()
    "Create a new silly.business blog post."
    (interactive)
    (let* ((post-title (read-string "Enter the title of the new post: "))
           (post-slug (replace-regexp-in-string "\\s-+" "-" post-title))
           (timestamp (format-time-string "%Y-%m-%d-%H:%M")))
      (shell-command (concat "cd ~/silly.business && hugo new blog/"
                             timestamp (format "-%s.org" post-slug)))
      (find-file (format "~/silly.business/content/blog/%s.org"
                         (concat timestamp "-" post-slug)))
                 ))

Hostname-based tweaks

This is a simple convention that I use for loading machine-specific configuration for the different machines I run Emacs on.

  1. looks for Org files in /home/$USER/.emacs.d/local/ with a name that is the same as the hostname of the machine.
  2. shells out to call hostname to determine the hostname.
  3. tangles that .org file to a .el file and executes it

This allows configuration to diverge to meet needs that are unique to a specific workstation.

  (let ;; find the hostname and assign it to a variable
       ((hostname (string-trim-right
                   (shell-command-to-string "cat /etc/hostname"))))

     (progn
       (org-babel-tangle-file
        (concat "~/.emacs.d/local/" hostname ".org")
        (concat hostname ".el"))

       (load (concat "~/.emacs.d/local/" hostname ".el"))
       (require 'local)))

There must be an Org file in local/ named $(hostname).org or init actually breaks. This isn't great but for now I've just been making a copy of one of the existing files whenever I start on a new machine. It may someday feel worth my time to automate this, but so far it hasn't been worth it, and I just create local/"$(hostname).org" as part of initial setup, along with other tasks that I do not automate in this file.

Launching Emacsclient

Nifty shell function for hassle-free starting of emacsclient

  args=""
  nw=false
  # check if emacsclient is already running
  if pgrep -U $(id -u) emacsclient > /dev/null; then running=true; fi

  # check if the user wants TUI mode
  for arg in "$@"; do
      if [ "$arg" = "-nw" ] || [ "$arg" = "-t" ] || [ "$arg" = "--tty" ]
      then
          nw=true
      fi
  done

  # if called without arguments - open a new gui instance
  if [ "$#" -eq "0" ] || [ "$running" != true ]; then
      args=(-c $args)           # open emacsclient in a new window
  fi
  if [ "$#" -gt "0" ]; then
      # if 'em -' open standard input (e.g. pipe)
      if [[ "$1" == "-" ]]; then
          TMP="$(mktemp /tmp/emacsstdin-XXX)"
          cat >$TMP
          args=($args --eval '(let ((b (generate-new-buffer "*stdin*"))) (switch-to-buffer b) (insert-file-contents "'${TMP}'") (delete-file "'${TMP}'"))')
      else
          args=($@ $args)
      fi
  fi

  # emacsclient $args
  if $nw; then
      emacsclient "${args[@]}"
  else
      (nohup emacsclient "${args[@]}" > /dev/null 2>&1 &) > /dev/null
  fi

Running Emacs properly from the GUI

This .desktop file calls emacs when it's not already running, and emacsclient otherwise. Slow on first launch, then fast for every new frame thereafter.

Tangling this file will install the .desktop file to the correct location (~/.local/share/applications/Emacsclient.desktop).

  [Desktop Entry]
  Name=Emacs
  GenericName=Text Editor
  Comment=Edit text
  MimeType=text/english;text/plain;text/x-makefile;text/x-c++hdr;text/x-c++src;text/x-chdr;text/x-csrc;text/x-java;text/x-moc;text/x-pascal;text/x-tcl;text/x-tex;application/x-shellscript;text/x-c;text/x-c++;
  Exec=emacsclient -c -a "emacs" %F
  Icon=emacs
  Type=Application
  Terminal=false
  Categories=Development;TextEditor;Utility;
  StartupWMClass=Emacs

TODO Figure out how to run Emacs as a daemon so that closing the last frame doesn't exit

Launching in headless mode introduces some font problems (fonts don't load when changing themes) that I haven't been able to debug.

Compiling Emacs from Source

Some notes on the dependencies that I found were needed to build Emacs 29.1 on fresh Ubuntu with the configuration flags that I like

./autogen.sh
sudo apt-get install make autoconf libx11-dev libmagickwand-dev libgtk-3-dev libwebkit2gtk-4.0-dev libgccjit-11-dev libxpm-dev libgif-dev libgnutls28-dev libjansson-dev libncurses-dev texinfo libtree-sitter-dev
./configure --with-imagemagick --with-xwidgets --with-json --with-x-toolkit=gtk3 --with-native-compilation --with-mailutils --with-x --with-tree-sitter --without-toolkit-scroll-bars

#Emacs