dotfiles/emacs-config.org

68 KiB
Raw Blame History

First things first

Literate Config Setup

To get started, run C-c C-v t

  (defun dotfiles/org-babel-tangle-config ()
    (when (string-equal (buffer-file-name)
                        (expand-file-name "~/.dotfiles/emacs-config.org"))
      (let ((org-confirm-babel-evaluate nil))
        (org-babel-tangle)
        (load-file (expand-file-name "~/.config/emacs/init.el")))))

  (add-hook 'org-mode-hook
            (lambda ()
              (add-hook 'after-save-hook
                        #'dotfiles/org-babel-tangle-config
                        nil
                        t)))
org-appear-mode org-tempo-setup yas-minor-mode #[nil (evil-org-mode) nil] #[0 \300\301\302\303\304$\207 [add-hook change-major-mode-hook org-fold-show-all append local] 5] #[0 \300\301\302\303\304$\207 [add-hook change-major-mode-hook org-babel-show-result-all append local] 5] org-babel-result-hide-spec org-babel-hide-all-hashes #[0 \301\211\207 [imenu-create-index-function org-imenu-get-tree] 2] #[nil ((display-line-numbers-mode 0)) nil] #[nil ((add-hook 'after-save-hook #'dotfiles/org-babel-tangle-config nil t)) nil]

Package initialization

