“I’m rarely happier than when spending an entire day programming my computer to perform automatically a task that would otherwise take me a good ten seconds to do by hand.” - Douglas Adams
I use Emacs as the primary interface to my machine and remote machines. In many situations, it replaces CLIs or GUIs with well-designed keyboard-driven interfaces (e.g. magit). There’s no going back.
I use Doom Emacs as a base Emacs configuration. This is a literate org file, which specifies and documents the entirety of my Doom configuration. If you link this file to ~/.doom.d/config.org
, Doom will automatically tangle it on startup and whenever it changes.
If you’re reading this in a browser, consider opening it in Emacs Org mode for the full experience.
- Jethro Kuan’s dotfiles
- bauer: an Emacs+Nix IDE
- Pierre Neidhardt: Emacs Everywhere
- LemonBreezes’ Literate Doom Config
- Diego Zamboni’s Literate Emacs Config
- Justin Abrahms’ Literate Emacs Config
- About
- Doom Module Declarations
- Package Configuration
- Header
- Global Constants
- Hardware
- Load helper functions
- Visual Settings
- Key Chord Config
- Org
- Remote Machine Interfaces
- Effective Editing
- Rust
- Consult Bindings
- Search Utilities
- Version Control
- Dired
- Window Management
- Registers
- Load Untracked Elisp (work-specific)
- Misc Global Keybindings
- Don’t save passwords to clipboard history
- Misc
- Autosave
- Better Expansions with Fancy Dabbrev
- Utility functions.
- Package declarations
This file controls what Doom modules are enabled and what order they load in.
Remember to run doom sync
or doom/reload
after modifying it.
;;; init.el -*- lexical-binding: t; -*-
(doom! :input
:personal
neurosys
:completion
(vertico +icons)
:ui
doom
modeline
zen
:editor
lispy
multiple-cursors
(format +onsave)
:emacs
dired
electric
:term
vterm
:checkers
syntax
:tools
direnv
docker
magit
(lsp +eglot)
pass
pdf
tree-sitter
:lang
cc
data
emacs-lisp
;; (go +lsp)
;; javascript
;; (haskell +dante)
;; (julia +lsp)
;; (latex +latexmk +cdlatex)
markdown
nix
(org +roam2)
python
;; rest
(rust +lsp)
sh
yaml
:config
literate
(default +bindings))
lexical-binding
for this file.
;;; $DOOMDIR/config.el -*- lexical-binding: t; -*-
Some functionality uses this to identify you, e.g. GPG configuration, email clients, file templates and snippets.
(setq user-full-name "Dan Girshovich"
user-mail-address (rot13 "[email protected]"))
(setq my/home-dir "/home/dan/")
(setq my/sync-base-dir (concat my/home-dir "Sync/"))
(setq my/work-base-dir (concat my/home-dir "Work/"))
(setq my/media-base-dir (concat my/home-dir "Media/"))
(setq org-directory my/sync-base-dir
org-roam-directory "/home/dan/Sync/org-roam2/"
org-roam-db-location (concat org-roam-directory "org-roam.db")
my/org-roam-todo-file (concat org-roam-directory "orgzly/todo.org"))
(save-window-excursion
(find-file my/org-roam-todo-file)
(save-buffer))
(setq my/brightness-min 1)
(setq my/brightness-max 100)
(setq my/brightness-step 5)
(defun my/set-brightness (level)
(interactive "nBrightness level: ")
(let ((safe-level
(cond ((< level my/brightness-min) my/brightness-min)
((> level my/brightness-max) my/brightness-max)
(t level))))
(save-window-excursion
(shell-command
(format "sudo light -S %s" safe-level) nil nil))))
(load-file (concat doom-private-dir "funcs.el"))
(setq
doom-font (font-spec :family "Iosevka" :size 24)
doom-variable-pitch-font (font-spec :family "Libre Baskerville")
doom-serif-font (font-spec :family "Libre Baskerville"))
(setq display-line-numbers-type nil)
;; Thin grey line separating windows
(set-face-background 'vertical-border "grey")
(set-face-foreground 'vertical-border (face-background 'vertical-border))
(use-package! doom-themes
:config
(setq doom-themes-enable-bold t
doom-themes-enable-italic t)
(load-theme 'doom-one t)
(doom-themes-visual-bell-config)
;; Corrects (and improves) org-mode's native fontification.
(doom-themes-org-config))
I don’t use Evil (Vim emulation), which would add an extra layer of complexity to everything. Instead, I heavily leverage key-chord.el, which enables binding simultaneous key presses (chords) to commands.
I have some custom code to bind chords to Doom’s leaders. Many commonly used commands are bound in these “key chord maps”.
Set hardware-specific delay. Tweak this if:
- there are false keychords triggered when typing fast (delay too large)
- if expected keychords don’t register (delay too small)
- there’s a noticable lag when typing normally (delay too large)
(use-package! key-chord
:config
(key-chord-mode 1)
(setq key-chord-one-key-delay 0.20 ; same key (e.g. xx)
key-chord-two-keys-delay 0.075)
(customize-set-variable 'key-chord-safety-interval-forward 0.0)
(customize-set-variable 'key-chord-safety-interval-backward 0.0))
(defun simulate-seq (seq)
(setq unread-command-events (listify-key-sequence seq)))
(defun send-doom-leader ()
(interactive)
(simulate-seq "\C-c"))
(setq doom-localleader-alt-key "M-c")
(defun send-doom-local-leader ()
(interactive)
(simulate-seq "\M-c"))
https://gist.github.com/dangirsh/86c001351c02b42321d20f462a66da6b
(after! key-chord
;; My external keyboard (Voyager) supports chords in the firmware
;; For some cases, I find it less error prone to use these instead of
;; keychord.el. In these cases, the keyboard sends a function key (e.g. f13)
(key-chord-define-global "pl" 'send-doom-leader)
;; (global-set-key (kbd "<XF86Launch7>") 'send-doom-leader)
(key-chord-define-global "bj" 'send-doom-local-leader)
(setq dk-keymap (make-sparse-keymap))
(key-chord-define-global "fu" dk-keymap)
(global-set-key (kbd "<XF86Launch6>") dk-keymap)
(defun add-to-keymap (keymap bindings)
(dolist (binding bindings)
(define-key keymap (kbd (car binding)) (cdr binding))))
(defun add-to-dk-keymap (bindings)
(add-to-keymap dk-keymap bindings))
(add-to-dk-keymap
'(("." . jump-to-register)
("<SPC>" . rgrep)
("a" . my/org-agenda)
("b" . my/set-brightness)
("c" . my/open-literate-private-config-file)
("d" . dired-jump)
("k" . doom/kill-this-buffer-in-all-windows)
("m" . magit-status)
("n" . narrow-or-widen-dwim)
("s" . save-buffer)
("t" . +vterm/here)
("v" . neurosys/open-config-file)
("w" . google-this-noconfirm)))
(key-chord-define-global ",." 'end-of-buffer)
(key-chord-define-global "xc" 'beginning-of-buffer)
(key-chord-define-global "zx" 'beginning-of-buffer)
(key-chord-define-global "qw" 'delete-window)
(key-chord-define-global "q;" 'delete-other-windows)
(key-chord-define-global ",," 'doom/open-scratch-buffer)
(key-chord-define-global "pu" 'other-window)
(key-chord-define-global "fl" 'rev-other-window)
(global-set-key (kbd "<XF86Launch5>") 'other-window)
(global-set-key (kbd "<XF86Tools>") 'rev-other-window)
(key-chord-define-global "dh" 'split-window-vertically-and-switch)
(key-chord-define-global "mn" 'split-window-horizontally-and-switch)
(key-chord-define-global "nh" 'my/duplicate-line-or-region)
(key-chord-define-global "td" 'comment-line)
(key-chord-define-global "uy" 'er/expand-region)
(key-chord-define-global "xx" 'execute-extended-command)
(key-chord-define-global "xt" 'ffap))
“Notes aren’t a record of my thinking process. They are my thinking process.” – Richard Feynman
I largely live inside Org. It currently manages:
- My second brain with org-roam & org-journal
- literate programming with babel and emacs-jupyter (e.g. this file)
- tasks + calendar with org-agenda and calfw
- Writing / blogging with ox-hugo, pandoc, etc…
- Has nice inline rendering of LaTeX
- Managing references + pdfs with org-ref
- Annotating PDFs with notes via org-noter
(use-package! org
:mode ("\\.org\\'" . org-mode)
:init
(add-hook 'org-src-mode-hook #'(lambda () (flycheck-mode 0)))
(add-hook 'org-mode-hook #'(lambda () (flycheck-mode 0)))
(map! :map org-mode-map
"M-n" #'outline-next-visible-heading
"M-p" #'outline-previous-visible-heading
"C-c ;" nil)
(setq org-src-window-setup 'current-window
org-return-follows-link t
org-confirm-elisp-link-function nil
org-confirm-shell-link-function nil
org-catch-invisible-edits 'show
;; Use with consel-org-goto (gh .)
org-goto-interface 'outline-path-completion)
(setq org-file-apps '((auto-mode . emacs)
(directory . emacs)
("\\.mm\\'" . default)
("\\.x?html?\\'" . default)
("\\.pdf\\'" . (lambda (file link) (org-pdftools-open link))))))
(after! org
;; FIXME: Don't know why this isn't loaded automatically...
(require 'ob-async)
(setq org-capture-templates '())
(setq org-confirm-babel-evaluate nil
org-use-property-inheritance t
org-export-use-babel nil
org-pretty-entities nil
org-use-speed-commands t
org-return-follows-link t
org-outline-path-complete-in-steps nil
org-ellipsis ""
org-fontify-whole-heading-line t
org-fontify-done-headline t
org-fontify-quote-and-verse-blocks t
org-image-actual-width nil
org-src-fontify-natively t
org-src-tab-acts-natively t
org-startup-indented t
org-src-preserve-indentation t
org-edit-src-content-indentation 0
org-adapt-indentation nil
org-hide-emphasis-markers t
org-special-ctrl-a/e t
org-special-ctrl-k t
org-yank-adjusted-subtrees t
org-src-window-setup 'reorganize-frame
org-src-ask-before-returning-to-edit-buffer nil
org-insert-heading-respect-content nil)
(add-to-list 'org-structure-template-alist '("el" . "src emacs-lisp"))
(add-to-list 'org-structure-template-alist '("sh" . "src sh"))
(add-to-list 'org-structure-template-alist '("r" . "src rust"))
(add-to-list 'org-structure-template-alist '("py" . "src jupyter-python"))
(setq org-refile-use-outline-path 'file
org-outline-path-complete-in-steps nil
org-refile-allow-creating-parent-nodes 'confirm)
;; Colorize org babel output. Without this color codes are left in the output.
(defun my/display-ansi-colors ()
(interactive)
(let ((inhibit-read-only t))
(ansi-color-apply-on-region (point-min) (point-max))))
(add-hook 'org-babel-after-execute-hook #'my/display-ansi-colors)
(advice-add 'org-meta-return :override #'my/org-meta-return)
(setq org-tags-match-list-sublevels 'indented)
(setq org-image-actual-width nil)
(setq org-agenda-files '())
(setq org-todo-keywords
'((sequence
"TODO(t)"
"WAIT(w)"
"HOLD(h)"
"IDEA(i)"
"DELEGATED(e)"
"|"
"DONE(d)"
"KILL(k)")
)
org-todo-keyword-faces
'(("WAIT" . +org-todo-onhold)
("HOLD" . +org-todo-onhold)
("DELEGATED" . +org-todo-onhold)
("KILL" . +org-todo-cancel)))
;; Update parent TODO state when all children TODOs are done
;; NOTE: Only works if the parent has a "[/]" or "[%]" in the heading!!
;; https://orgmode.org/manual/Breaking-Down-Tasks.html#Breaking-Down-Tasks
(defun org-summary-todo (n-done n-not-done)
"Switch entry to DONE when all subentries are done, to TODO otherwise."
(let (org-log-done org-log-states) ; turn off logging
(org-todo (if (= n-not-done 0) "DONE" "TODO"))))
(add-hook 'org-after-todo-statistics-hook 'org-summary-todo)
;; (add-to-list 'org-agenda-files "~/Sync/org-roam/orgzly/boox-incoming.org")
(add-to-list 'org-agenda-files "~/Sync/org-roam2/orgzly/pixel-incoming.org")
(add-to-list 'org-agenda-files "~/Sync/org-roam2/orgzly/incoming.org")
(add-to-list 'org-latex-default-packages-alist "\\PassOptionsToPackage{hyphens}{url}")
(require 'ox-latex))
;; Setup syntax highlighting for code block pdf exports
;; (after! ox-latex
;; (setq org-latex-pdf-process
;; '("pdflatex -shell-escape -interaction nonstopmode -output-directory %o %f")
;; org-latex-listings 'minted
;; org-latex-packages-alist '(("" "minted"))))
(use-package! toc-org
:hook (org-mode . toc-org-mode))
aka my exocortex
(defun my/org-roam-capture-new-node-hook ()
(org-entry-put (point) "header-args" ":noweb yes"))
(after! org-roam
(setq +org-roam-open-buffer-on-find-file nil
org-id-link-to-org-use-id t
org-roam-mode-section-functions (list #'org-roam-backlinks-section
#'org-roam-reflinks-section
#'org-roam-unlinked-references-section))
(add-hook 'org-roam-capture-new-node-hook 'my/org-roam-capture-new-node-hook))
(after! org-roam-dailies
(setq org-roam-dailies-directory "daily/")
(setq org-roam-dailies-capture-templates
'(("d" "default" entry
"* %?"
:if-new (file+head "%<%Y-%m-%d>.org"
"#+TITLE: %<%Y-%m-%d>\n#+FILETAGS: daily")))))
(add-to-dk-keymap
'(("J" . org-roam-dailies-goto-today)))
;; leader-n-r-d-t also works, but this muscle-memory from the org-journal days is easier to type
(map! :leader
(:prefix-map ("n" . "notes")
(:prefix ("j" . "journal")
:desc "Today" "j" #'my/today)))
In real Roam, TODO tags can be conveniently interspersed in any file. Then, filtering backlinks on the TODO page is the agenda view.
Unfortunately, this workflow doesn’t work for org-roam, since org-agenda is implemented too ineffeciently to handle thousands of agenda files.
My fix, as recommended here, is to put capture todos to a single file, but auto-insert links back to the context of the todo. Then, any TODOs for a page should be visible in the backlinks of that page. This is an inversion of the setup available in Roam.
Jethro mentions a better solution potentially coming soon (org-roam-agenda) at the bottom of this post.
The org-capture-templates
templates used here:
Template | Doc |
---|---|
%? | Initial cursor position |
%F | File path of original buffer |
%i | Body |
%a | Link back to context |
(after! org
(add-to-list 'org-agenda-files my/org-roam-todo-file)
(add-to-list 'org-capture-templates '("t" "Todo" entry (file my/org-roam-todo-file)
"* TODO %?"))
(add-to-list 'org-capture-templates '("T" "Todo with Context" entry (file my/org-roam-todo-file)
"* TODO %? #[[%F][%(my/org-get-title \"%F\")]]\n%i\n%a"))
)
(setq org-agenda-start-day "+0d" ; start today
org-agenda-show-current-time-in-grid nil
org-agenda-timegrid-use-ampm t
org-agenda-use-time-grid nil ; Toggle it with 'G' in agenda view
org-agenda-span 3
org-agenda-skip-timestamp-if-done t
org-agenda-skip-deadline-if-done t
org-agenda-overriding-header "⚡ Agenda"
org-agenda-prefix-format '((agenda . " %i %-12:c%?-12t% s")
(todo . " %i %b")
(tags . " %i %-12:c %b")
(search . " %i %-12:c %b"))
org-agenda-category-icon-alist
`(("Personal" ,(list (nerd-icons-mdicon "nf-md-home" :height 1.2)) nil nil :ascent center)
("Incoming" ,(list (nerd-icons-mdicon "nf-md-inbox_arrow_down" :height 1.2)) nil nil :ascent center))
org-agenda-todo-keyword-format "%-1s"
org-agenda-scheduled-leaders '("" "")
org-agenda-deadline-leaders '("Deadline: " "In %3d d.: " "%2d d. ago: ")
org-priority-highest 1
org-priority-lowest 5
org-priority-default 3)
(customize-set-variable 'org-priority-faces '((49 . error)
(50 . warning)
(51 . success)
(52 . success)
(53 . success)))
(defun my/org-agenda ()
(interactive)
(org-agenda nil "n"))
(use-package! org-super-agenda
:after org-agenda
:config
(setq org-super-agenda-groups
'((:discard (:todo "HOLD" :todo "IDEA"))
(:name "WIP"
:todo "[-]")
(:name "High Priority"
:priority "1")
(:name "Med Priority"
:priority "2")
(:name "Low Priority"
:priority "3")
(:name "Lower Priority"
:priority "4")
(:name "Lowest Priority"
:priority "5")
(:name "Today"
;; :time-grid t
:scheduled today
:deadline today)
(:auto-todo t)))
(org-super-agenda-mode))
Automatically pulls the titles from pages from a URL, then inserts a corresponding org-link.
(use-package! org-cliplink)
(after! tramp
(add-to-list 'tramp-remote-path 'tramp-own-remote-path)
(setq tramp-use-scp-direct-remote-copying t)
(customize-set-variable 'tramp-default-method "scp"))
(setq password-cache-expiry nil)
(use-package! teleport
:init (teleport-tramp-add-method)
:bind (:map teleport-list-nodes-mode-map
("v" . vterm)
("t" . term)
("d" . dired)))
(with-eval-after-load 'vterm
(add-to-list 'vterm-tramp-shells `(,teleport-tramp-method "/bin/bash")))
(with-eval-after-load 'dired-rsync
(defun teleport--is-file-on-teleport (filename)
(when (tramp-tramp-file-p filename)
(with-parsed-tramp-file-name filename v
(string= v-method teleport-tramp-method))))
(defun teleport-rsync-advice (orig-func sfiles dest)
(if (or (teleport--is-file-on-teleport (car sfiles)) (teleport--is-file-on-teleport dest))
(let ((dired-rsync-options (format "%s %s" dired-rsync-options "-e \"tsh ssh\"")))
(funcall orig-func sfiles dest))
(funcall orig-func sfiles dest)))
(advice-add 'dired-rsync--remote-to-from-local-cmd :around #'teleport-rsync-advice))
(use-package! org-ai
:hook
(org-mode . org-ai-mode)
:init
(org-ai-global-mode) ; installs global keybindings on C-c M-a
:config
(setq org-ai-service 'anthropic)
(setq org-ai-default-max-tokens 'nil)
(setq org-ai-default-chat-model "claude-3-5-sonnet-20240620")
(setq org-ai-anthropic-api-version "2023-06-01"))
;; hack around password-store init. neither "after" or "requires" worked...
(defun my/set-org-ai-token ()
(setq org-ai-openai-api-token (encode-coding-string (format "%s" (password-store-get "claude/dan.girsh/api-key/emacs")) 'utf-8)))
(run-with-idle-timer 1 nil #'my/set-org-ai-token)
(use-package! copilot
:hook (prog-mode . copilot-mode)
:bind (:map copilot-completion-map
("<tab>" . 'copilot-accept-completion)
("TAB" . 'copilot-accept-completion)
("C-TAB" . 'copilot-accept-completion-by-word)
("C-<tab>" . 'copilot-accept-completion-by-word)))
FIXME: This pulls in ivy/swiper/counsel :/
(use-package! lispy
:config
(advice-add 'delete-selection-pre-hook :around 'lispy--delsel-advice)
;; FIXME: magit-blame still fails to all "ret" when lispy is on
;; the compat code isn't even getting hit!
(setq lispy-compat '(edebug magit-blame-mode))
;; this hook leaves lispy mode off, but that's not as bad as breaking blame!
(add-hook 'magit-blame-mode-hook #'(lambda () (lispy-mode 0)))
:hook
((emacs-lisp-mode common-lisp-mode lisp-mode) . lispy-mode)
:bind (:map lispy-mode-map
("'" . nil) ; leave tick behaviour alone
("M-n" . nil)
("C-M-m" . nil)))
(use-package! wrap-region
:hook
(org-mode . wrap-region-mode)
(latex-mode . wrap-region-mode)
:config
(wrap-region-add-wrappers
'(("*" "*" nil (org-mode))
("~" "~" nil (org-mode))
("/" "/" nil (org-mode))
("=" "=" nil (org-mode))
("_" "_" nil (org-mode))
("$" "$" nil (org-mode latex-mode)))))
(use-package! aggressive-indent
:hook
(emacs-lisp-mode . aggressive-indent-mode)
(common-lisp-mode . aggressive-indent-mode))
(use-package! multiple-cursors
:init
(setq mc/always-run-for-all t)
:config
(add-to-list 'mc/unsupported-minor-modes 'lispy-mode)
:bind (("C-S-c" . mc/edit-lines)
("C-M-g" . mc/mark-all-like-this-dwim)
("C->" . mc/mark-next-like-this)
("C-<" . mc/mark-previous-like-this)
("C-)" . mc/skip-to-next-like-this)
("C-M->" . mc/skip-to-next-like-this)
("C-(" . mc/skip-to-previous-like-this)
("C-M-<" . mc/skip-to-previous-like-this)))
(use-package! undo-tree
:init
(setq undo-tree-auto-save-history nil
undo-tree-visualizer-timestamps t
undo-tree-visualizer-diff t)
:config
;; stolen from layers/+spacemacs/spacemacs-editing/package.el
(progn
;; restore diff window after quit. TODO fix upstream
(defun my/undo-tree-restore-default ()
(setq undo-tree-visualizer-diff t))
(advice-add 'undo-tree-visualizer-quit :after #'my/undo-tree-restore-default))
(global-undo-tree-mode 1))
(use-package! topsy
:defer t
:init
(add-hook! prog-mode
(unless (memq major-mode '(+doom-dashboard-mode org-mode dirvish-mode))
(topsy-mode +1))))
;; Colemak
(customize-set-variable 'avy-keys '(?a ?r ?s ?t ?n ?e ?i ?o))
(setq rustic-lsp-client 'eglot)
(add-hook 'eglot-managed-mode-hook
(lambda ()
(flymake-mode -1)
(eglot-inlay-hints-mode -1)))
(map! :map vertico-map
"C-SPC" #'+vertico/embark-preview)
(global-set-key (kbd "M-i") 'iedit-mode)
(use-package! consult
:bind
;; swiper muscle-memory
("M-s l" . consult-line))
(add-to-dk-keymap
'(("<SPC>" . deadgrep)
;; Project content search. ripgrep automatically understands .gitignore
("g" . consult-ripgrep)
;; Project file search.
("j" . consult-projectile)
("i" . consult-imenu)
("l" . consult-buffer)
("o" . consult-outline)))
(global-set-key [remap yank-pop] 'consult-yank-pop)
(use-package! ctrlf
:init
(ctrlf-mode +1))
(use-package! deadgrep)
Edit results with deadgrep-edit-mode
(replaces wgrep). Save changes with save-some-buffers
(C-x s !
).
This is one of my primary ways of navigating next: jump through other occurances of the text currently under the cursor.
(use-package! smartscan
:init (global-smartscan-mode 1)
:bind (("M-N" . smartscan-symbol-go-forward)
("M-P" . smartscan-symbol-go-backward)
:map smartscan-map
("M-p" . nil)
("M-n" . nil)))
Disable version control when using TRAMP to avoid extra delays
(setq vc-ignore-dir-regexp
(format "\\(%s\\)\\|\\(%s\\)"
vc-ignore-dir-regexp
tramp-file-name-regexp))
Stunningly useful.
(use-package! magit
:config
(set-default 'magit-stage-all-confirm nil)
(set-default 'magit-unstage-all-confirm nil)
(remove-hook 'magit-mode-hook 'turn-on-magit-gitflow)
;; Restores "normal" behavior in branch view (when hitting RET)
(setq magit-visit-ref-behavior '(create-branch checkout-any focus-on-ref))
(setq git-commit-finish-query-functions nil)
(setq magit-visit-ref-create 1)
(setq magit-revision-show-gravatars nil))
(setq dired-omit-extensions nil)
(after! dired
(remove-hook 'dired-mode-hook 'dired-omit-mode)
(setq dired-listing-switches "-aBhlv --group-directories-first"
dired-dwim-target t
dired-recursive-copies (quote always)
dired-recursive-deletes (quote top)
;; Directly edit permisison bits!
wdired-allow-to-change-permissions t))
(use-package! dired-rsync
:bind (:map dired-mode-map
("C-c C-r" . dired-rsync)))
(use-package! dired-x)
;; Directly edit permission bits!
(setq wdired-allow-to-change-permissions t)
;; prevents horizontal splits when split-window-sensibly is used
(setq split-width-threshold nil)
(delete 'register-alist savehist-additional-variables)
(set-register ?h '(file . "~/Sync/home/config.org"))
(set-register ?r '(file . "~/Sync/resume/resume.tex"))
Load extra work config if the environment variable EMACS_WORK_MODE
is set.
(unless (getenv "EMACS_NON_WORK_MODE")
(load-file "/home/dan/Work/w/emacs/work-config.el")
(require 'work-config))
(map!
"M-p" (lambda () (interactive) (scroll-down 4))
"M-n" (lambda () (interactive) (scroll-up 4))
"C-h h" 'helpful-at-point
"C-h f" 'helpful-function
"C-h v" 'helpful-variable
"C-h k" 'helpful-key
"M-SPC" 'avy-goto-word-or-subword-1
"C-S-d" 'my/duplicate-line-or-region
"C-c <left>" 'winner-undo
"C-c <right>" 'winner-redo
"C-+" 'text-scale-increase
"C--" 'text-scale-decrease
"C-<f5>" 'my/night-mode
"C-<f6>" 'my/day-mode
"C-z" 'undo-fu-only-undo
"C-S-z" 'undo-fu-only-redo
"C-/" 'undo-fu-only-undo
"C-?" 'undo-fu-only-redo
"C-x C-z" nil)
;; remove binding for suspend-frame
;; (global-set-key [remap goto-line] 'goto-line-with-feedback)
;; (global-set-key [remap goto-line] 'goto-line-with-feedback)
(defun pause-greenclip-daemon ()
(shell-command "ps axf | grep 'greenclip daemon' | grep -v grep | awk '{print $1}' | xargs kill -20"))
(defun resume-greenclip-daemon ()
(shell-command "greenclip print ' ' && ps axf | grep 'greenclip daemon' | grep -v grep | awk '{print $1}' | xargs kill -18"))
(defadvice password-store-copy (around pause-and-resume-greenclip activate)
"Pause the greenclip daemon before saving the password to the kill ring, then resume the daemon after saving."
(pause-greenclip-daemon)
ad-do-it
(run-with-idle-timer 10 1 #'resume-greenclip-daemon)
)
(doom/open-scratch-buffer nil nil t)
(set-company-backend! 'text-mode nil)
(defun my/file-local-p (f)
(not (file-remote-p f)))
(after! recentf
(add-to-list 'recentf-keep 'my/file-local-p))
;; (setq warning-minimum-level :emergency)
;; (when doom-debug-p
;; (require 'benchmark-init)
;; (add-hook 'doom-first-input-hook #'benchmark-init/deactivate))
(setq async-shell-command-buffer 'new-buffer)
; (add-to-list 'auto-mode-alist '("\\.eps\\'" . doc-view-minor-mode))
;; all backup and autosave files in the tmp dir
(setq backup-directory-alist
`((".*" . ,temporary-file-directory)))
(setq auto-save-file-name-transforms
`((".*" ,temporary-file-directory t)))
;; Coordinate between kill ring and system clipboard
(setq save-interprogram-paste-before-kill t)
;; (setq eshell-history-file-name (concat doom-private-dir "eshell-history"))
;; This is dangerous, but reduces the annoying step of confirming local variable settings each time
;; a file with a "Local Variables" clause (like many Org files) is opened.
(setq-default enable-local-variables :all)
;; This is usually just annoying
(setq compilation-ask-about-save nil)
;; No confirm on exit
(setq confirm-kill-emacs nil)
;; Alternative to calling save-buffers-kill-emacs, since
;; a) Muscle memory sends me to "kill-emacs" via fj-q-q
;; b) save-buffers-kill-emacs sometimes fails
;; This way, we try to save things, but quit in any case.
(defun my/save-ignore-errors ()
(ignore-errors
(save-some-buffers)))
(add-hook 'kill-emacs-hook 'my/save-ignore-errors)
;; Help out Projectile for remote files via TRAMP
;; https://sideshowcoder.com/2017/10/24/projectile-and-tramp/
(defadvice projectile-on (around exlude-tramp activate)
"This should disable projectile when visiting a remote file"
(unless (--any? (and it (file-remote-p it))
(list
(buffer-file-name)
list-buffers-directory
default-directory
dired-directory))
mad-do-it))
(setq projectile-mode-line "Projectile")
(setq password-store-password-length 20)
;; Truncate compiilation buffers, otherwise Emacs gets slow
;; https://stackoverflow.com/questions/11239201/can-i-limit-the-length-of-the-compilation-buffer-in-emacs
(add-hook 'compilation-filter-hook 'comint-truncate-buffer)
(setq comint-buffer-maximum-size 2000)
(setq recentf-max-saved-items 10000)
(after! vterm
(setq vterm-max-scrollback 100000
vterm-copy-exclude-prompt t))
(customize-set-variable 'vterm-buffer-name-string nil)
;; Emacs 28: Hide commands in M-x which do not work in the current mode.
;; Vertico commands are hidden in normal buffers.
(setq read-extended-command-predicate
#'command-completion-default-include-p)
(defun crm-indicator (args)
(cons (concat "[CRM] " (car args)) (cdr args)))
(advice-add #'completing-read-multiple :filter-args #'crm-indicator)
(defun my-compilation-mode-hook ()
(visual-line-mode 1))
(add-hook 'compilation-mode-hook 'my-compilation-mode-hook)
(use-package! real-auto-save
:hook
(prog-mode . real-auto-save-mode)
(org-mode . real-auto-save-mode))
(use-package! fancy-dabbrev
:hook
(prog-mode . fancy-dabbrev-mode)
(org-mode . fancy-dabbrev-mode)
:config
;; (setq fancy-dabbrev-preview-delay 0.1)
(setq fancy-dabbrev-preview-context 'before-non-word)
;; Let dabbrev searches ignore case and expansions preserve case:
(setq dabbrev-case-distinction nil)
(setq dabbrev-case-fold-search t)
(setq dabbrev-case-replace nil)
(add-hook 'minibuffer-setup-hook (lambda () (fancy-dabbrev-mode 0)))
(add-hook 'minibuffer-exit-hook (lambda () (fancy-dabbrev-mode 1))))
;;; funcs.el -*- lexical-binding: t; -*-
(defun my/open-literate-private-config-file ()
"Open the private config.org file."
(interactive)
(find-file (expand-file-name "config.org" doom-private-dir)))
(defun my/rot13-and-kill-region ()
(interactive)
(kill-new (rot13
(buffer-substring (region-beginning) (region-end)))))
(defun my/org-export-subtree-as-markdown-and-copy ()
(interactive)
(save-window-excursion
(let ((export-buffer (org-md-export-as-markdown nil t nil)))
(with-current-buffer export-buffer
(clipboard-kill-ring-save (point-min) (point-max)))
(kill-buffer export-buffer))))
(defun goto-line-with-feedback ()
"Show line numbers temporarily, while prompting for the line number input"
(interactive)
(unwind-protect
(progn
(linum-mode 1)
(call-interactively 'goto-line))
(linum-mode -1)))
(defun split-window-horizontally-and-switch ()
(interactive)
(split-window-horizontally)
(other-window 1))
(defun split-window-vertically-and-switch ()
(interactive)
(split-window-vertically)
(other-window 1))
(defun my-increment-number-decimal
(&optional
arg)
"Increment the number forward from point by 'arg'."
(interactive "p*")
(save-excursion
(save-match-data
(let (inc-by field-width answer)
(setq inc-by
(if arg
arg
1))
(skip-chars-backward "0123456789")
(when (re-search-forward "[0-9]+" nil t)
(setq field-width (- (match-end 0)
(match-beginning 0)))
(setq answer (+ (string-to-number (match-string 0) 10) inc-by))
(when (< answer 0)
(setq answer (+ (expt 10 field-width) answer)))
(replace-match (format (concat "%0" (int-to-string field-width) "d") answer)))))))
(defun rev-other-window ()
(interactive)
(other-window -1))
(defun eshell-here ()
"Opens up a new shell in the directory associated with the
current buffer's file. The eshell is renamed to match that
directory to make multiple eshell windows easier."
(interactive)
(let* ((parent (if (buffer-file-name)
(file-name-directory (buffer-file-name))
default-directory))
(name (car (last (split-string parent "/" t)))))
(eshell "new")
(rename-buffer (concat "*eshell: " name "*"))
(insert (concat "ls"))
(eshell-send-input)))
;; https://www.emacswiki.org/emacs/CopyingWholeLines
(defun my/duplicate-line-or-region (&optional n)
"Duplicate current line, or region if active.
With argument N, make N copies.
With negative N, comment out original line and use the absolute value."
(interactive "*p")
(let ((use-region (use-region-p)))
(save-excursion
(let ((text (if use-region ; Get region if active, otherwise line
(buffer-substring (region-beginning) (region-end))
(prog1 (thing-at-point 'line)
(end-of-line)
(if (< 0 (forward-line 1)) ; Go to beginning of next line, or make a new one
(newline))))))
(dotimes (i (abs (or n 1))) ; Insert N times, or once if not specified
(insert text))))
(if use-region nil ; Only if we're working with a line (not a region)
(let ((pos (- (point) (line-beginning-position)))) ; Save column
(if (> 0 n) ; Comment out original with negative arg
(comment-region (line-beginning-position) (line-end-position)))
(forward-line 1)
(forward-char pos)))))
(defun my/org-ref-noter-link-from-arxiv (arxiv-number)
"Retrieve a pdf for ARXIV-NUMBER and save it to the default PDF dir.
Then, add a bibtex entry for the new file in the default bib
file. Then, create a new org-ref note heading for it (see
org-ref-create-notes-hook in packages.el to see it also creates
a property for org-noter). Finally, insert a descriptive link to
the note heading at point, using the paper title as the link
text.
"
(interactive "sarxiv number: ")
(let ((bibtex-dialect 'BibTeX))
(org-ref-save-all-bibtex-buffers)
(save-window-excursion
(arxiv-get-pdf-add-bibtex-entry arxiv-number
(car org-ref-default-bibliography)
org-ref-pdf-directory)
(org-ref-save-all-bibtex-buffers))
(let* ((parsed-entry (save-excursion
(with-temp-buffer
;; In case of dir-local path to references.bib
(hack-dir-local-variables-non-file-buffer)
(insert-file-contents (car org-ref-default-bibliography))
(bibtex-set-dialect (parsebib-find-bibtex-dialect) t)
(search-forward (format "{%s}" arxiv-number))
(bibtex-narrow-to-entry)
(bibtex-beginning-of-entry)
(bibtex-parse-entry)))))
(org-insert-heading)
(let* ((raw-ref-title (cdr (assoc "title" parsed-entry)))
(ref-title (s-replace-regexp (rx (sequence "\n" (+ space))) " "
(car (cdr (s-match (rx "{" (group (+ anything)) "}") raw-ref-title)))))
(ref-key (cdr (assoc "=key=" parsed-entry))))
(insert ref-title)
(insert "\n\n")
(insert (format "cite:%s" ref-key))))))
(defun my/night-mode ()
(interactive)
(load-theme 'doom-dark+ t)
(doom/reload-theme))
(defun my/day-mode ()
(interactive)
(load-theme 'doom-nord-light t)
(doom/reload-theme))
(defun narrow-or-widen-dwim (p)
"If the buffer is narrowed, it widens. Otherwise, it narrows intelligently.
Intelligently means: region, subtree, or defun, whichever applies
first.
With prefix P, don't widen, just narrow even if buffer is already
narrowed."
(interactive "P")
(declare (interactive-only))
(cond ((and (buffer-narrowed-p) (not p)) (widen))
((region-active-p)
(narrow-to-region (region-beginning) (region-end)))
((derived-mode-p 'org-mode) (org-narrow-to-subtree))
(t (narrow-to-defun))))
;; https://stackoverflow.com/questions/28727190/org-babel-tangle-only-one-code-block
(defun my/org-babel-tangle-block()
(interactive)
(let ((current-prefix-arg '(4)))
(call-interactively 'org-babel-tangle)))
(defun my/open-org-files-list ()
(delq nil
(mapcar (lambda (buffer)
(buffer-file-name buffer))
(org-buffer-list 'files t))))
(defun my/org-latex-toggle-recent ()
(when (looking-back (rx "$ "))
(save-excursion
(backward-char 1)
(org-toggle-latex-fragment))))
;; (add-hook 'org-mode-hook
;; (lambda ()
;; (org-cdlatex-mode)
;; (add-hook 'post-self-insert-hook #'my/org-latex-toggle-recent 'append 'local)))
(defun my/save-shebanged-file-as-executable ()
(and (save-excursion
(save-restriction
(widen)
(goto-char (point-min))
(save-match-data
(looking-at "^#!"))))
(not (file-executable-p buffer-file-name))
(shell-command (concat "chmod +x " buffer-file-name))
(message
(concat "Saved as script: " buffer-file-name))))
(add-hook 'after-save-hook #'my/save-shebanged-file-as-executable)
;; https://llazarek.com/2018/10/images-in-org-mode.html
(defun my/org-link-file-path-at-point ()
"Get the path of the file referred to by the link at point."
(let* ((org-element (org-element-context))
(is-subscript-p (equal (org-element-type org-element) 'subscript))
(is-link-p (equal (org-element-type org-element) 'link))
(is-file-p (equal (org-element-property :type org-element) "file")))
(when is-subscript-p
(user-error "Org thinks you're in a subscript. Move the point and try again."))
(unless (and is-link-p is-file-p)
(user-error "Not on file link"))
(expand-file-name (org-element-property :path org-element))))
(defun my/org-resize-image-at-point (&optional arg)
"Resize the image linked at point."
(interactive)
(let ((img (my/org-link-file-path-at-point))
(percent (read-number "Resize to what percentage of current size? ")))
(start-process "mogrify" nil "/usr/bin/mogrify"
"-resize"
(format "%s%%" percent)
img)))
(defun my/run-in-fresh-compilation (cmd dir)
(defun local-compile-buffer-namer (ignored)
(generate-new-buffer-name cmd))
(let* ((compilation-buffer-name-function #'local-compile-buffer-namer)
(compilation-ask-about-save nil)
(default-directory (if dir dir default-directory)))
(compile cmd)))
(defun my/publish-dangirsh.org ()
(interactive)
(let ((neurosys-org-file "/home/dan/repos/dangirsh.org/site/projects/neurosys.org")
(doom-org-file "/home/dan/repos/dangirsh.org/site/projects/doom-config.org"))
;; Hack: copy in the files - had issues hardlinking it.
(copy-file (concat neurosys/base-dir "README.org") neurosys-org-file t)
(copy-file (concat doom-private-dir "config.org") doom-org-file t)
(my/run-in-fresh-compilation "./publi.sh" "/home/dan/repos/dangirsh.org/")))
(defun my/org-get-title (path)
(save-window-excursion
;; A simple find-file didn't work when the original was narrowed
(with-temp-buffer
(insert-file-contents path)
(org-mode)
(cadr (assoc "TITLE" (org-collect-keywords '("title"))
#'string-equal)))))
(defun my/set-timezone ()
(interactive)
;; (shell-command "sudo timedatectl set-timezone America/Los_Angeles")
;; (shell-command "sudo timedatectl set-timezone America/New_York")
;; (shell-command "sudo timedatectl set-timezone Europe/Paris")
(shell-command "sudo timedatectl set-timezone Europe/Berlin")
)
;; (my/set-timezone)
(defun my/insert-jupyter-julia-block ()
(interactive)
(org-insert-structure-template "src jupyter-julia"))
(defun my/insert-jupyter-python-block ()
(interactive)
(org-insert-structure-template "src jupyter-python"))
;; https://emacs.stackexchange.com/questions/10091/sentence-in-text-is-read-only-even-though-the-buffer-is-not-how-to-fix-this/10093#10093
(defun my/set-region-read-only (begin end)
"Sets the read-only text property on the marked region.
Use `set-region-writeable' to remove this property."
;; See https://stackoverflow.com/questions/7410125
(interactive "r")
(with-silent-modifications
(put-text-property begin end 'read-only t)))
(defun my/set-region-writeable (begin end)
"Removes the read-only text property from the marked region.
Use `set-region-read-only' to set this property."
;; See https://stackoverflow.com/questions/7410125
(interactive "r")
(with-silent-modifications
(remove-text-properties begin end '(read-only t))))
(defun my/copy-yubikey-token (account-name)
"Expects ykman to be installed."
(interactive (list (completing-read "Account: " '("yubi" "yubi3") nil t)))
(kill-new (my/get-yubikey-token account-name)))
(defun my/get-yubikey-token (account-name)
"Expects ykman to be installed."
(format "%s"
(with-temp-buffer
(message "Touch Yubikey!")
(call-process-region (point-min) (point-max) "ykman" t t nil "oath" "code" account-name)
(let* ((output (buffer-string))
(cells (split-string output)))
(car (last cells))))))
(defun my/save-yubikey-token (account-name)
(let ((yubikey-token-file (format "/tmp/current-yubi-token/%s" account-name)))
(save-window-excursion
(find-file yubikey-token-file)
(erase-buffer)
(insert (my/get-yubikey-token account-name))
(save-buffer))
yubikey-token-file))
(defun my/run-in-vterm-kill (process event)
"A process sentinel. Kills PROCESS's buffer if it is live."
(let ((b (process-buffer process)))
(and (buffer-live-p b)
(kill-buffer b))))
;; https://www.reddit.com/r/emacs/comments/ft84xy/run_shell_command_in_new_vterm/
(defun my/run-in-vterm (command dir &optional term-name)
"Execute string COMMAND in a new vterm.
Like `async-shell-command`, but run in a vterm for full terminal features.
The new vterm buffer is named in the form `*foo bar.baz*`, the
command and its arguments in earmuffs.
When the command terminates, the shell remains open, but when the
shell exits, the buffer is killed."
(interactive)
;; Ensure the vterm is opened in the right directory
(let ((default-directory dir))
(with-current-buffer (vterm (if term-name term-name (format "*%s*" command)))
;; (set-process-sentinel vterm--process #'my/run-in-vterm-kill)
(vterm-send-string command)
(vterm-send-return))))
;; https://github.com/org-roam/org-roam/wiki/Hitchhiker's-Rough-Guide-to-Org-roam-V2#hiding-the-properties-drawer
(defun org-hide-properties ()
"Hide all org-mode headline property drawers in buffer. Could be slow if it has a lot of overlays."
(interactive)
(save-excursion
(goto-char (point-min))
(while (re-search-forward
"^ *:properties:\n\\( *:.+?:.*\n\\)+ *:end:\n" nil t)
(let ((ov_this (make-overlay (match-beginning 0) (match-end 0))))
(overlay-put ov_this 'display "")
(overlay-put ov_this 'hidden-prop-drawer t))))
(put 'org-toggle-properties-hide-state 'state 'hidden))
(defun org-show-properties ()
"Show all org-mode property drawers hidden by org-hide-properties."
(interactive)
(remove-overlays (point-min) (point-max) 'hidden-prop-drawer t)
(put 'org-toggle-properties-hide-state 'state 'shown))
(defun org-toggle-properties ()
"Toggle visibility of property drawers."
(interactive)
(if (eq (get 'org-toggle-properties-hide-state 'state) 'hidden)
(org-show-properties)
(org-hide-properties)))
(defun alist-get-nested (alist path)
(let ((result alist))
(dolist (key path)
(setq result (alist-get key result)))
result))
(defun my/edit-resume ()
(interactive)
(find-file "~/Sync/resume/resume.tex"))
(defun my/org-split-block ()
"Sensibly split the current Org block at point."
(interactive)
(if (my/org-in-any-block-p)
(save-match-data
(save-restriction
(widen)
(let ((case-fold-search t)
(at-bol (bolp))
block-start
block-end)
(save-excursion
(re-search-backward "^\\(?1:[[:blank:]]*#\\+begin_.+?\\)\\(?: .*\\)*$" nil nil 1)
(setq block-start (match-string-no-properties 0))
(setq block-end (replace-regexp-in-string
"begin_" "end_" ;Replaces "begin_" with "end_", "BEGIN_" with "END_"
(match-string-no-properties 1))))
;; Go to the end of current line, if not at the BOL
(unless at-bol
(end-of-line 1))
(insert (concat (if at-bol "" "\n")
block-end
"\n\n"
block-start
(if at-bol "\n" "")))
;; Go to the line before the inserted "#+begin_ .." line
(beginning-of-line (if at-bol -1 0)))))
(message "Point is not in an Org block")))
(defun my/org-in-any-block-p ()
"Return non-nil if the point is in any Org block.
The Org block can be *any*: src, example, verse, etc., even any
Org Special block.
This function is heavily adapted from `org-between-regexps-p'."
(save-match-data
(let ((pos (point))
(case-fold-search t)
(block-begin-re "^[[:blank:]]*#\\+begin_\\(?1:.+?\\)\\(?: .*\\)*$")
(limit-up (save-excursion (outline-previous-heading)))
(limit-down (save-excursion (outline-next-heading)))
beg end)
(save-excursion
;; Point is on a block when on BLOCK-BEGIN-RE or if
;; BLOCK-BEGIN-RE can be found before it...
(and (or (org-in-regexp block-begin-re)
(re-search-backward block-begin-re limit-up :noerror))
(setq beg (match-beginning 0))
;; ... and BLOCK-END-RE after it...
(let ((block-end-re (concat "^[[:blank:]]*#\\+end_"
(match-string-no-properties 1)
"\\( .*\\)*$")))
(goto-char (match-end 0))
(re-search-forward block-end-re limit-down :noerror))
(> (setq end (match-end 0)) pos)
;; ... without another BLOCK-BEGIN-RE in-between.
(goto-char (match-beginning 0))
(not (re-search-backward block-begin-re (1+ beg) :noerror))
;; Return value.
(cons beg end))))))
(defun my/org-meta-return (&optional arg)
"Insert a new heading or wrap a region in a table.
Calls `org-insert-heading', `org-insert-item',
`org-table-wrap-region', or `my/org-split-block' depending on
context. When called with an argument, unconditionally call
`org-insert-heading'."
(interactive "P")
(org-check-before-invisible-edit 'insert)
(or (run-hook-with-args-until-success 'org-metareturn-hook)
(call-interactively (cond (arg #'org-insert-heading)
((org-at-table-p) #'org-table-wrap-region)
((org-in-item-p) #'org-insert-item)
((my/org-in-any-block-p) #'my/org-split-block)
(t #'org-insert-heading)))))
;; https://emacs.stackexchange.com/questions/50649/jumping-from-a-source-block-to-the-tangled-file
(defun my/org-babel-tangle-jump ()
"Jump to tangle file for the source block at point."
(interactive)
(let (file org-babel-pre-tangle-hook org-babel-post-tangle-hook)
(cl-letf (((symbol-function 'write-region) (lambda (start end filename &rest _ignore)
(setq file filename)))
((symbol-function 'delete-file) #'ignore))
(org-babel-tangle '(4)))
(when file
(setq file (expand-file-name file))
(if (file-readable-p file)
(find-file file)
(error "Cannot open tangle file %S" file)))))
;; https://sachachua.com/blog/2019/07/tweaking-emacs-on-android-via-termux-xclip-xdg-open-syncthing-conflicts/
(defun my/org-archive-done-tasks (&optional scope)
"Archive finished or cancelled tasks.
SCOPE can be 'file or 'tree."
(interactive)
(beginning-of-buffer)
(org-map-entries
(lambda ()
(org-archive-subtree)
(setq org-map-continue-from (outline-previous-heading)))
"TODO=\"DONE\"|TODO=\"KILL\"" (or scope (if (org-before-first-heading-p) 'file 'tree))))
(defun my/org-titlify-link-or-noop ()
(interactive)
(org-beginning-of-line)
(kill-line)
(condition-case nil
(progn
(org-cliplink)
(sleep-for 5))
(error (yank))))
(defun my/org-jupyter-execute-subtree-by-id (id)
(save-window-excursion
(org-id-goto id)
(save-excursion
(org-narrow-to-subtree)
(end-of-buffer)
(jupyter-org-execute-to-point nil)
(widen))))
Any desired package not declared in a Doom module must be declared here. This seems redundant given the corresponding use-package!
declarations, but required by Doom (presumably for lazy loading).
;; -*- no-byte-compile: t; -*-
;;; $DOOMDIR/packages.el
(package! ace-window)
(package! aggressive-indent)
(package! consult-projectile :recipe (:host gitlab :repo "OlMon/consult-projectile"))
(package! copilot
:recipe (:host github :repo "copilot-emacs/copilot.el" :files ("*.el")))
(package! ctrlf)
(package! deadgrep)
(package! dired-rsync)
(package! elegant-agenda-mode :recipe (:host github :repo "justinbarclay/elegant-agenda-mode"))
(package! fancy-dabbrev)
(package! google-this)
;; (package! greader)
(package! gptel)
(package! helpful)
(package! key-chord)
(package! org-ai)
(package! org-cliplink)
(package! org-roam :recipe (:host github :repo "jethrokuan/org-roam"))
(package! org-super-agenda)
(package! phi-search)
(package! real-auto-save)
(package! rust-mode)
(package! smartscan)
(package! teleport)
(package! topsy)
(package! undo-tree)
(package! wrap-region)
;; (package! whisper :recipe (:local-repo "~/repos/whisper.el/"))