Skip to content

jladdjr/emacs.d

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Introduction

Pre-flight steps

Initialize skip-dev-config (if necessary). This variable indicates whether to skip sections focused on installing developpment-focused packages.

; initialize skip-dev-config if not defined
(if (not (boundp 'skip-dev-config))
    (setq skip-dev-config nil))

Uncomment to profile loading profile.

;(require 'profiler)
;(profiler-start 'cpu)

Bootstrap Package Manager

Using straight.el and use-package.

See this article for benefits of using straight.el.

Bootstrap straight

(message "bootstrapping straight.el")

(defvar bootstrap-version)
(let ((bootstrap-file
       (expand-file-name "straight/repos/straight.el/bootstrap.el" user-emacs-directory))
      (bootstrap-version 6))
  (unless (file-exists-p bootstrap-file)
    (with-current-buffer
        (url-retrieve-synchronously
         "https://raw.githubusercontent.com/radian-software/straight.el/develop/install.el"
         'silent 'inhibit-cookies)
      (goto-char (point-max))
      (eval-print-last-sexp)))
  (load bootstrap-file nil 'nomessage))

Disable package.el in favor of straight.el

(setq package-enable-at-startup nil)

Install use-package

use-package allows for you to install and configure a package in one step.

(straight-use-package 'use-package)

Configure use-package to use straight.el by default

(message "load straight")
(use-package straight
    :custom (straight-use-package-by-default t))

Now in order to install a package, all we need to do is:

; (use-package evil-commentary)

There are options for configuring packages with straight.el, too:

  • :init - code that will be run before installing the package
  • :config - code that will be run right after the package is installed
  • :bind - adds key bindings after a module has been installed
  • :custom: - set customizable variables

See the straight.el getting started guide for more documentation on how to load and configure packages with straight.el.

Add Melpa as Package Source

Add Milkypostman’s Emacs Lisp Package Archive (MELPA) as a package archive.

This was first introduced in order to install Org-roam.

(require 'package)
(add-to-list 'package-archives
             '("melpa" . "http://melpa.org/packages/") t)

Global Preferences

Disable GUI elements

Disable GUI elements. Will rely on key bindings.

(scroll-bar-mode -1)
(tool-bar-mode -1)
(menu-bar-mode -1)

Disable tabs

Never, ever use tabs.

(setq-default indent-tabs-mode nil)

Highlight current line

It’s easy to lose track of where the cursor is. Globally enable line highlighting.

Continue to highlight a line even when focus has moved to another window.

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

Show trailing whitespace

Otherwise, don’t notice this until it’s time to commit changes.

(setq-default show-trailing-whitespace t)

Misc

(show-paren-mode 1)
(setq initial-scratch-message nil)
(setq confirm-kill-emacs 'y-or-n-p)
(setq inhibit-startup-message t)
(setq initial-scratch-message "")
(fset 'yes-or-no-p 'y-or-n-p)

Allow narrowing to a region (C-x n n)

(put 'narrow-to-region 'disabled nil)

Global Keybinding Tweaks

Replace M-x with C-m

M-x is frequently used, but not as convenient to enter as a Control command. Globally replace M-x with C-m.

(keyboard-translate ?\C-m ?\M-x)

Map C-k to kill-buffer

I need to kill buffers all. the. time. Let’s make it easier.

(global-set-key (kbd "C-k") #'kill-current-buffer)

Map C-. to delete-window

I also need to kill windows all the time. Let’s make that easier, too.

(global-set-key (kbd "C-.") #'delete-window)

Configure Dired

Enabled Dired Omit Mode to hide uninteresting files. Update regex filter used to omit files so that Emacs backup files are excluded, too.

(setq dired-omit-files "\\`[.]?#\\|~$\\|__pycache__\\|.swp\\|.pytest_cache")
(add-hook 'dired-mode-hook (lambda ()
                             (dired-omit-mode)))

Configure org-mode

org-mode code blocks may seem to have improper indenting. Remember to use C-c ’ to get proper indenting.

Create recommended keybindings for org-mode.

(global-set-key (kbd "C-c l") #'org-store-link)
(global-set-key (kbd "C-c a") #'org-agenda)

Add support for TODO, DOING, and DONE states when working with TODO items.

(setq org-todo-keywords
      '((sequence
         ;; open items
         "TODO"
         "DOING"
         "|"  ; entries after pipe are considered completed in [%] and [/]
         ;; closed items
         "DONE"
         "BLOCKED"
         )))

(setq org-todo-keyword-faces
      '(
        ("TODO" . "light pink")
        ("DOING" . "yellow")
        ("DONE" . "light green")
        ("BLOCKED" . "red")
        ))

Include org files in agenda if they are in \~/org/agenda.

(setq org-directory (expand-file-name "~/org"))

(let ((agenda-dir (expand-file-name "agenda" org-directory)))
    (setq org-agenda-files (list agenda-dir))
    (setq org-default-notes-file (expand-file-name "notes.org" agenda-dir)))

Enable auto-fill-mode for org-mode. Set fill-column to 80.

(setq fill-column 80)
(add-hook 'org-mode-hook 'turn-on-auto-fill)

Add support for links that open PDFs to a given page. (Retrieved from this emacs.stackexchange.com answer on 2023-08-11).

(defun org-pdf-open (link)
  "Where page number is 105, the link should look like:
   [[pdf:/path/to/file.pdf#page=105][My description.]]"
  (let* ((path+page (split-string link "#page="))
         (pdf-file (car path+page))
         (page (car (cdr path+page))))
    (start-process "view-pdf" nil "evince" "--page-index" page pdf-file)))

(org-add-link-type "pdf" 'org-pdf-open nil)

Install org-download

Install org-download.

Instruct org to always display inline images.

Configure org-download to store images in an images directory located in the current directory of the Org file.

Finally, instruct org-download to not use the org-mode heading to help organize images on the file system (e.g. do not create a sub-directory with the current heading’s name).

Bind org-download-clipboard – which “pastes” the contents of the clipboard into the current org file – to M-g.

        ; Ensure org-pictures directory exists
(message "load org-download")
(use-package org-download
  :config
  (add-hook 'dired-mode-hook 'org-download-enable)
  :custom
  (org-startup-with-inline-images t)
  (org-download-image-dir "images")
  (org-download-heading-lvl nil)
  :bind
  ("M-g" . org-download-clipboard))

Install evil

See this page for information on how to get started with evil mode.

In the config section, set evil-want-C-i-jump to nil since C-i is tab and we want to preserve tab’s default behavior. (If we don’t set this to nil, tab will invoke evil-jump-forward instead of org-cycle in org-mode, for example, preventing us from cycling through the different folding options for a node).

For some reason, the above approach works in Debian, but not Mac OSX. Taking things a step further, we also use with-eval-after-load to forcefully unset tab in evil-motion-state-map. (Found this approach here.)

(message "load evil")
(use-package evil
  :config
  (evil-mode)
  (evil-set-initial-state 'help-mode 'emacs)
  (evil-set-initial-state 'Info-mode 'emacs)
  (evil-set-initial-state 'ivy-occur-mode 'emacs)
  (evil-set-undo-system 'undo-tree)
  (setq evil-want-C-i-jump nil)
  (with-eval-after-load 'evil-maps (define-key evil-motion-state-map (kbd "TAB") nil))
  (with-eval-after-load 'evil-maps (define-key evil-motion-state-map (kbd "C-b") 'org-roam-node-find))
  (with-eval-after-load 'evil-maps (define-key evil-normal-state-map (kbd "C-r") 'org-roam-capture))
  (with-eval-after-load 'evil-maps (define-key evil-motion-state-map (kbd "C-d") 'avy-goto-char-timer))
  (with-eval-after-load 'evil-maps (define-key evil-normal-state-map (kbd "C-p") 'projectile-command-map))
  (with-eval-after-load 'evil-maps (define-key evil-normal-state-map (kbd "C-.") 'delete-window)))

Install magit

We can’t go anywhere without Magit!

Include a hook that drops the user into emacs mode when prompted for a Git commit message.

(message "load magit")
(use-package magit
  :config
  (add-hook 'git-commit-mode-hook 'evil-emacs-state))

Make the Magit status window the only window in view when it opens.

(defun jl/magit-status ()
  "Open magit-status window by itself"
  (interactive)
  (magit-status)
  (delete-other-windows))
(define-key (current-global-map) [remap magit-status] 'jl/magit-status)

Install browse-at-remote

browse-at-remote opens the GitHub page corresponding to current location in buffer.

(use-package browse-at-remote)
(global-set-key (kbd "C-c g g") 'browse-at-remote)

; When working with Enterprise GitHub, let browse-at-remote
; know that the remote represents a Git Hub repository by running:
; > git config --add browseAtRemote.type "github"

; If all remotes use github, you can apply this setting globally with:
; > git config --global --add browseAtRemote.type "github"

Install org-roam

Install Org-roam.

Be aware that Org-roam tends to assume that newer versions of Emacs packages are installed (e.g. org-mode, magit).

(message "load org-roam")
(use-package org-roam
  :ensure t
  :bind (("C-c h" . (lambda () (interactive) (call-interactively 'org-roam-buffer-toggle) (other-window 1)))
         ("C-c i" . org-roam-node-insert)
         ("C-c u" . org-roam-dailies-goto-today)
         ("C-c y" . org-roam-dailies-goto-yesterday)
         ("C-c n" . jl/org-roam-goto-week-by-number))
  :config
  (setq org-roam-completion-everywhere t))

Create and configure default Org-roam directory.

(make-directory "~/org-roam" t)
(setq org-roam-directory (file-truename "~/org-roam"))

Prompt Org-roam indexing

This may take some time during the first run. Subsequent runs should be much faster, as they will only process modified files.

Can call M-x org-roam-db-sync interactively to re-index.

(org-roam-db-autosync-mode)

Configure org-roam capture templates

  (setq org-roam-capture-templates
    '(("d" "default" plain "%?"
        :target (file+head "%<%Y%m%d%H%M%S>-${slug}.org" "#+title: ${title}
  ")
        :unnarrowed t)
      ("c" "common template" plain "* Overview

\* Reference

\* Links

%?

\* History

\* See Also

"       :target (file+head "%<%Y%m%d%H%M%S>-${slug}.org" "#+title: ${title}
")
        :unnarrowed t)
        ("h" "history note" plain "** %<%Y-%m-%d %H:%M> %?"
         :target (file+head+olp "%<%Y%m%d%H%M%S>-${slug}.org" "#+title: ${title}"
                                ("History"))
         :empty-lines 1)
        ("y" "year note" plain "* %<%Y>"
         :target (file+head "%<%Y%m%d%H%M%S>-${slug}.org" "#+title: ${title}
"))))
  (global-set-key (kbd "C-r") #'org-roam-capture)

Install YASnippet

Install YASnippet.

Snippet examples available here.

Walkthrough of using snippets available here.

(message "load yasnippet")
(use-package yasnippet
  :config
  (yas-global-mode)
  (setq yas-indent-line 'fixed))

Install yaml-mode

Install yaml-mode.

More information on yaml-mode is available here.

(message "load yaml-mode")
(use-package yaml-mode
  :config
  (add-to-list 'auto-mode-alist '("\\.yml\\'" . yaml-mode)))

Install perspective

Perspective offers the ability to:

  • Create (named) window layouts, refered to as perspectives
  • Save perspectives to disk
  • Only list buffers used by current perspective

Key Perspective commands are outlined here.

The Perspective prefix key is set to C-c C-z below.

(message "load perspective")
(use-package perspective
  :custom
  (persp-mode-prefix-key (kbd "C-c C-z"))
  :init
  (persp-mode)
  :config
  (setq persp-state-default-file "~/.emacs.d/persp-"))

Install projectile

Projectile offers several commands for interacting with files within the scope of a project.

Map projectile-find-file to C-f in the evil-normal-state-map because of how frequently this gets called. We map this in evil-normal-state-map specifically so that C-f is not shadowed in other modes where it is less likely to be used anyways.

(message "load projectile")
(use-package projectile
  :init
  (setq projectile-project-search-path
        '("~/git/" "~/org/" "~/.emacs.d"
          ("~/johnny-bookkeeper" . 5)
          ("~/johnny-do-your-work" . 5)
          ("~/johnny-inventory" . 5)
          ("~/johnny-jim" . 5)
          ("~/johnny-shared" . 5)
          ("~/johnny-steph" . 5)))
  (projectile-mode)
  (with-eval-after-load 'evil-maps (define-key evil-normal-state-map (kbd "C-f") 'projectile-find-file))
  :bind
  (:map projectile-command-map
    ("s s" . (lambda () (interactive) (call-interactively 'projectile-ag) (other-window 1)))))

Install the ag package as well so that projectile can make ag searches.

Map projectile-ag to C-n in the evil-normal-state-map because of how frequently this gets called. We map this in evil-normal-state-map specifically so that C-n is not shadowed in other modes where it is less likely to be used anyways.

As a convenience, wrap projectile-ag in a lambda function that automatically switches us over to the other window. (Did not find any way to configure this behavior using projectile variables or function arguments).

(message "load ag")
(use-package ag
  :init
  (with-eval-after-load 'evil-maps
    (define-key evil-normal-state-map (kbd "C-n")
      (lambda () (interactive)
        (call-interactively 'projectile-ag)
        (other-window 1))))
  :config
  (add-hook 'ag-mode-hook 'evil-emacs-state))

Create a custom initial project view that will be used when invoking projectile-switch-project.

(defun open-last-modified-scm-file ()
  "Determine most recently modified file according to Git"
  (interactive)
  (let ((my-output-buffer (generate-new-buffer "*git-last-modified-file*"))
        (my-project-directory (cdr (project-current))))
    (call-process "git" nil my-output-buffer nil "-C" my-project-directory "log" "-1" "--name-only" "--format=oneline" "--no-merges")
    (save-excursion
      (with-current-buffer my-output-buffer
        (goto-char (point-max))
        (join-line)
        (setq my-last-modified-scm-file (concat my-project-directory (buffer-substring (point-at-bol) (point-at-eol))))
        (kill-buffer my-output-buffer))))
  (find-file my-last-modified-scm-file))

(defun jl/default-project-view ()
  ""
  (interactive)

  (if (one-window-p)
    ; if only one window is open, proceed with opening a full project workspace
    (progn
       ; clear all other windows
       (delete-other-windows)

       ; in first window, show directory
       (let* ((project-abs-path (cdr (project-current)))
             (project-name (file-name-nondirectory (directory-file-name
                                                    (expand-file-name project-abs-path)))))
         (dired (cdr (project-current)))
         (open-last-modified-scm-file)

         ; in next window, show terminal
         (split-window-right)
         (other-window 1)
         (call-interactively 'magit-fetch-all)
         (projectile-run-vterm)))
    ; otherwise, open dired, pointing at the root directory of the project
    (dired (cdr (project-current)))))
(setq projectile-switch-project-action #'jl/default-project-view)

Install avy

Avy provides an efficient, character / tree-based approach to jumping to a line or matching substring.

(message "load avy")
(use-package avy
  :config
  (global-set-key (kbd "C-l") 'avy-goto-line))

Install Hydra

Collapse a series of keybindings into single keystrokes using Hydra.

Note that the :color key has a special meaning with hydras; red hydra heads do not exit, whereas blue hydra heads exit after executing their action.

In the snippet below, all heads are red by default, but the C-w head is marked as blue. So to exit the hydra, the user can press C-w.

(message "load hydra")
(use-package hydra)

; From https://blog.genenakagaki.com/en/my-emacs-life-is-better-with-hydra.html
(defhydra hydra-window (global-map "C-c w" :color red)
  "
| Navigation^^      | Placement^^         | Create, Delete^^          | Adjustment^^         |
|^^-----------------+^^-------------------+^^-------------------------+^^--------------------|
| _h_: go left      | _H_: move to left   | _v_: split vertically     | _=_: balance windows |
| _j_: go down      | _J_: move to bottom | _s_: split horizontally   | _+_: increase height |
| _k_: go up        | _K_: move to top    | _q_: delete window        | _-_: decrease height |
| _l_: go right     | _L_: move to right  | _Q_: delete other windows | _>_: increase width  |
| _w_: go to next   | ^^                  | ^^                        | _<_: decrease width  |
| _C-w_: go to next | ^^                  | ^^                        | ^^                   |
"
  ("+"   evil-window-increase-height)
  ("-"   evil-window-decrease-height)
  ("<"   evil-window-decrease-width)
  (">"   evil-window-increase-width)
  ("="   balance-windows)
  ("C-w" evil-window-next nil :color blue)
  ("H"   evil-window-move-far-left)
  ("J"   evil-window-move-very-bottom)
  ("K"   evil-window-move-very-top)
  ("L"   evil-window-move-far-right)
  ("h"   evil-window-left)
  ("j"   evil-window-down)
  ("k"   evil-window-up)
  ("l"   evil-window-right)
  ("q"   evil-window-delete)
  ("Q"   delete-other-windows)
  ("s"   evil-window-split)
  ("v"   evil-window-vsplit)
  ("w"   evil-window-next))

Install flycheck

While we won’t enable flycheck globally (via (global-flycheck-mode)), we don’t want the ability to check syntax across various modes. Flycheck - a replacement for Flymake - should do the trick.

The quickstart guide for Flycheck is available here.

(message "load flycheck")
(use-package flycheck)

To enable flycheck in a buffer, call M-x flycheck-mode.

Install company

company-mode offers very helpful auto-completion.

company-mode ignores case by default. The configuration below ensures case is preserved.

(message "load company")
(use-package company
    :config
    (add-hook 'after-init-hook 'global-company-mode)
    (setq company-dabbrev-downcase nil)
    (setq company-dabbrev-ignore-case nil)
    (setq company-keywords-ignore-case nil)
    (setq company-dabbrev-code-ignore-case nil)
    (setq company-etags-ignore-case nil)
    (setq company-idle-delay 0.4))

Install Base16 Theme

Use base16-eighties from the base16-theme package.

(message "load base16-theme")
(use-package base16-theme
    :config (load-theme 'base16-eighties t))

Install undo-tree

Undo Tree provides a convenient tool for mapping out previous undo steps. It also restructures undos / redos as a tree, instead of as a linear series of events.

Move undo data to .emacs.d/backups/undo-tree. These files were confusing org-roam.

(message "load undo-tree")
(use-package undo-tree
  :defer t
  :init
  (global-undo-tree-mode)
  :config
  (progn
    (evil-set-initial-state 'undo-tree-visualizer-mode 'emacs)
    (setq undo-tree-history-directory-alist '(("." . "~/.emacs.d/backups/undo-tree")))))

Install multiple-cursors

Add support for multiple cursors.

An overview video of multiple-cursors is available here.

(message "load multiple-cursors")
(use-package multiple-cursors
  :init
  (global-unset-key (kbd "M-<down-mouse-1>"))
  (global-set-key (kbd "M-<mouse-1>") 'mc/add-cursor-on-click))

Install pomm (for pomodoro and third-time techniques)

See pomm for overview of features, commands, and configuration options.

Note that this requires an up-to-date version of transient.el (provided by Magit).

(use-package pomm
  :straight t
  :commands (pomm pomm-third-time))

Nyan-Mode

An analog indicator of your position in the buffer. With a little help from Nyan Cat.

Use M-x nyan-mode to enable.

(message "load nyan-mode")
(use-package nyan-mode)

Show the weather with wttr.in

Install wttr.el.

Use etiago’s patch to fix an issue where raw html is shown.

(use-package wttrin
  :straight (:type git
                   :host nil
                   :repo "git@github.com:etiago/emacs-wttrin.git"
                   :branch "user-agent-fix")
  :custom
  (wttrin-default-cities '("Portland")))

(advice-add 'wttrin
            :after
            (lambda (&rest args)
              (setq show-trailing-whitespace nil)
              (evil-emacs-state)))

Advanced Developer Configuration

Install vterm

Be sure to follow the installation instructions before using vterm.

Start vterm-mode in Emacs mode; in Normal mode the user is limited to navigating a read-only buffer. Refer to Evil mode for an explanation of Emacs mode versus Vim modes.

Disable highlighting (which is quirky when applied to the terminal).

(unless skip-dev-config
  (message "load vterm")
  (use-package vterm
      :ensure t
      :config
      (evil-set-initial-state 'vterm-mode 'emacs)
      (add-hook 'vterm-mode-hook
              (lambda ()
                  (set (make-local-variable 'global-hl-line-mode) nil)
                  (setq show-trailing-whitespace nil)))))
(with-eval-after-load 'vterm
  (define-key vterm-mode-map (kbd "C-b") 'org-roam-node-find)
  (define-key vterm-mode-map (kbd "C-p") 'projectile-command-map)
  (define-key vterm-mode-map (kbd "C-.") 'delete-window))
(setq vterm-keymap-exceptions (append vterm-keymap-exceptions '("C-w")))

Install Git time machine

Git time machine looks like a very useful way of walking through a file’s version history.

Map git-timemachine-toggle to C-x G. Note that C-x g will still map to jl/magit-status (a wrapper for magit-status).

(unless skip-dev-config
  (message "load git-timemachine")
  (use-package git-timemachine
      :config
      (evil-set-initial-state 'git-timemachine-mode 'emacs)
      (global-set-key (kbd "C-x G") 'git-timemachine-toggle)))

Install markdown-mode

Install markdown-mode.

(unless skip-dev-config
  (message "load markdown-mode")
  (use-package markdown-mode))

Install json-mode

Install json-mode.

(unless skip-dev-config
  (message "load json-mode")
  (use-package json-mode))

Install json-navigator

Install json-navigator.

Note: If Emacs complains about a void variable while trying to load the hierarchy package, it is likely due to a dependency pointing to the old version of hierarchy.

In my case, I noticed that in .emacs.d/straight/repos/melpa/recipes/hierarchy, there was the following definition:

(hierarchy :fetcher github :repo "DamienCassou/hierarchy").

Deleting this file cleared up the errors I was seeing.

The hierarchy package became a part of Emacs core, so dependency definitions like this should eventually be purged or marked as only applying to older versions of Emacs.

More specifically, it seems like this recipe for hierarchy should either be removed or marked as only applying to older versions of Emacs.

(unless skip-dev-config
  (message "load json-navigator")
  (use-package json-navigator
      :requires hierarchy))

Install terraform-mode

Installs terraform-mode

Includes support for outline-minor-mode.

(unless skip-dev-config
  (message "load terraform-mode")
  (use-package terraform-mode
  :straight t
  :custom (terraform-indent-level 2)
  :config
  (defun my-terraform-mode-init ()
      (outline-minor-mode 1))

  (add-hook 'terraform-mode-hook 'my-terraform-mode-init)))

Install VLF

Install VLF, a mode for reading very large files in batch.

To view a large file, use M-x vlf and then enter the file’s path.

(unless skip-dev-config
  (message "load vlf")
  (use-package vlf))

Install typescript-mode

A minimal setup for working with TypeScript. typescript-mode provides highlight modes for TypeScript.

(unless skip-dev-config
  (message "load typescript-mode")
  (use-package typescript-mode))

Install groovy-mode

Installs groovy-mode.

(unless skip-dev-config
  (message "load groovy-mode")
  (use-package groovy-mode))

Install js2-mode

Install js2-mode and enable for *.js files.

More tips on how to configure js2-mode and friends is available here.

(use-package js2-mode)
(add-to-list 'auto-mode-alist '("\\.js\\'" . js2-mode))

Install Steel Bank Common Lisp and slime

What better way to code in Lisp than to use Steel Bank Common Lisp and slime? 😁

There are currently two manual steps required before this section will work:

To install sbcl on Debian 12, run: > sudo apt install sbcl sbcl-doc

To install sbcl on Mac, run: > brew install sbcl

To clone the slime repo, run: > git clone git@github.com:slime/slime.org ~/git/slime

(unless skip-dev-config
  (message "load slime")
  (add-to-list 'load-path "~/git/slime")
  (require 'slime-autoloads)
  (setq inferior-lisp-program "/usr/bin/sbcl"))
  ; TODO: apply correct path to sbcl based on platform
  ;
  ; On macs, with brew installs of sbcl, path should be:
  ; (setq inferior-lisp-program "/opt/homebrew/bin/sbcl"))

One of slime’s dependencies, macrostep, has been archived according to it’s readme (on 2024-07-11).

Based on this commit in the emacsorphanage clone of macrostep, it looks like the repo may have a new maintainer.

In the meantime, I cannot find macrostep in MELPA, and calling M-x slime results in the error:

> Symbol’s value as variable is void: macrostep-mode-map

To work around this, I have a copy of macrostep.el taken from here. Based on my testing, loading this file resolves the macrostep error.

(load-file "~/.emacs.d/hacky-contrib/macrostep.el")

Configure slime to use Quicklisp (and friends)

With that in place, let’s setup slime.

You will need to install quicklisp and the quicklisp slime-helper for this next part to work.

The basic steps for doing so include:

  1. Downloading quicklisp.lisp
  2. Loading this file with: sbcl –load quicklisp.lisp
  3. Installing quicklisp (in the sbcl session) with: (quicklisp-quickstart:install)
  4. Loading the quicklisp setup (TODO: is this necessary? if so, why?) (load “~/quicklisp/setup.lisp”)
  5. Installing quicklisp-slime-helper with: (ql:quickload “quicklisp-slime-helper”)
(slime-setup '(slime-fancy slime-quicklisp slime-asdf slime-mrepl))

; Need to call (ql:quickload "quicklisp-slime-helper")
; then this file will get created for you and you can load it.
(load (expand-file-name "~/quicklisp/slime-helper.el"))

Install counsel, ivy, swiper

(message "load counsel")
(use-package counsel
  :config
  (ivy-mode 1)
  (setq ivy-use-virtual-buffers t)
  (setq ivy-count-format "(%d/%d) "))

Add Keybindings

Ivy-based interface to standard commands

Adopting suggested keybindings from here.

(global-set-key (kbd "C-s") 'swiper-isearch)
(global-set-key (kbd "M-x") 'counsel-M-x)
(global-set-key (kbd "C-x C-f") 'counsel-find-file)
(global-set-key (kbd "M-y") 'counsel-yank-pop)
(global-set-key (kbd "<f1> f") 'counsel-describe-function)
(global-set-key (kbd "<f1> v") 'counsel-describe-variable)
(global-set-key (kbd "<f1> l") 'counsel-find-library)
(global-set-key (kbd "<f2> i") 'counsel-info-lookup-symbol)
(global-set-key (kbd "<f2> u") 'counsel-unicode-char)
(global-set-key (kbd "<f2> j") 'counsel-set-variable)
(global-set-key (kbd "C-x b") 'ivy-switch-buffer)
(global-set-key (kbd "C-c v") 'ivy-push-view)
(global-set-key (kbd "C-c V") 'ivy-pop-view)

Ivy-based interface to standard commands

(global-set-key (kbd "C-c j") 'counsel-git-grep)
(global-set-key (kbd "C-c L") 'counsel-git-log)
(global-set-key (kbd "C-c k") 'counsel-rg)
(global-set-key (kbd "C-c m") 'counsel-linux-app)
;(global-set-key (kbd "C-c f") 'counsel-fzf)
(global-set-key (kbd "C-x l") 'counsel-locate)
(global-set-key (kbd "C-c J") 'counsel-file-jump)
(global-set-key (kbd "C-S-o") 'counsel-rhythmbox)

Ivy-based interface to standard commands

(global-set-key (kbd "C-c C-r") 'ivy-resume)
(global-set-key (kbd "C-c o") 'counsel-outline)
(global-set-key (kbd "C-c t") 'counsel-load-theme)
(global-set-key (kbd "C-c F") 'counsel-org-file)

Install zygospore

Zygospore temporarily hides all but the currently active window.

(message "load zygospore")
(use-package zygospore
  :config
  (global-set-key (kbd "C-x 1") 'zygospore-toggle-delete-other-windows))

Install key-chord

Key Chord Mode lets you execute a command by pressing two keys down at the same time.

The Emacs Wiki has some helpful tips on using this mode.

(message "load key-chord")
(use-package key-chord)
(key-chord-mode 1)

; If this is too long, then there are noticeable typing delays
; If it is too short, then two-key chords are nearly impossible to invoke
; https://github.com/emacsorphanage/key-chord/blob/e724def60fdf6473858f2962ae276cf4413473eb/key-chord.el#L37
(setq key-chord-two-keys-delay 0.025)

(key-chord-define-global "ts" 'save-buffer)
(key-chord-define-global "et" 'evil-avy-goto-char-timer)
(key-chord-define-global "on" 'vterm)
(key-chord-define-global "as" 'zygospore-toggle-delete-other-windows)

Install vimish-fold

vimish-fold lets you fold a region, or lets you fold down to a point specified using avy. It calls out the folded region using the left sidebar (instead of ellipses) which feels a little cleaner.

(message "load vimish-fold")
(use-package vimish-fold
  :config
  (vimish-fold-global-mode 1)
  (global-set-key (kbd "C-c @ a") #'vimish-fold-avy)
  (global-set-key (kbd "C-c @ f") #'vimish-fold)
  (global-set-key (kbd "C-c @ v") #'vimish-fold-delete)
  (global-set-key (kbd "C-c @ U") #'vimish-fold-unfold-all))

Install docker.el

docker.el provides support for managing docker containers, images, volumes, networks, contexts and docker-compose.

Because docker.el doesn’t seem to use modes, used add-hook with the docker-open-hook mode hook to switch into Emacs mode whenever C-c d is pressed. (Normal mode masks most, if not all, docker.el bindings).

(message "load docker")
(use-package docker
:ensure t
:bind ("C-c d" . docker)
:config
(add-hook 'docker-open-hook 'evil-emacs-state)
)

Install dockerfile-mode

Use dockerfile-mode to enable Dockerfile syntax highlighting.

(message "load dockerfile-mode")
(use-package dockerfile-mode)

Install kubernetes-el

See kubernetes-el for more information.

Call kubernetes-overview (or its alias, k8s) to get started.

(message "load kubernetes")
(use-package kubernetes
  :ensure t
  :commands (kubernetes-overview)
  :config
  (setq kubernetes-poll-frequency 3600
        kubernetes-redraw-frequency 3600))

(message "load kubernetes-evil")
(use-package kubernetes-evil
  :ensure t
  :after kubernetes)

(fset 'k8s 'kubernetes-overview)
(evil-set-initial-state 'kubernetes-mode 'emacs)
(evil-set-initial-state 'kubernetes-logs 'emacs)
(evil-set-initial-state 'kubernetes-log-line 'emacs)

Install keepass-mode

keepass-mode lets you interact with your KeePassXC Database.

(message "load keepass-mode")
(use-package keepass-mode
  :config
  (evil-set-initial-state 'keepass-mode 'emacs))

Miscellaneous Functions

Insert Time

Insert timestamp using C-c p.

(defun insert-current-date ()
  "Insert the current date in YYYY-MM-DD format."
  (interactive)
  (insert (format-time-string "%Y-%m-%d")))

(global-set-key (kbd "C-c p") 'insert-current-date)

Open org-roam node for (ISO) week of the year

(defun jl/first-day-of-year ()
  "Get first day of year as list (D M Y)"
  (let ((today (calendar-current-date)))
    (setq first-day-of-year (copy-sequence today))
    (setf (nth 0 first-day-of-year) 1)
    (setf (nth 1 first-day-of-year) 1)
    first-day-of-year))

(defun jl/days-since-start-of-year ()
  "Get number of days since the start of the year"
  (let ((today (calendar-current-date))
        (first-day-of-year (jl/first-day-of-year)))
   (setq today-abs (calendar-absolute-from-gregorian today))
   (setq first-day-of-year-abs
         (calendar-absolute-from-gregorian first-day-of-year))
   (setq days-since-start-of-year
         (1+ (- today-abs first-day-of-year-abs)))))

(defun jl/day-of-week-iso (date)
  "Given a date in list form (D M Y) returns the day of the week by number, with Monday being 0"
  (% (+ (1- (calendar-day-of-week date)) 8) 7))

(defun jl/days-since-first-monday-of-first-week ()
  "Get number of days since the start of the year"
  (+ (jl/days-since-start-of-year)
     (jl/day-of-week-iso (jl/first-day-of-year))))

(defun jl/week-number ()
  "Get current week number"
  (1+ (/ (jl/days-since-first-monday-of-first-week) 7)))

(defun jl/org-roam-goto-week-by-number ()
    "Visit org-roam node with titile 'Week #'"
  (interactive)
  (org-roam-node-find t (format "Week %s" (jl/week-number))))

Clear all buffers except scratch, all other windows/frames, too

Function fresh-start helps clear any buffers, windows, or frames that have been opened during the current session. The function does preserve the *scratch* buffer, however.

; Code generated with the assistance of ChatGPT, version 3.5, developed by OpenAI
; More information: https://www.openai.com/chatgpt
; Generated on: October 4, 2023

; Jim Ladd updated snippet to use ~delete-other-frames~
; and to move ~delete-other-windows~ outside of ~let~.

(defun fresh-start ()
  "Kill all buffers except for *scratch*, close all other windows, and delete all other frames."
  (interactive)
  ;; Close all other frames
  (delete-other-frames)
  (delete-other-windows)
  (let ((buffer-list (buffer-list)))
    ;; Close all other windows
    (dolist (buffer buffer-list)
      (unless (string-equal (buffer-name buffer) "*scratch*")
        (kill-buffer buffer)))
    (message "Fresh start: All buffers except *scratch*, other windows, and frames have been closed.")))

Edit yaml files with long values (e.g. .kube/config)

(defun jl/edit-yaml-with-long-values ()
  "Edit yaml files with long values"
  (interactive)
  ; hacky test to see if yaml-mode is on
  ; (couldn't find anything more obvious to key off of)
  (unless (equal font-lock-defaults '(yaml-font-lock-keywords))
    (yaml-mode))

  ; truncate long lines
  (toggle-truncate-lines 1))

Fill region Shortcut

(eval-after-load 'org
  '(progn
     (unbind-key "C-c C-x f" org-mode-map)
     (global-set-key (kbd "C-c C-x f") #'fill-region)))

Unfill region

Sometimes it can be helpful to do the opposite of fill-region.

(defun jl/unfill-paragraph (beg end)
  "Unfill the paragraph, joining text into a single logical line"

  (interactive "r")
  (let ((fill-column (point-max)))
    (fill-region beg end)))
(global-set-key (kbd "C-c C-x F") #'jl/unfill-paragraph)

(search-forward-regexp “\n:space:*\n” nil t)

This is such a long line. This is such a long line. This is such a long line. This is such a long line. This is such a long line. This is such a long line. This is such a long line. This is such a long line. This is such a long line. This is such a long line. This is such a long line. This is such a long line. This is such a long line. This is such a long line. This is such a long line. This is such a long line. This is such a long line. This is such a long line. This is such a long line. This is such a long line. This is such a long line.

Automatically Switch to Opened Window

Automatically Switch to Help Window

Requesting this behavior is as easy as setting help-window-select. Perfect.

(setq help-window-select t)

Automatically Switch to Opened Window After Splitting Windows

When emacs splits the current window (horizontally or vertically), point remains in the current window. I almost always want to hop over to the new window.

Unfortunately, to make this change we can’t set a global variable or pass in an argument. Instead, we replace the default function with a lambda that calls the original function and then calls other-window.

For more thougts on this tweak – including reasons why advice-add should not be used – check out this Stackoverflow question.

(global-set-key "\C-x2" (lambda () (interactive)(split-window-below) (other-window 1)))
(global-set-key "\C-x3" (lambda () (interactive)(split-window-right) (other-window 1)))

Adjust Font Size for Frame

Add convenience functions (and keybindings) for adjusting font size of entire frame while preserving frame’s dimensions.

Use C-x C-= to increase font size. Use C-x C-- to decrease font size.

Use C-u <number> before using either of the previous chords to set how much to increment / decrement font size.

Alternatively, C-x z (repeat) can be used to repeat the previous command. Hitting z after the initial call to C-x z can be used as a shortcut for quickly repeating the previous command.

                                        ; Resize the whole frame, and not only a window
;; Adapted from:
;; https://stackoverflow.com/questions/24705984/increase-decrease-font-size-in-an-emacs-frame-not-just-buffer
(defun jl/zoom-frame (&optional amt frame)
  "Increaze FRAME font size by amount AMT. Defaults to selected
frame if FRAME is nil, and to 1 if AMT is nil."
  (interactive "p")
  (let* ((frame (or frame (selected-frame)))
         (font (face-attribute 'default :font frame))
         (size (font-get font :size))
         (size (if (eq size 0) 12 size))  ; hack to avoid case where font-get returns size of 0 on macs
         (amt (or amt 1))
         (new-size (+ size amt)))
    (set-frame-font (font-spec :size new-size) t `(,frame))
    (message "Frame's font new size: %d" new-size)))

(defun jl/zoom-frame-out (&optional amt frame)
  "Call `jl/zoom-frame' with negative argument."
  (interactive "p")
  (jl/zoom-frame (- (or amt 1)) frame))

(global-set-key (kbd "C-x C-=") 'jl/zoom-frame)
(global-set-key (kbd "C-x C--") 'jl/zoom-frame-out)

Configure mode line

The default mode line is long and gets cut off when the frame is split. Update the default mode line to be shorter.

Information on mode line variables can be found here.

(setq-default mode-line-format
  '("%e"
    evil-mode-line-tag
    mode-line-mule-info
    mode-line-modified
    " "
    mode-line-buffer-identification
    " "
    mode-line-position
    mode-line-misc-info
    (vc-mode vc-mode)
    " "
    mode-line-end-spaces))

Set exec-path from shell path

This is especially helpful when running Emacs on Mac OSX, where Brew apps are otherwise not visible to Emacs.

When running ZSH on Mac, make sure that export PATH=... statements are placed in .zprofile; .zshrc is not sourced by the function below!

(defun set-exec-path-from-shell-PATH ()
  "Set up Emacs' `exec-path' and PATH environment variable to match
that used by the user's shell.

This is particularly useful under Mac OS X and macOS, where GUI
apps are not started from a shell."
  (interactive)
  (let ((path-from-shell (replace-regexp-in-string
                          "[ \t\n]*$" "" (shell-command-to-string
                                          "$SHELL --login -c 'echo $PATH'"
                                         ))))
    (setenv "PATH" path-from-shell)
    (setq exec-path (split-string path-from-shell path-separator))))

(set-exec-path-from-shell-PATH)

Disable auto-save files

In practice, not sure that I have ever used auto-save files to recover any data. And in the meantime, they can form cruft that trips up other applications working with the file tree I’m working with.

Note that this does not affect backup files; these are not created in directories managed by a version control system.

See this page for a comparison of backup and auto-save files.

(setq auto-save-default nil)

Store backup files in central location

When looking at directories outside of Emacs (e.g. using a regular shell), directory contents can quickly become congested by Emacs backup files. Move them to /.emacs.d/backups/emacs instead.

(setq backup-directory-alist `(("." . "~/.emacs.d/backups/emacs")))

Start server

After Emacs starts, start the Emacs server so that we can quickly open new sessions with emacsclient.

(defun sole-emacs-process ()
  "Determine if this is the only emacs process that is running."
  (let ((output-buffer (generate-new-buffer "*emacs-process-count*")))
    ; TODO: Mac version cannot use any "--" flags
    ; Instead it should use something like pgrep Emacs
    ; and then count the number of lines returned
    ; Should probably take a similar approach for linux
    ; if it's possible to take the same approach on both OSes
    ;(call-process "pgrep" nil output-buffer nil "--exact" "--count" "emacs")

    (call-process "pgrep" nil output-buffer nil "--exact" "--count" "emacs")
    (save-excursion
      (with-current-buffer "*emacs-process-count*"
        (end-of-buffer)
        (join-line)
        (let ((buffer-contents (buffer-substring-no-properties (point-min) (point-max))))
          (kill-buffer output-buffer)
          (string= buffer-contents "1"))))))

    (if (sole-emacs-process)
      (server-start))

Previous attempts to start an emacs server from the commandline using emacs --daemon have been unsuccessful up to this point; the command loads init files very differently for some reason. It begins with the site-wide init files (under /etc), and when it finishes with that and tries loading init files in HOME/emacs.d it does not start with init.el.

Create convenience script for starting emacsclient

Create a script, ec, that will call emacsclient and note that the command should create a new frame.

(let* ((bin_dir "~/bin")
       (ec_script (concat bin_dir "/ec")))
    (make-directory bin_dir t)
    (if (not (file-exists-p ec_script))
        (progn
            (find-file "~/bin/ec")
            (insert "#!/bin/bash\nemacsclient -c")
            (save-buffer)
            (kill-buffer)
            (set-file-modes ec_script #o755))))

Finish profiling

;(profiler-stop)
;(profiler-report)