TODO: savehist mode doesn't seem to save the history correctly

  (require 'package)

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

  ;; Initialize use-package on non-linux platforms
  (unless (package-installed-p 'use-package)
    (package-install 'use-package))

  (require 'use-package)
  (setq use-package-always-ensure t)

  (package-refresh-contents)

  (use-package savehist
    :init
    (savehist-mode))

Paths

  ;; (add-to-list 'exec-path "/opt/homebrew/bin")
  ;; (setenv "PATH" (concat "/opt/homebrew/bin:" (getenv "PATH")))

Straight

I don't use straight for much currently, but needed it in order to install beancount-mode and intend to migrate all my packages to it eventually.

  (defvar bootstrap-version)
  (let ((bootstrap-file
         (expand-file-name
          "straight/repos/straight.el/bootstrap.el"
          (or (bound-and-true-p straight-base-dir)
              user-emacs-directory)))
        (bootstrap-version 7))
    (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))

  (setq package-enable-at-startup nil)

Password Retrieval

password-store integrations require you to have pass and pinentry configured on your local system outside of emacs.

  (use-package pinentry)
  (use-package pass)
  (use-package password-store)
  (pinentry-start)
  (auth-source-pass-enable)

Variables

Things referenced throughout the config which are specific to me

  (setq user-init-file (expand-file-name "~/.config/emacs/init.el"))

  (setq IS-MACOS (eq system-type 'darwin))

  (setq openai-key-string
	(if IS-MACOS "self/openai-alt" "openai-alt"))

  (setq gptel-api-key (password-store-get openai-key-string))

Required directories for emacs setup

Eventually I'll set this up so that all these directories are set in the config of their respective packages; I'll still run ensure-directory on all of them beforehand though.

  (defun ian/ensure-directory (dir)
    "Ceate a dir if it doesn't exist."
    (unless (file-directory-p dir)
      (make-directory dir t)))

  (ian/ensure-directory "~/.config/emacs/backups/")
  (ian/ensure-directory "~/.config/emacs/autosaves")
  (ian/ensure-directory "~/notes/roam")
  (ian/ensure-directory "~/notes/deft")
  (ian/ensure-directory "~/notes/code")
  (ian/ensure-directory "~/notes/work")
  (ian/ensure-directory "~/.config/emacs/video")
  (ian/ensure-directory "~/.config/emacs/audio")

  (setq projectile-project-search-path '("~/code/")
        empv-video-dir "~/.config/emacs/video"
        empv-audio-dir "~/.config/emacs/audio"
        backup-directory-alist `(("." . "~/.config/emacs/backups/"))
        auto-save-file-name-transforms `((".*" "~/.config/emacs/autosaves/" t)))

  ;; (projectile-mode)

Helper Functions

  (defun ian/eval-region-or-buffer ()
    "Eval the region if anything is selected, otherwise eval the whole buffer."
    (interactive)
    (if mark-active
        (eval-region (region-beginning) (region-end))
      (eval-buffer)))

  (defun ian/conditional-imenu ()
    "Call `consult-org-heading` in Org mode, otherwise `consult-imenu`."
    (interactive)
    (if (eq major-mode 'org-mode)
        (consult-org-heading)
      (consult-imenu)))

  (defun ian/reload-config ()
    "Reload the Emacs configuration."
    (interactive)
    (load-file user-init-file))

  (defun ian/open-from-project-root (filename)
  "Open a file from the root of the current Projectile project."
  (interactive)
  (find-file (expand-file-name filename (projectile-project-root))))

Evil-want-keybinding

Setting this in the :init of evil doesn't work for some reason. It has to be early in the file TODO: find a better solution

  (setq evil-want-keybinding nil)

Basic Appearance and Behaviors

Font and Theme

  (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
    ;; Enable flashing mode-line on errors
    (setq doom-themes-treemacs-theme "doom-atom") ; use "doom-colors" for less minimal icon theme
    (doom-themes-treemacs-config)
    ;; Corrects (and improves) org-mode's native fontification.
    (doom-themes-org-config))

  ;; (set-frame-font "DejaVu Sans Mono 16")
  ;; (add-to-list 'default-frame-alist
  ;;              '(font . "Fira Code Mono 16"))

  (set-face-attribute 'default nil
                    :family "FiraCode Nerd Font Mono"
                    :height 160)

  ;; (load-theme 'doom-oceanic-next t)

  (use-package base16-theme)
  (load-theme 'base16-oceanicnext t)

  (use-package default-text-scale
    :defer 1
    :config
    (default-text-scale-mode))

Cosmetic

  (setq fancy-splash-image "~/.dotfiles/sink.png")  ;; the best emacs logo
  (setq lsp-lens-enable nil)  ;; gets rid of the '0 references' note from lsp

  ;; remove visual noise
  (menu-bar-mode -1)
  (tool-bar-mode -1)
  (scroll-bar-mode -1)
  (recentf-mode 1)
  (savehist-mode 1)  ; remember previous commands in minibuffer
  (save-place-mode 1)  ; put cursor in last place when opening a file

  (global-display-line-numbers-mode t)

  (use-package rainbow-delimiters
    :hook (prog-mode . rainbow-delimiters-mode))

  (use-package solaire-mode
    :config (solaire-global-mode +1))

  (use-package nerd-icons
    :ensure t
    :config
    (unless (find-font (font-spec :name "Symbols Nerd Font"))
      (nerd-icons-install-fonts t)))

  (use-package doom-modeline
    :hook (after-init . doom-modeline-mode)
    :config
    (setq doom-modeline-minor-modes t
          doom-modeline-height 24
          nerd-icons-font-family "Symbols Nerd Font Mono")
    (set-face-attribute 'mode-line nil :height 0.9)
    (set-face-attribute 'mode-line-inactive nil :height 0.9))

  (use-package minions
    :config (minions-mode 1))

  ;; turn off number mode in some contexts
  (dolist (mode
           '(org-mode-hook
             term-mode-hook
             eshell-mode-hook
             treemacs-mode-hook
             vterm-mode-hook
             nov-mode-hook))
    (add-hook mode (lambda () (display-line-numbers-mode 0))))
  (column-number-mode)

Buffers

TODO: Make this use currently selected buffer

The base display action tries to reuse an existing window showing the same mode or buffer before opening a new one, falling back to the same window. This is the default for buffers not covered by display-buffer-alist in the Popper section. The goal is to avoid gratuitous window splits for ordinary buffer switching — splits should be intentional.

  ;; Generate buffers in last window
  (setq display-buffer-base-action
	'(display-buffer-reuse-mode-window
	  display-buffer-reuse-window
	  display-buffer-same-window))

Behavioral Tweaks

  (setq ring-bell-function 'ignore)

  (when IS-MACOS
    (setq mac-pass-command-to-system nil)) 

  ;; Avoid customization UI auto-variables in init.el
  (setq custom-file (locate-user-emacs-file "custom-vars.el"))
  (load custom-file 'noerror 'nomessage)

  ;; Don't show popup UI when prompting
  (setq use-dialog-box nil)

  ;; Update files when changed on disk
  (global-auto-revert-mode 1)

  (use-package helpful
    :ensure t
    :bind
    ([remap describe-function] . helpful-callable)
    ([remap describe-variable] . helpful-variable)
    ([remap describe-key]      . helpful-key)
    ([remap describe-command]  . helpful-command))

  ;; If a popup does happen, don't resize windows to be equal-sized
  (setq even-window-sizes nil)
  (setq whitespace-style '(trailing tabs newline tab-mark )) ; show tab characters
  (setq tab-always-indent 'complete)
  (setq tab-first-completion 'word)
  (setq indent-tabs-mode nil)  ; tabs not spaces

  (visual-line-mode)  ; wrap long lines visually
  (whitespace-mode)
  (setq-default indent-tabs-mode nil)

Tab bar

  (use-package tab-bar-echo-area
    :ensure
    :config
    (tab-bar-echo-area-mode 1)
    (setq tab-bar-show nil))

Evil

  (use-package evil
    :ensure t
    :config
    (global-set-key (kbd "<escape>") 'keyboard-escape-quit)
    (define-key evil-normal-state-map (kbd "j") 'evil-next-visual-line)
    (define-key evil-normal-state-map (kbd "k") 'evil-previous-visual-line)
    (define-key evil-normal-state-map (kbd "ghp") 'git-gutter-previous-hunk)
    (define-key evil-normal-state-map (kbd "ghn") 'git-gutter-next-hunk)
    (define-key evil-normal-state-map (kbd "ghr") 'git-gutter-revert-hunk)
    (define-key evil-normal-state-map (kbd "ghs") 'git-gutter-popup-hunk)
    (define-key evil-normal-state-map (kbd "ghS") 'git-gutter-stage-hunk)
    (define-key evil-normal-state-map (kbd "gp") nil) 
    (define-key evil-normal-state-map (kbd "gpn") 'popper-cycle)
    (define-key evil-normal-state-map (kbd "gpp") 'popper-cycle-backwards)
    (define-key evil-normal-state-map (kbd "gpq") 'popper-kill-latest-popup)
    (define-key evil-normal-state-map (kbd "gpt") 'popper-toggle-type)
    (define-key evil-normal-state-map (kbd "gbn") 'projectile-next-project-buffer)
    (define-key evil-normal-state-map (kbd "gbp") 'projectile-previous-project-buffer)
    (evil-mode 1))

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


  (use-package evil-org
    :ensure t
    :after org
    :hook (org-mode . (lambda () evil-org-mode)))


  (use-package evil-collection
    :after evil
    :ensure t
    :diminish evil-collection-unimpaired-mode
    :init
    :config
    (setq evil-respect-visual-line-mode t)
    (setq evil-collection-repl-submit-state 'insert)
    (evil-collection-init))


  (use-package evil-commentary
    :config
    (evil-commentary-mode))

Navigation

Helper Functions

  (defun split-and-follow-horizontally ()
    (interactive)
    (split-window-below)
    (balance-windows)
    (other-window 1))

  (defun split-and-follow-vertically ()
    (interactive)
    (split-window-right)
    (balance-windows)
    (other-window 1))

Windows and Workspaces

Credit for manual dispatch function.

General window management uses ace-window for jumping and splitting, with winner-mode for undo/redo of window layouts. The philosophy here is to keep manual window control lightweight — ace-window covers most navigation needs with single keypresses, and winner catches mistakes. Workspaces are handled at the tab level rather than with a dedicated workspace package.

  • SPC w jumps to a window by label
  • SPC W opens the full ace-window dispatch menu for splits, swaps, etc.
  • C-c <left> / C-c <right> undo/redo window layouts via winner

TODO: Window width controls

  (use-package winner
    :after evil
    :config
    (winner-mode))

  (use-package ace-window)
  (setq aw-keys '(?a ?s ?d ?f ?g ?h ?j ?k ?l))
  (setq aw-dispatch-alist
	'(
	  (?x aw-delete-window "Delete Window")
	  (?m aw-swap-window "Swap Windows")
	  (?M aw-move-window "Move Window")
	  ;; (?c aw-copy-window "Copy Window")
	  ;; (?j aw-switch-buffer-in-window "Select Buffer")
	  ;; (?n aw-flip-window)
	  ;; ;;
	  (?b aw-switch-buffer-other-window "Switch Buffer Other Window")
	  (?u winner-undo)
	  (?U winner-redo)
	  (?c aw-split-window-fair "Split Fair Window")
	  (?v aw-split-window-vert "Split Vert Window")
	  (?H aw-split-window-horz "Split Horz Window")
	  (?o delete-other-windows "Delete Other Windows")
	  (?? aw-show-dispatch-help)))
  (setq aw-scope 'frame)

  (defun ace-window-manual-dispatch (arg)
    "Calls `ace-window' with ARG asking for the dispatch, even if `aw-dispatch-always' is nil.
    This could be useful to use the advanced commands"
    (interactive "p")
    (let ((aw-dispatch-always t))
      (ace-window arg)
      ))

StumpWM-aware window navigation

s-h/j/k/l (mod+hjkl) move between Emacs windows using windmove. When there's no further Emacs window in that direction (i.e. we're at the edge of Emacs's frame), the command instead shells out to StumpWM via stumpish to move focus to the next tiled frame in that direction — so navigation flows seamlessly out of Emacs and into the rest of the WM using the same keys StumpWM itself binds to s-h/j/k/l.

  (defvar stumpwm-stumpish-path
    (expand-file-name "~/.stumpwm.d/modules/util/stumpish/stumpish")
    "Path to the stumpish binary used to talk to a running StumpWM.")

  (defun stumpwm-move-focus (direction)
    "Ask StumpWM to move focus to the frame in DIRECTION (\"left\" \"right\" \"up\" \"down\").
  No-op on macOS (or anywhere StumpWM isn't running/available)."
    (when (and (not IS-MACOS)
               (file-executable-p stumpwm-stumpish-path))
      (call-process stumpwm-stumpish-path nil nil nil
                    (concat "move-focus " direction))))

  (defun stumpwm-windmove-or-escape (windmove-fn direction)
    "Call WINDMOVE-FN; if it errors (no Emacs window in DIRECTION),
  fall through to StumpWM and move focus to the next tiled frame there."
    (condition-case nil
        (funcall windmove-fn)
      (error (stumpwm-move-focus direction))))

  (defun stumpwm-windmove-left ()  (interactive) (stumpwm-windmove-or-escape #'windmove-left  "left"))
  (defun stumpwm-windmove-right () (interactive) (stumpwm-windmove-or-escape #'windmove-right "right"))
  (defun stumpwm-windmove-up ()    (interactive) (stumpwm-windmove-or-escape #'windmove-up    "up"))
  (defun stumpwm-windmove-down ()  (interactive) (stumpwm-windmove-or-escape #'windmove-down  "down"))

  (global-set-key (kbd "s-h") #'stumpwm-windmove-left)
  (global-set-key (kbd "s-j") #'stumpwm-windmove-down)
  (global-set-key (kbd "s-k") #'stumpwm-windmove-up)
  (global-set-key (kbd "s-l") #'stumpwm-windmove-right)

Popper

Transient/popup buffers — terminals, help, compilation, messages — are managed by popper, which tracks them as a group and lets you cycle or dismiss them together. Popper's display control is enabled, meaning it defers to display-buffer-alist (below) for the actual placement logic rather than using its own defaults. Buffers are grouped by projectile project so popup state is workspace-local.

  • Popper decides what is a popup
  • display-buffer-alist decides where it goes
  • Terminals always go to the bottom
  • Help and compilation go to the bottom unless the window is too narrow (<65 columns), in which case they replace the current buffer instead
  • Messages appear at the bottom without stealing focus
  (use-package popper
    :after projectile
    :init
    (setq popper-reference-buffers
          '(("\\*Messages\\*"       . hide)
            "\\*Error\\*"
            "Output\\*$"
            ("\\*Warnings\\*"       . hide)
            "\\*scratch\\*"
            "\\*Async Shell Command\\*"
            "^\\*eshell.*\\*$"      eshell-mode
            "^\\*shell.*\\*$"       shell-mode
            "^\\*term.*\\*$"        term-mode
            "^\\*vterm.*\\*$"       vterm-mode
            helpful-mode
            help-mode
            compilation-mode
            inferior-python-mode
            consult-flymake-mode))
    :config
    (setq popper-display-control t)
    (setq popper-group-function #'popper-group-by-projectile)
    (popper-mode +1)
    (popper-echo-mode +1))

  ;; display-buffer-alist rules (replaces shackle)
  (setq display-buffer-alist
        `(;; Help buffers: bottom split, fall back to same window if narrow
          ((or (derived-mode . help-mode)
               (derived-mode . helpful-mode))
           (display-buffer-reuse-window
            ,(lambda (buf alist)
               (when (< (window-width) 65)
                 (display-buffer-same-window buf alist)))
            display-buffer-in-side-window)
           (side . bottom)
           (slot . 0)
           (window-height . 0.35)
           (select . t))

          ;; Compilation: bottom split
          ((derived-mode . compilation-mode)
           (display-buffer-in-side-window)
           (side . bottom)
           (slot . 1)
           (window-height . 0.35)
           (select . t))

          ;; Messages: bottom, no focus steal
          ("\\*Messages\\*"
           (display-buffer-in-side-window)
           (side . bottom)
           (slot . 2)
           (window-height . 0.25)
           (select . nil))

          ;; Terminals: always bottom
          ((or "^\\*eshell.*\\*$"
               "^\\*shell.*\\*$"
               "^\\*term.*\\*$"
               "^\\*vterm.*\\*$")
           (display-buffer-in-side-window)
           (side . bottom)
           (slot . 3)
           (window-height . 0.35)
           (select . t))))

Dired

  (setq dired-listing-switches "-alh --group-directories-first")

  ;; opening 'other window' on a subdir enters 2 window dired mode for copy/paste
  (setq dired-dwim-target t)

  ;; refresh dired buffers automatically after file operations
  (setq dired-auto-revert-buffer t)
  (add-hook 'dired-mode-hook 'dired-hide-details-mode)

  (use-package dired-open
    :config
    (setq dired-open-extensions '(("png" . "nsxiv")
                                  ("mkv" . "mpv")
                                  ("avi" . "mpv"))))

  (use-package nerd-icons-dired
    :hook (dired-mode . nerd-icons-dired-mode))

  ;; Reuse dired buffer when navigating directories
  (put 'dired-find-alternate-file 'disabled nil)
  (with-eval-after-load 'dired
    (evil-collection-define-key 'normal 'dired-mode-map
      (kbd "RET") 'dired-find-alternate-file
      (kbd "^")   'dired-up-directory))

Treemacs

  (use-package treemacs
    :config
    (setq treemacs-width  45))

  ;; Follow project buffers instantly
  (setq treemacs--project-follow-delay 0)

  (defun ian/treemacs-open-current-or-jump ()
    "Find the current file and open treemacs if it's not open, otherwise jump
      to the treemacs window"
    (interactive)
    (if
        (not(eq (treemacs-current-visibility) 'visible))
        (treemacs-find-file))
    (treemacs-select-window))

  (use-package all-the-icons)
  (use-package treemacs-all-the-icons)
  (use-package treemacs-evil)

Which-key

  (use-package which-key
    :ensure nil
    :init (which-key-mode)
    :diminish which-key-mode
    :config
    (setq which-key-idle-delay 0.3
          which-key-add-column-padding 1
          which-key-max-description-length 32
          which-key-min-column-description-width 20))

General

I know these could be bound on a per-package basis, but since this is my primary interface with emacs I'd rather keep all keymaps in one place, both for reference and to discourage myself from organizing my top-level bindings on a per-plugin basis rather than keeping the focus on functionality. TODO:

  • bind lispy for colemak-dh
  • bind lispy doc features and ide-like features
  • bind embark
  (use-package general
    :ensure t
    :after nov evil evil-collection treemacs
    :config

    (general-create-definer general-definition
      ;; :keymaps '(normal insert visual emacs nov-mode-map nov-button-map dired-mode-map)
      :states '(normal visual symex)
      :keymaps 'override
      :prefix "SPC"
      :global-prefix "C-SPC")

    (general-definition

      ;; Top level bindings
      "SPC" '(projectile-find-file                      :which-key "Find project file")
      "<backspace>" '(ian/treemacs-open-current-or-jump :which-key "Find current file in sidebar")
      "TAB" '(popper-toggle                             :which-key "Toggle popup")
      "," '(projectile-ibuffer                          :which-key "Buffers (workspace)")
      "/" '(consult-ripgrep                             :which-key "Search project")
      ":" '(execute-extended-command                    :which-key "M-x")
      ";" '(eval-expression                             :which-key "Eval")
      "<" '(consult-buffer                              :which-key "Buffers (all)")
      "-" '(dired-jump                                    :which-key "Find Directory")
      "d" '(docker                                      :which-key "Docker")
      "i" '(ian/conditional-imenu                       :which-key "Imenu")
      "w" '(ace-window                                  :which-key "Window")  
      "W" '(ace-window-manual-dispatch                  :which-key "Window (dispatch)")  

      ;; Agenda
      "a" '(:ignore t                     :which-key "Agenda")
      "aa" '(org-agenda                   :which-key "Open Agenda")
      "a/" '(consult-org-agenda           :which-key "Search agenda headlines")

      "ac" '(:ignore t                    :which-key "Clock")
      "acc" '(org-clock-goto              :which-key "Active clock")
      "ack" '(org-clock-cancel            :which-key "Cancel current clock")
      "act" '(+org/toggle-last-clock      :which-key "Toggle last clock")


      ;; Buffers / bookmarks
      "b" '(:ignore t                 :which-key "Buffers")
      "bb" '(consult-bookmark         :which-key "Goto Bookmark")  ;; consult-bookmark
      "bk" '(kill-current-buffer      :which-key "Kill buffer")
      "bm" '(bookmark-set             :which-key "Set bookmark")
      "bM" '(bookmark-delete          :which-key "Delete bookmark")
      ;; "bo" '(NEEDS CUSTOM FUNCTION :which-key "Kill other buffers")
      "br" '(revert-buffer            :which-key "Revert buffer")
      "bR" '(rename-buffer            :which-key "Rename buffer")
      "bz" '(bury-buffer              :which-key "Bury buffer")
      ;; "bZ" '(NEEDS CUSTOM FUNCTION :which-key "Kill buried buffers")
      "bj" '(+ivy/jump-list           :which-key "Jump list")
      "bu" '(vundo                    :which-key "Undo history")

      ;; Code
      "c" '(:ignore t                         :which-key "Code")
      "cc" '(compile                          :which-key "Compile")
      "cC" '(recompile                        :which-key "Recompile")
      "cd" '(+lookup/definition               :which-key "Goto definition")
      "cr" '(+lookup/references               :which-key "Goto references")
      "ce" '(ian/eval-region-or-buffer        :which-key "Eval buffer/region")
      "cE" '(consult-flymake                  :which-key "Errors")
      ;; "cf" '(NOT A COMMAND (re-do)         :which-key "Format buffer/region")
      "ci" '(+lookup/implementations          :which-key "Find implementations")
      "cj" '(lsp-ivy-workspace-symbol         :which-key "Goto symbol (curr. workspace)")
      "cJ" '(lsp-ivy-global-workspace-symbol  :which-key "Goto symbol (any workspace)")
      "ck" '(+lookup/documentation            :which-key "Jump to documentation")
      "cl" '(+default/lsp-command-map         :which-key "LSP")
      "co" '(lsp-organize-imports             :which-key "LSP organize imports")
      "cp" '(completion-at-point              :which-key "Completion at point")
      "cR" '(lsp-rename                       :which-key "LSP rename")
      "cs" '(+eval/send-region-to-repl        :which-key "Send to repl")
      "ct" '(+lookup/type-definition          :which-key "Find type definition")
      "cw" '(delete-trailing-whitespace       :which-key "Delete trailing whitespace")
      ;; "cn" '(doom/delete-trailing-newlines :which-key "Delete trailing newlines")
      "cx" '(consult-lsp-diagnostics          :which-key "List errors")

      ;; File
      "f" '(:ignore t                                      :which-key "File")
      ;; "fp" '(doom/open-private-config                   :which-key "Browse private config")
      "fr" '(projectile-recentf                            :which-key "Recent files")
      "fR" '(consult-recent-file                           :which-key "Recent files")
      "fc" '(ian/reload-config                             :which-key "Reload config")
      ;; "fm" '(doom/move-this-file                        :which-key "Move/rename file")
      ;; "fS" '(doom/sudo-find-file                        :which-key "Sudo find file")
      ;; "fs" '(doom/sudo-this-file                        :which-key "Sudo this file")
      "fy" '(+default/yank-buffer-path                     :which-key "yank file path")
      "fY" '(+default/yank-buffer-path-relative-to-project :which-key "Yank file path from project")

      ;; Git
      "g"   '(:ignore t :which-key "git")
      "gb"  '(magit-branch-checkout         :which-key "magit switch branch")
      "gB"  '(magit-blame-addition          :which-key "magit blame")
      "gF"  '(magit-fetch                   :which-key "magit fetch")
      "gg"  '(magit-status                  :which-key "magit status")
      "gG"  '(magit-status-here             :which-key "magit status here")
      "gL"  '(magit-log-buffer-file         :which-key "magit buffer log")
      "gt"  '(git-timemachine-toggle        :which-key "git time machine")
      "g/"  '(consult-git-log-grep          :which-key "Search git log")
      "gd"   '(:ignore t :which-key "dispatch")
      "gdF" '(forge-dispatch                :which-key "Forge dispatch")
      "gdd" '(magit-dispatch                :which-key "Magit dispatch")
      "gdf" '(magit-file-dispatch           :which-key "Magit file dispatch")

      "gf"   '(:ignore t :which-key "find")
      "gfc" '(magit-show-commit             :which-key "find commit")
      "gff" '(magit-find-file               :which-key "find file")
      "gfg" '(magit-find-git-config-file    :which-key "find gitconfig file")
      "gfi" '(forge-visit-issue             :which-key "find issue")
      "gfp" '(forge-visit-pullreq           :which-key "find pull request")

      "gh"   '(:ignore t :which-key "hunk")

      "ghn" '(git-gutter-previous-hunk     :which-key "jump to previous hunk")
      "ghp" '(git-gutter-next-hunk         :which-key "jump to next hunk")
      "ghr" '(git-gutter-revert-hunk       :which-key "revert hunk at point")
      "ghs" '(git-gutter-popup-hunk        :which-key "show hunk")
      "ghS" '(git-gutter-stage-hunk        :which-key "stage hunk at point")

      "gl"   '(:ignore t :which-key "list")
      "gli" '(forge-list-issues             :which-key "list issues")
      "gln" '(forge-list-notifications      :which-key "list notifications")
      "glp" '(forge-list-pullreqs           :which-key "list pull requests")
      "glr" '(forge-list-issues             :which-key "list repositories")
      "gls" '(magit-list-submodules         :which-key "list submodules")

      "go"   '(:ignore t :which-key "open")
      "goI" '(forge-browse-issues           :which-key "Browse issues")
      "goP" '(forge-browse-pullreqs         :which-key "Browse pull requests")
      "goc" '(forge-browse-commit           :which-key "Browse commit")
      "goh" '(+vc/browse-at-remote-homepage :which-key "Browse homepage")
      "goi" '(forge-browse-issue            :which-key "Browse an issue")
      "goo" '(+vc/browse-at-remote          :which-key "Browse file or region")
      "gop" '(forge-browse-pullreq          :which-key "Browse a pull request")
      "gor" '(forge-browse-remote           :which-key "Browse remote")

      ;; Help
      "h"   '(:ignore t :which-key "help")
      "h<return>" '(info-emacs-manual                :which-key "Emacs manual")
      "h'"        '(describe-char                    :which-key "Describe character")
      "ha"        '(apropos                          :which-key "Apropos")
      "hb"        '(embark-bindings                  :which-key "Describe bindings")
      "he"        '(view-echo-area-messages          :which-key "View echo area messages")
      "hf"        '(helpful-function                 :which-key "Describe function")
      "hi"        '(info                             :which-key "Info")
      "hk"        '(helpful-key                      :which-key "Describe key")
      "hm"        '(describe-mode                    :which-key "Describe mode")
      "ho"        '(helpful-symbol                   :which-key "Describe symbol")
      "hp"        '(explain-pause-top                :which-key "Processes")
      "hs"        '(yas-describe-tables              :which-key "Describe symbol")
      "hv"        '(helpful-variable                 :which-key "Describe variable")
      "hx"        '(helpful-command                  :which-key "Describe command")
      "hA"        '(apropos-documentation            :which-key "Apropos(documentation)")
      "hF"        '(describe-face                    :which-key "Describe Face")
      "hM"        '(popwin:messages                  :which-key "View system messages")

      "hw"        '(:ignore t :which-key "which-key")
      "hwf"       '(which-key-show-full-keymap       :which-key "Full keymap")
      "hwi"       '(which-key-show-minor-mode-keymap :which-key "Minor mode keymap")
      "hwk"       '(which-key-show-keymap            :which-key "Show keymap")
      "hwm"       '(which-key-show-major-mode        :which-key "Show major mode")
      "hwt"       '(which-key-show-top-level         :which-key "Show top-level")

      "m"       '(:ignore t :which-key "media")
      "my"      '(empv-youtube          :which-key "youtube")
      "mt"      '(empv-toggle           :which-key "toggle")
      "mT"      '(empv-toggle-video     :which-key "toggle-video")
      "mq"      '(empv-exit             :which-key "quit")
      "ms"      '(empv-display-current  :which-key "show currently playing")

      "mr"      '(:ignore t :which-key "radio")
      "mrr"     '(empv-play-radio                    :which-key "play")
      "mrR"     '(empv-play-random-channel           :which-key "play random")
      "mrl"     '(empv-log-current-radio-song-name   :which-key "log song name")

      "mp"      '(:ignore t :which-key "playlist")
      "mpP"     '(empv-playlist                 :which-key "playlist")
      "mps"     '(empv-playlist-select  :which-key "select")
      "mpn"     '(empv-playlist-next    :which-key "next")
      "mpp"     '(empv-playlist-prev    :which-key "prev")
      "mpc"     '(empv-playlist-clear   :which-key "clear")
      "mpS"     '(empv-playlist-shuffle :which-key "shuffle")

      "mf"      '(:ignore t :which-key "file")
      "mff"     '(empv-play-file        :which-key "play file")
      "mfd"     '(empv-play-directory   :which-key "play directory")

      "mv"      '(:ignore t :which-key "volume")
      "mvd"     '(empv-volume-down      :which-key "down")
      "mvu"     '(empv-volume-up        :which-key "up")

      "mc"      '(:ignore t :which-key "chapter")
      "mcp"     '(empv-chapter-prev     :which-key "prev")
      "mcn"     '(empv-chapter-next     :which-key "next")
      "mcs"     '(empv-chapter-select   :which-key "select")

      ;; Notes
      "nd" '(deft                              :which-key "Deft")
      "nt" '(org-todo-list                     :which-key "Todo list")
      "nf" '(org-roam-node-find                :which-key "Find node")
      "ng" '(org-roam-ui-open                  :which-key "Show graph")
      "nr" '(org-roam-buffer-toggle            :which-key "Toggle roam buffer")
      "nb" '(org-roam-buffer-display-dedicated :which-key "Launch roam buffer")
      "ns" '(org-roam-db-sync                  :which-key "Sync database")
      "nc" '(org-roam-capture                  :which-key "Capture to node")
      "ni" '(org-roam-node-insert                   :which-key "Insert node at point")

      ;; Org mode
      "o" '(:ignore t :which-key "org")
      "ol"  '(org-store-link                        :which-key "Store link")
      "oC"  '(org-capture-goto-target               :which-key "Goto Capture")
      "oo"  '(org-open-at-point                     :which-key "Open link")
      "ot"  '(org-babel-tangle                      :which-key "Tangle")
      "oy"  '(+org/export-to-clipboard              :which-key "Export to clipboard")
      "oY"  '(+org/export-to-clipboard-as-rich-text :which-key "Export to clipboard (rich)")
      "oc"  '(:ignore t :which-key "Capture")
      "ocn" '(ian/capture-to-notes-org              :which-key "Capture to local notes")
      "oct" '(ian/capture-to-todo-org               :which-key "Capture to local todo")
      "oe" '(:ignore t :which-key "export")
      "oeh" '(org-html-export-to-html               :which-key "Export Org to HTML")
      "oa" '(:ignore t :which-key "attach")
      "oaa" '(org-attach                            :which-key "Attach")
      "oac" '(org-download-clipboard                :which-key "Org download clipboard")
      "oad" '(org-attach-delete-one                 :which-key "Delete one attachment")
      "oaD" '(org-attach-delete-all                 :which-key "Delete all attachments")
      "oas" '(org-attach-sync                       :which-key "Sync attachments")
      "oay" '(org-download-yank                     :which-key "Org download yank")

      ;; Project
      "p"  '(:ignore t :which-key "project")
      "ps" '(+neotree/open                     :which-key "Open project sidebar")
      "pa" '(projectile-add-known-project      :which-key "Add new project")
      "pc" '(+ivy/project-compile              :which-key "Compile in project")
      "pd" '(projectile-remove-known-project   :which-key "Remove known project")
      "ps" '(projectile-switch-project         :which-key "Switch projects")
      "pC" '(projectile-configure-project      :which-key "Configure project")
      "pr" '(projectile-run-project            :which-key "Run project")
      "pt" '(magit-todos-list                  :which-key "List project todos")
      "pT" '(projectile-test-project           :which-key "Test project")
      "pz" '(+taskrunner/project-tasks         :which-key "List  project tasks")

      ;; Reference buffers
      "r"  '(:ignore t :which-key "reference buffers")
      "rs" '(scratch-buffer                                                       :which-key "Scratch")
      "rm" '(view-echo-area-messages                                              :which-key "Messages")
      "rc" '(view-lossage                                                         :which-key "Commands History")
      "rn" '((lambda () (interactive) (ian/open-from-project-root "notes.org"))   :which-key "Notes")
      "rt" '((lambda () (interactive) (ian/open-from-project-root "todo.org"))    :which-key "Todo")
      "rl" '((lambda () (interactive) (find-file "~/notes/work/time.org"))        :which-key "Time Log")

      ;; Shell
      "s"  '(:ignore t :which-key "shell")
      "se" '(+eshell/toggle               :which-key "Toggle eshell popup")
      "sE" '(+eshell/here                 :which-key "Open eshell here")
      "sr" '(+eval/open-repl-other-window :which-key "Repl")
      "st" '(+vterm/toggle                :which-key "Toggle vterm popup")
      "sT" '(+vterm/here                  :which-key "Open vterm here")

      ;; Toggle
      "t"  '(:ignore t :which-key "toggle")
      "tc" '(global-display-fill-column-indicator-mode :which-key "Fill Column Indicator")
      "tf" '(flycheck-mode                             :which-key "Flycheck")
      "tm" '(consult-minor-mode-menu                   :which-key "Minor modes")
      "tp" '(org-tree-slide-mode                       :which-key "Org-tree-slide-mode")
      "tw" '(+word-wrap-mode                           :which-key "Soft line wrapping")
      "tz" '(+zen/toggle                               :which-key "Zen mode")
      ;; "tl" '(doom/toggle-line-numbers                  :which-key "Line numbers")

      ;; Visual / appearance
      "v"  '(:ignore t  :which-key "visual/appearance")
      "vt" '(load-theme :which-key "load theme")

      ;; TODO: maybe make this mode-specific stuff, not using workspaces
      ;; Workspaces
      ;; "TAB TAB" '(+workspace/display              :which-key "Display tab bar")
      ;; "TAB d"   '(+workspace/delete               :which-key "Delete this workspace")
      ;; "TAB l"   '(+workspace/load                 :which-key "Load workspace from file")
      ;; "TAB n"   '(+workspace/new                  :which-key "New workspace")
      ;; "TAB N"   '(+workspace/new-named            :which-key "New named workspace")
      ;; "TAB r"   '(+workspace/rename               :which-key "Rename workspace")
      ;; "TAB R"   '(+workspace/restore-last-session :which-key "Restore last session")
      ;; "TAB s"   '(+workspace/save                 :which-key "Save workspace")
      ;; "TAB x"   '(+workspace/kill-session         :which-key "Delete session")
      ))

Notes

  (use-package deft)
  (setq deft-recursive t
        deft-extensions '("org" "txt"))

AI

  (use-package gptel)
  (setq gptl-default-mode 'org-mode)

Org Mode

  (use-package org)

  (add-hook 'org-mode-hook 'yas-minor-mode)

  (org-babel-do-load-languages
   'org-babel-load-languages
   '((emacs-lisp . t)
     (python . t)))

  (setq org-confirm-babel-evaluate nil)

  (require 'org-tempo)
  (add-to-list 'org-structure-template-alist '("sh" . "src shell"))
  (add-to-list 'org-structure-template-alist '("el" . "src emacs-lisp"))

  (add-to-list 'org-structure-template-alist '("py" . "src python"))  

  (setq org-hide-emphasis-markers t)

  (use-package org-appear
    :hook (org-mode . org-appear-mode))

  (use-package org-roam
    :custom
    (org-roam-directory "~/notes/roam")
    :config
    (org-roam-db-autosync-mode)
    (setq org-link-frame-setup '((file . find-file))))

  (use-package deft
    :commands (deft)
    :config 
    (setq deft-directory "~/notes/deft"
          deft-extensions '("org" "txt")
          deft-recursive t))

  (use-package ox-hugo
    :after ox)

  (use-package org-roam-ui
    :after org-roam
    :config
    (setq org-roam-ui-sync-theme t
          org-roam-ui-follow t
          org-roam-ui-update-on-save t
          org-roam-ui-open-on-start t))

  (use-package ox-gfm)  ; github-flavored-markdown exporter

  (use-package org-download)

Agenda

  (setq org-agenda-start-with-log-mode t)
  (setq org-agenda-files '("~/notes/deft/tasks.org" "~/notes/roam/daily"))
  
  (setq org-agenda-span 10)  
  

  (setq org-todo-keywords '((sequence "TODO(t)" "NEXT(n)" "EVENT(e)" "|" "DONE(d!)" "CANCELLED(c@)")))

Capture templates

  (defun ian/capture-to-notes-org ()
    "Capture the current region (if selected) or a link to the point into 'notes.org' at the top of the Projectile project directory."
    (interactive)
    (let* ((selected-text (if (use-region-p)
                              (buffer-substring-no-properties (mark) (point))
                            nil))
           (file-path (buffer-file-name))
           (project-root (projectile-project-root))
           (notes-file (expand-file-name "notes.org" project-root))
           (link (format "[[file:%s::%d][Link to %s::%d]]\n\n"
                         file-path
                         (line-number-at-pos)
                         (file-name-nondirectory file-path)
                         (line-number-at-pos)))
           (template (if selected-text
                         (concat "* %?\n:PROPERTIES:\n:CAPTURED: %U\n:END:\n#+BEGIN_SRC\n" selected-text "\n#+END_SRC\n\n" link)
                       (concat "* %?\n:PROPERTIES:\n:CAPTURED: %U\n:END:\n" link)))
           (org-capture-entry `("r" "Capture" entry
                                (file ,notes-file)
                                ,template)))
      (org-capture nil "r")
      (with-current-buffer (find-file-noselect notes-file)
        (save-buffer))))

  (defun ian/capture-to-todo-org ()
    "Capture the current region (if selected) or a link to the point into 'todo.org' at the top of the Projectile project directory."
    (interactive)
    (let* ((selected-text (if (use-region-p)
                              (buffer-substring-no-properties (mark) (point))
                            nil))
           (file-path (buffer-file-name))
           (project-root (projectile-project-root))
           (notes-file (expand-file-name "todo.org" project-root))
           (link (format "[[file:%s::%d][Link to %s::%d]]\n\n"
                         file-path
                         (line-number-at-pos)
                         (file-name-nondirectory file-path)
                         (line-number-at-pos)))
           (template (if selected-text
                         (concat "* %?\n:PROPERTIES:\n:CAPTURED: %U\n:END:\n#+BEGIN_SRC\n" selected-text "\n#+END_SRC\n\n" link)
                       (concat "* %?\n:PROPERTIES:\n:CAPTURED: %U\n:END:\n" link)))
           (org-capture-entry `("r" "Capture" entry
                                (file ,notes-file)
                                ,template)))
      (org-capture nil "r")
      (with-current-buffer (find-file-noselect notes-file)
        (save-buffer))))

  (defun checklist-daily-capture ()
  "Generates daily checklist content."
  (concat "* Daily Checklist\n"
          "- [ ] Brush teeth\n"
          "- [ ] Stretch\n"
          "- [ ] Clean 20m\n"))

  (defun weekday-specific-daily-capture (day-of-week)
  "Generate checklist content which is specific to the weekday"
  (cond
      ((string= day-of-week "Monday")
      (concat ""))
      ((string= day-of-week "Tuesday")
      (concat "* Tuesday Checklist\n"
          "- [ ] Rollout trash\n"))
      ((string= day-of-week "Thursday")
      (concat 
          "- [ ] Water Houseplants\n"))
      ((string= day-of-week "Friday")
      (concat ""))
      ((string= day-of-week "Saturday")
      (concat ""))
      ((string= day-of-week "Sunday")
      (concat "* Sunday Checklist\n"
          "- [ ] Start Sourdough\n"))
      ))

  (defun workout-daily-capture (day-of-week)
  "Generate workout table content based on DAY-OF-WEEK."
  (concat
          "* Workout\n"
          "| Exercise | Weight | Sets | Reps | Notes |\n"
          "|----------+--------+------+------+-------|\n"
          (cond
              ((string= day-of-week "Monday")
              (concat
              "| VSQ      |        |      |      |       |\n"
              "| CGB      |        |      |      |       |\n"
              "| BTN      |        |      |      |       |\n"
              "| OHP      |        |      |      |       |\n"
              "| SC       |        |      |      |       |\n"))
              ((string= day-of-week "Tuesday")
              (concat
              "| RDL      |        |      |      |       |\n"
              "| ROW      |        |      |      |       |\n"
              "| WCHP     |        |      |      |       |\n"
              "| CU       |        |      |      |       |\n"))
              ((string= day-of-week "Thursday")
              (concat
              "| BP       |        |      |      |       |\n"
              "| JMP      |        |      |      |       |\n"
              "| SQ       |        |      |      |       |\n"
              "| SC       |        |      |      |       |\n"))
              ((string= day-of-week "Friday")
              (concat
              "| DL       |        |      |      |       |\n"
              "| WPU      |        |      |      |       |\n"
              "| CU       |        |      |      |       |\n"))
              (t "Rest Day\n"))))

  (defun diet-daily-capture ()
      "Capture for calorie tracking"
      (concat
       "* Diet\n"
       "- Weight :: \n"
       "| Food | Calories | Total |\n"
       "|------|----------|-------|\n"
       "|      |          |       |\n"))

  (defun spending-daily-capture ()
      "Capture for spending"
      (concat
       "* Spending\n"
       "| Expense | Price | Total |\n"
       "|---------|-------|-------|\n"
       "|         |       |       |\n"))

  (setq org-roam-dailies-capture-templates
        `(("d" "Daily Routine" entry
           ,(concat 
             "* Scheduled Today\n\n"
             "%(weekday-specific-daily-capture \"%<%A>\")\n"
             "%(checklist-daily-capture)\n"
             "%(workout-daily-capture \"%<%A>\")\n"
             "%(diet-daily-capture)\n"
             "%(spending-daily-capture)\n")
           :target (file+head "%<%Y-%m-%d>.org" "#+title: %<%Y-%m-%d>\n"))))
d Daily Routine entry * Scheduled Today

Project management

  (use-package projectile)  ; todo config inside use-package
  (use-package treemacs-projectile)

Autocomplete

Vertico

(use-package vertico
    :init
    (vertico-mode)
    :config
    (setq vertico-cycle t)
    :bind (:map vertico-map
                ("DEL" . vertico-directory-delete-char)
                ("M-DEL" . vertico-directory-delete-word)))

Consult

  (use-package consult
    :bind (:map minibuffer-local-map ("C-r" . consult-history))
    :init
    (setq register-preview-delay 0.3
          register-preview-function #'consult-register-format)
    (advice-add #'register-preview :override #'consult-register-window)
    (setq xref-show-xrefs-function #'consult-xref
          xref-show-definitions-function #'consult-xref)
    :config
    (setq consult-ripgrep-args "rg --null --line-buffered --color=never --max-columns=1000 --path-separator / --smart-case --no-heading --with-filename --line-number --search-zip --hidden")
    (consult-customize
     consult-recent-file consult-buffer :preview-key "M-.")
    (setq completion-in-region-function #'consult-completion-in-region))

Generic completion

Idk I think this came from the vertico readme

  (use-package emacs
  :init
  ;; 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)

  (setq enable-recursive-minibuffers t))
  (setq completion-styles '(basic substring partial-completion flex))
  ;; Note: overridden below by orderless

Orderless

  (use-package orderless
    :init
    (setq completion-styles '(orderless basic)
          completion-category-defaults nil
          completion-category-overrides '((file (styles partial-completion)))))

Marginalia

  (use-package marginalia
    :bind (:map minibuffer-local-map ("M-A" . marginalia-cycle))
    :init
    (marginalia-mode))

Company-mode

  (use-package company
    :ensure t
    :hook (prog-mode . company-mode)
    :diminish
    :bind (:map company-active-map
                ("<tab>" . company-select-next)
                ("TAB" . company-select-next)
                ("<backtab>" . company-select-previous)
                ("<S-tab>" . company-select-previous))
    (:map lsp-mode-map  ; more general mode than this?
          ("<tab>" . company-indent-or-complete-common))
    :config
    (global-company-mode 1)
    :init
    (setq company-idle-delay nil))

Embark

  (use-package embark
    :ensure t
    :after evil evil-collection evil-org
    :diminish eldoc-mode
    :bind
    (("C-." . embark-act)         ;; pick some comfortable binding
     ("C-;" . embark-dwim)        ;; good alternative: M-.
     ("C-h B" . embark-bindings)) ;; alternative for `describe-bindings'
    :init

    ;; Show the Embark target at point via Eldoc.  You may adjust the Eldoc
    ;; strategy, if you want to see the documentation from multiple providers.
    ;; (add-hook 'eldoc-documentation-functions #'embark-eldoc-first-target)
    (general-def :states '(normal visual) "C-." 'embark-act)
    (general-def :states '(normal visual) "C-;" 'embark-dwim)


    :config
    ;; Hide the mode line of the Embark live/completions buffers
    (add-to-list 'display-buffer-alist
                 '("\\`\\*Embark Collect \\(Live\\|Completions\\)\\*"
                   nil
                   (window-parameters (mode-line-format . none)))))

  ;; Consult users will also want the embark-consult package.
  (use-package embark-consult
    :ensure t ; only need to install it, embark loads it after consult if found
    :hook
    (embark-collect-mode . consult-preview-at-point-mode))

  ;; Use mixed indicator: minimal prompt that switches to completing-read for narrowing
  (setq embark-indicators
        '(embark-mixed-indicator
          embark-highlight-indicator
          embark-isearch-highlight-indicator))

Snippets

  (use-package yasnippet
    :diminish yas-minor-mode)
  (use-package yasnippet-snippets
    :after lsp-mode)
  (yas-global-mode)

Languages

LSP

  (use-package lsp-mode
    :commands (lsp lsp-deferred)
    :init
    (setq lsp-keymap-prefix "C-c l")
    :config
    (lsp-enable-which-key-integration t)
    (setq lsp-headerline-breadcrumb-enable nil)
    :hook (terraform-mode . lsp-deferred)
    :hook (rust-mode . lsp-deferred))

  (use-package lsp-ui)
  (use-package lsp-treemacs)
  (use-package flycheck
    :ensure t
    :diminish
    :init (global-flycheck-mode))

Rust

  (use-package rust-mode
    :ensure t
    :mode "\\.rs\\'"
    :config
    (setq rust-format-on-save t))

  (use-package cargo
    :ensure t
    :hook (rust-mode . cargo-minor-mode))

Terraform

  (use-package terraform-mode
    :ensure t)

Lisp

All lisp-family languages

                                              ; Required for some reason
      ;; symex's deps are all countvajhula packages not in any recipe
      ;; repo. Pre-register all recipes so straight can resolve
      ;; transitive deps regardless of build order.
      (dolist (recipe '((pubsub       :host github :repo "countvajhula/pubsub")
                        (virtual-ring :host github :repo "countvajhula/virtual-ring")
                        (mantra       :host github :repo "countvajhula/mantra")
                        (repeat-ring  :host github :repo "countvajhula/repeat-ring")
                        (lithium      :host github :repo "countvajhula/lithium")))
        (straight-register-package recipe))

      (use-package pubsub       :straight (pubsub       :host github :repo "countvajhula/pubsub"))
      (use-package virtual-ring :straight (virtual-ring :host github :repo "countvajhula/virtual-ring"))
      (use-package mantra       :straight (mantra       :host github :repo "countvajhula/mantra"))
      (use-package repeat-ring  :straight (repeat-ring  :host github :repo "countvajhula/repeat-ring"))
      (use-package lithium      :straight (lithium      :host github :repo "countvajhula/lithium"))

      ;; rigpa and symex-rigpa disabled for now — rigpa pulls in a
      ;; newer transient that needs cond-let which straight can't resolve
      ;; (use-package rigpa
      ;;   :straight
      ;;   (rigpa
      ;;    :host github
      ;;    :repo "countvajhula/rigpa"))

      (use-package symex-core
        :straight
        (symex-core
         :host github
         :repo "drym-org/symex.el"
         :files ("symex-core/symex*.el")))

      (use-package symex
        :after (symex-core)
        :straight
        (symex
         :host github
         :repo "drym-org/symex.el"
         :files ("symex/symex*.el" "symex/doc/*.texi" "symex/doc/figures"))
        :config
        (symex-mode 1)
        (global-set-key (kbd "s-;") #'symex-mode-interface)
        ;; RET in normal mode enters symex
        (evil-define-key 'normal symex-lisp-mode-map
          (kbd "RET") #'symex-mode-interface)
        ;; ESC in insert mode goes to symex, then normal on next ESC
        (evil-define-key 'insert symex-lisp-mode-map
          (kbd "<escape>") (lambda ()
                             (interactive)
                             (evil-normal-state)
                             (symex-mode-interface))))

      (use-package symex-evil
        :after (symex evil)
        :straight
        (symex-evil
         :host github
         :repo "drym-org/symex.el"
         :files ("symex-evil/symex*.el"))
        :config
        (symex-evil-mode 1))

      (use-package symex-ide
        :after (symex)
        :straight
        (symex-ide
         :host github
         :repo "drym-org/symex.el"
         :files ("symex-ide/symex*.el"))
        :config
          (define-key symex-editing-mode-map
              (kbd "?")
              #'symex-describe)
          (define-key symex-editing-mode-map
              (kbd "g d")
              #'geiser-edit-symbol-at-point)
        (symex-ide-mode 1))

      ;; (use-package symex-rigpa
      ;;   :after (symex rigpa symex-evil)
      ;;   :straight
      ;;   (symex-rigpa
      ;;    :host github
      ;;    :repo "drym-org/symex.el"
      ;;    :files ("symex-rigpa/symex*.el"))
      ;;   :config
      ;;   (symex-rigpa-mode 1))

Scheme

Guile
  (use-package geiser)
  (use-package geiser-guile)

  ;; Guile debug mode uses ',' to bind a bunch of useful command
  ;; None of my evil packages seem to account for this
  (with-eval-after-load 'geiser-debug
    (evil-define-key 'motion geiser-debug-mode-map
      (kbd ",") #'geiser-guile-debug-menu))

  ;; Why why why doesn't homebrew just handle this?
  (when IS-MACOS
    (let ((brew-guile-share "/opt/homebrew/share/guile/site/3.0")
          (brew-guile-cache "/opt/homebrew/lib/guile/3.0/site-ccache")
          (brew-guile-ext   "/opt/homebrew/lib/guile/3.0/extensions"))
      (setenv "GUILE_LOAD_PATH" brew-guile-share)
      (setenv "GUILE_LOAD_COMPILED_PATH" brew-guile-cache)
      (setenv "GUILE_SYSTEM_EXTENSIONS_PATH" brew-guile-ext)
      (setenv "GUILE_TLS_CERTIFICATE_DIRECTORY"
              "/opt/homebrew/etc/gnutls/")
      (setenv "GUILE_LOAD_PATH"
              (string-join
               '("/opt/homebrew/share/guile/site/3.0"
                 "/opt/homebrew/share/guile-lib")
               ":"))
      ))

Go

  (use-package go-mode)
  (add-hook 'go-mode-hook 'lsp-deferred)

Markups

Markdown

  (use-package markdown-mode)
  (use-package evil-markdown
    :after evil
    :straight (evil-markdown :type git :host github :repo "somelauw/evil-markdown")
    :mode ("\\.md\\'" . evil-markdown-mode))
  (use-package json-mode)
  (use-package yaml-mode)

Beancount

This is mostly lifted from the doom emacs config. Beancount-mode by default has a pretty borked tab function.

  (defun beancount-tab-function ()
    "Improved beancount tab ripped off from doom emacs"
    (interactive)
    (if (and outline-minor-mode (outline-on-heading-p))
	(beancount-outline-cycle)
      (indent-according-to-mode)))

  (use-package beancount
    :straight (beancount :type git :host github :repo "beancount/beancount-mode")
    :mode ("\\.beancount\\'" . beancount-mode)
    :hook (beancount-mode . outline-minor-mode)
    :config
    (evil-define-key 'normal 'beancount-mode-map
      (kbd "TAB") 'beancount-tab-function)) ;; Bind the TAB key

  (add-hook 'beancount-mode-hook #'flymake-bean-check-enable)

Epub

  (use-package nov
    :after evil evil-collection
    :config
    ;; (define-key nov-button-map (kbd "SPC") nil)
    ;; (evil-define-key 'normal nov-mode-map (kbd "SPC") nil)
    ;; (evil-define-key 'normal nov-button-map (kbd "SPC") nil)
    ;; (define-key nov-mode-map (kbd "SPC") nil)
    (setq nov-text-width 80))

  (add-to-list 'auto-mode-alist '("\\.epub\\'" . nov-mode))

Containers

Docker

  (use-package dockerfile-mode)
  (use-package docker-compose-mode)
  ;; (use-package docker)
  ;; Todo: lsp-docker? docker-tramp? docker-cli?

Kubernetes

  (use-package k8s-mode)
  ;; kubernetes
  ;; kubernetes-evil
  ;; kubernetes-helm
  ;; kubernetes-tramp

Shells

Enable fish completions

Uses the external fish program to provide fish-style completions to eshell etc

  (use-package fish-completion)
  (when (and (executable-find "fish")
             (require 'fish-completion nil t))
    (global-fish-completion-mode))

Git

  (use-package magit)
  (use-package magit-todos)
  (use-package git-gutter
    :diminish)
  (global-git-gutter-mode t)
  (setq magit-display-buffer-function 'magit-display-buffer-same-window-except-diff-v1)

  (use-package git-timemachine)

Web browsing and youtube

  ;; Using straight:
  ;;(use-package empv
  ;;  :straight (:host github :repo "isamert/empv.el"))

  (use-package empv)

  (setq empv-radio-channels
        '(("SomaFM - Groove Salad" . "http://www.somafm.com/groovesalad.pls")
          ("SomaFM - Drone Zone" . "http://www.somafm.com/dronezone.pls")
          ("SomaFM - Sonic Universe" . "https://somafm.com/sonicuniverse.pls")
          ("SomaFM - Metal" . "https://somafm.com/metal.pls")
          ("SomaFM - Vaporwaves" . "https://somafm.com/vaporwaves.pls")
          ("ADHD 1" . "https://www.youtube.com/watch?v=CmMrm4BpQHU")
          ("WREK FM" . "http://streaming.wrek.org:8000/wrek_live-128kb.m3u")
          ))

  (with-eval-after-load 'embark (empv-embark-initialize-extra-actions))
  (add-to-list 'empv-mpv-args "--ytdl-format=best")
  (add-to-list 'empv-mpv-args "--save-position-on-quit")
  (add-hook 'empv-init-hook #'empv-override-quit-key)

  (setq empv-invidious-instance "https://invidious.projectsegfau.lt/api/v1")

Email, rss, newsgroups

Oh, boy. Here we go. This will come in handy. (TODO: smtp header)

  (setq user-mail-address "ian@keane.sh"
	user-full-name "Ian Keane")

  ;; Assuming your passwords are stored in Password Store with names like:
  ;; "email/fastmail" and "email/gmail"

  ;; IMAP configuration for Fastmail

  (setq gnus-select-method
	'(nnimap "Fastmail-emacs"
		 (nnimap-user "ian@keane.sh")
		 (nnimap-address "imap.fastmail.com")
		 (nnimap-server-port 993)
		 (nnimap-stream ssl)))


  ;; Additional secondary IMAP account for Gmail
  ;; (add-to-list 'gnus-secondary-select-methods
  ;; 	     '(nnimap "Gmail"
  ;; 		      (nnimap-address "imap.gmail.com")
  ;; 		      (nnimap-server-port 993)
  ;; 		      (nnimap-stream ssl)
  ;; 		      (nnimap-authinfo-file nil)  ; Not using .authinfo for Gmail
  ;; 		      (nnir-search-engine imap)
  ;; 		      (nnimap-authenticator login)
  ;; 		      (nnimap-login "your-gmail-username") ; Replace with your Gmail username
  ;; 		      (nnimap-password (password-store-get "email/gmail"))))

  (setq send-mail-function 'smtpmail-send-it
	message-send-mail-function 'smtpmail-send-it
	smtpmail-default-smtp-server "smtp.fastmail.com"
	smtpmail-smtp-server "smtp.fastmail.com"
	smtpmail-smtp-service 587
	smtpmail-stream-type 'starttls
	smtpmail-smtp-user "ian@keane.sh"
	smtpmail-debug-info t)

  ;; You may also want to add a conditional to choose the SMTP server based on the current 'From' address.

  ;; RSS Feeds
  ;; (add-to-list 'gnus-secondary-select-methods '(nnrss ""))

  ;; Configure RSS feed subscriptions
  ;; (setq gnus-secondary-sources
  ;;       '((nnrss "http://example.com/rss") ; replace with actual RSS URL
  ;;         ;; Add more RSS feeds here
  ;;         ))


  ;; Use Gnus for composing email
  (setq mail-user-agent 'gnus-user-agent)

  ;; Set up HTML email rendering
  (setq mm-text-html-renderer 'shr)

Emacs Meta

Explain-Pause

Scrutinize which processes are gumming up emacs

  ;; (use-package explain-pause-mode
  ;;   :straight (explain-pause-mode :type git :host github :repo "lastquestion/explain-pause-mode")
  ;;   :diminish explain-pause-mode
  ;;   :config
  ;;   (explain-pause-mode))

Todo

General

  • Refactor to use use-package more effectively

    • Use :mode and :config like here
    • Set things which should be set before package install in :init
  • Disable source editing issue

    • Goes away when I run (defun org-edit-src-code nil)
  • Go through AWB's notes about using gnus
  • TODO aggregation
  • Suppress evilmarkdown mode and org-roam-ui minor mode

Terminals

  • More terminals
  • emacs-fish-completions
  • setup terminals to complete to vertico
  • starship prompts

New functions

  • Git-time-machine-or-vc-region-history

Note taking

  • deft
  • org : roam2, hugo, pretty, dragndrop, bullet
  • meetlog reference buffer
  • work roam directory
  • Dalies for scheduling things
  • Fix org indentation stuff

Lisp stuff

  • paredit
  • lispy
  • no tabs for yaml-mode (wtf?)

One-offs

  • 'ex' snippet for the most common example of a thing I do per filetype

    • docker-compose example
  • hl-todo
  • persistent undo
  • commentary
  • highlight tabs and newlines
  • Vertico backwards delete
  • alphabetical keybindings
  • brighter modeline
  • surpress global minor modes

    • Undo-Tree, company, flycheck, which-key
  • ignore case in imenu
  • improve which-key visibility
  • gpt popup
  • org-roam-buffer popup
  • conflicts with global popup buffers
  • init popup buffer binding? spc r i?
  • org-roam buffer in popup (follow)
  • prettify-symbols-mode
  • create 'context' keybinding

    • map smerge mode

Embark

  • open file in other window

NFS stuff

  • map music from nfs
  • map books from nfs with nov.el

LSP stuff

  • look into treemacs integrations
  • consider eglot
  • dap-mode

Completion

  • dabbrev-expand or company on c-n c-p
  • c-N c-P for expand full line
  • yasnippet
  • company-mode to corfu (or vertico completions? or cofu-in-vertico?)
  • get vertico as completion box

Keybindings

  • bind (custom?) git-view-from-other-branch

    • want to be able to view a file as it is in another branch
  • bind (custom?) git diff from other branch
  • bind capture to init todo
  • bind capture to project todo
  • bind org-set-property
  • treemacs-open-other-window
  • treemacs | to open in split
  • create-tab-with-name function
  • buffer to spc b b local, spc b B global
  • do somthing useful with ','
  • custom function for toggle between last 2 windows
  • custom function to scroll-other-window using last 2 windows
  • evil-define-key instead of define-key everywhere
  • bind something to create file in dired / treemacs
  • fix general prefix in treemacs mode
  • fix vim-surround
  • snippets help spc h y, helpful-symbol spc h s
  • put general bindings in separate keymaps in separate sections
  • map hyper bindings for buffer nav and jumping
  • evil-org
  • map alignment functions in 'code'section
  • fix c-u in evil mode
  • fix tab key
  • window size functions, winner undo/redo
  • map default-text-scale operations
  • org-capture from string and org-ripgrep
  • move spc p s back to spc p p
  • org eval block
  • explain-pause spc h p

    • Want this for org-roam, don't want to rebind tab here
  • empv

    • map
    • configure

      • empv-radio-log-file
      • empv-radio-log-format
      • spc m y(outube)
      • spc m l(ocal)
      • spc m r(adio)
      • empv-video-dir empv-audio-dir?
      • empv-play-file
  • Set display-buffer-alist values for how I want windows treated (integrates with popper), e.g.
  (add-to-list 'display-buffer-alist
	       '("\\*ielm\\*"
		 (display-buffer-in-side-window)
		 (side . left)
		 (window-width . 50)))

selected radio (See empv-radio-log-file and empv-radio-log-format variables and their documentations).

New plugins

  • tramp
  • k8s
  • docker
  • terraform
  • straight?
  • embark?
  • emms, emms-play-url binding (youtube view)
  • consult-lsp (better for errors than treemacs integrations probably)
  • srht.el
  • affe
  • gnus

Appearance switches

  • fonts
  • random theme
  • org mode bullets / pretty / etc

Research

  • What does C-d do when in a minibuffer for vertico?
  • embark
  • ace-window / embark integration

Annoyances

  • set require-final-newline = nil
  • fix gitgutter behavior
  • remove extra line file modifying behavior
  • fix missing snippets in note-taking mode
  • always open treemacs from most recent window
  • prevent :wq from killing frame

Ediff

  • vertical
  • no new frame
  • bind magit-ediff-compare

      (setq ediff-window-setup-function #'ediff-setup-windows-plain)
  • bind ediff two files
  • fix fuzzy searching for functions
  • bind SPC - to dired-jump
  • fix markdown-mode (doesn't start with .md files automatically)
  • bind magit-log-buffer-file (handles selection-based history)
  • adaptive wrap (plus fix visual-line-mode to always load)

  • install / configure consult-org-roam
  • map default-text-scale-increase/decrease (better than regular text scaling)

    • set default-text-scale-amount to 5 instead of 10
  • map outline-toggle-children in tf mode
  • map ripgrep in parent dir
  • map ripgrep in (dired select)
  • add .git to ripgrep ignore
  • fix vterm

    • make the popup frame-wide
    • fix spc v t
    • maybe just make spc tab open shell and exclude it from ace managed windows
  • make eshell pretty
  • when toggling shell, enter insert mode
  • migrate to wezterm, impelement popup term
  • look into letting org fold blocks or folding bracket blocks in general cases
  • try (add-hook 'magit-post-refresh-hook 'git-gutter:refresh)
  • make treemacs enter always open in most recent buffer
  • add meeting log to reference reaches
  • install ini-mode plugin
  • install lua-mode plugin
  • install fish-mode plugin