dotfiles/old-emacs/init.org
2021-03-03 11:14:56 -05:00

7120 lines
244 KiB
Org Mode
Executable File

#+PROPERTY: header-args :results silent
#+PROPERTY: header-args:emacs-lisp :lexical t :tangle ~/.emacs.d/config/base.el
This is a literate init file holding my Emacs configuration. It is
initially loaded by a [[file:init.el][bootstrap file]] that lives at ~/.emacs.d/init.el.
* Prelude
Enables lexical binding for everything in init.el:
#+BEGIN_SRC emacs-lisp
;;; -*- lexical-binding: t; -*-
#+END_SRC
** Garbage collection
Some GC tweaks [[https://github.com/hlissner/doom-emacs/blob/develop/docs/faq.org#how-does-doom-start-up-so-quickly]["borrowed" from Doom emacs]].
Turn off GC during init and restore it afterwards:
#+BEGIN_SRC emacs-lisp
(setq gc-cons-threshold most-positive-fixnum
gc-cons-percentage 0.6)
(add-hook 'emacs-startup-hook
(lambda ()
(setq gc-cons-threshold 100000000
gc-cons-percentage 0.1)))
#+END_SRC
Also suppress GC for 1 second after the minibuffer is active to avoid stuttering autocompletion and other GC hangups:
#+BEGIN_SRC emacs-lisp
(defun defer-garbage-collection ()
(setq gc-cons-threshold most-positive-fixnum))
(defun restore-garbage-collection ()
(run-at-time
1 nil (lambda () (setq gc-cons-threshold 100000000))))
(add-hook 'minibuffer-setup-hook #'defer-garbage-collection)
(add-hook 'minibuffer-exit-hook #'restore-garbage-collection)
#+END_SRC
** Unset file-handler-alist during initialization
Another optimization from [[https://github.com/hlissner/doom-emacs/blob/develop/docs/faq.org#how-does-doom-start-up-so-quickly][Doom Emacs]].
#+BEGIN_SRC emacs-lisp
(defvar file-name-handler-alist-backup file-name-handler-alist)
(setq file-name-handler-alist nil)
(add-hook 'emacs-startup-hook
(lambda ()
(setq file-name-handler-alist file-name-handler-alist-backup)))
#+END_SRC
** Variables
#+BEGIN_SRC emacs-lisp
(setq vc-follow-symlinks t
frame-resize-pixelwise t
tab-always-indent 'complete
enable-recursive-minibuffers t
read-process-output-max (* 1024 1024)
bookmark-save-flag 1)
(setq-default indent-tabs-mode nil)
#+END_SRC
* Default directory
#+BEGIN_SRC emacs-lisp
(cd "~")
#+END_SRC
* Packages
Load [[https://github.com/raxod502/straight.el][straight.el]] to manage package installation:
#+BEGIN_SRC emacs-lisp
(defvar bootstrap-version)
(unless (boundp 'bootstrapping-init)
(let ((bootstrap-file
(expand-file-name "straight/repos/straight.el/bootstrap.el" user-emacs-directory))
(bootstrap-version 5))
(unless (file-exists-p bootstrap-file)
(with-current-buffer
(url-retrieve-synchronously
"https://raw.githubusercontent.com/raxod502/straight.el/develop/install.el"
'silent 'inhibit-cookies)
(goto-char (point-max))
(eval-print-last-sexp)))
(load bootstrap-file nil 'nomessage)))
#+END_SRC
`use-package` is a macro that simplifies installing and loading packages.
#+BEGIN_SRC emacs-lisp
(straight-use-package 'use-package)
(setq straight-use-package-by-default t)
#+END_SRC
** Utility functions
#+BEGIN_SRC emacs-lisp
(defun find-library-readme (library)
(interactive (list (read-library-name)))
(let* ((dir (file-name-directory (file-truename (find-library-name library))))
(doc (car (directory-files dir t "\\(readme\\|README\\)\\..*"))))
(if (not doc)
(error "No README found")
(find-file doc)
(when (eq major-mode 'markdown-mode)
(markdown-view-mode)))))
#+END_SRC
* Benchmarking
`benchmark-init` does what it says on the box. This sets it up to benchmark my init time and then disable benchmarking after init completes.
#+BEGIN_SRC emacs-lisp
(use-package benchmark-init
:config
(add-hook 'after-init-hook 'benchmark-init/deactivate))
#+END_SRC
* General
Better keybinding.
#+BEGIN_SRC emacs-lisp
(use-package general
:init
(setq general-override-states '(insert
emacs
hybrid
normal
visual
motion
operator
replace)))
#+END_SRC
* Which-key
`which-key` makes keybindings discoverable.
#+BEGIN_SRC emacs-lisp
(use-package which-key
:config
(which-key-mode))
#+END_SRC
This function defines a prefix group for `which-key` so that it doesn't display `prefix`.
#+BEGIN_SRC emacs-lisp
(defun jdormit/define-prefix (binding name)
(which-key-add-key-based-replacements
(concat leader " " binding)
name)
(which-key-add-key-based-replacements
(concat "," " " binding)
name))
#+END_SRC
* Evil Mode
Because I like modal editing and dislike RSI.
#+BEGIN_SRC emacs-lisp
(use-package evil
:init
(setq evil-want-keybinding nil)
:config
(evil-mode 1))
#+END_SRC
Make undo not undo paragraphs at a time:
#+BEGIN_SRC emacs-lisp
(setq evil-want-fine-undo t)
#+END_SRC
Add a convenience function for making buffer-local ex-commands:
#+BEGIN_SRC emacs-lisp
(defun evil-ex-define-local-cmd (cmd function)
(set (make-local-variable 'evil-ex-commands) (copy-tree evil-ex-commands))
(evil-ex-define-cmd cmd function))
#+END_SRC
** evil-collection
A collection of evil bindings for various modes
#+BEGIN_SRC emacs-lisp
(use-package evil-collection
:after (evil)
:hook ((after-init . evil-collection-init))
:config
(setq evil-collection-company-use-tng nil))
#+END_SRC
** leader key
Use the spacebar as a leader key in evil-mode's normal state and in various other modes:
#+BEGIN_SRC emacs-lisp
(defconst leader "SPC")
(general-define-key
:keymaps 'override
:states '(normal visual motion)
"SPC" nil)
(general-create-definer leader-def-key
:keymaps 'override
:states '(normal visual motion)
:prefix leader
:prefix-map 'leader-map)
#+END_SRC
#+BEGIN_SRC emacs-lisp
(jdormit/define-prefix "?" "help")
(leader-def-key "?" help-map)
#+END_SRC
** evil-snipe
#+BEGIN_SRC emacs-lisp
(use-package evil-snipe
:after (evil)
:config
(evil-snipe-mode 1)
(evil-snipe-override-mode 1)
(add-to-list 'evil-snipe-disabled-modes 'dired-mode)
(add-to-list 'evil-snipe-disabled-modes 'structlog-mode)
(with-eval-after-load 'magit
(add-hook 'magit-mode-hook 'turn-off-evil-snipe-override-mode))
(with-eval-after-load 'prodigy
(add-hook 'prodigy-mode-hook 'turn-off-evil-snipe-mode))
(with-eval-after-load 'pass
(add-hook 'pass-mode-hook 'turn-off-evil-snipe-mode)))
#+END_SRC
** evil-commentary
Adds Evil commands to comment out lines of code:
#+BEGIN_SRC emacs-lisp
(use-package evil-commentary
:after (evil)
:hook ((prog-mode . evil-commentary-mode)))
#+END_SRC
** Additional keybindings
#+BEGIN_SRC emacs-lisp
(general-def 'normal "zM" #'hs-hide-level :keymaps 'override)
(general-def 'normal "z=" #'text-scale-increase :keymaps 'override)
(general-def 'normal "z-" #'text-scale-decrease :keymaps 'override)
(general-def 'normal "z0" #'text-scale-adjust :keymaps 'override)
(general-def 'normal view-mode-map "0" nil :keymaps 'override)
(general-def 'normal prodigy-view-mode-map "0" nil :keymaps 'override)
(general-def 'normal messages-buffer-mode-map "SPC" leader-map :keymaps 'override)
#+END_SRC
* Hydra
[[https://github.com/abo-abo/hydra][Hydra]]s are convenient keybinding menus.
#+BEGIN_SRC emacs-lisp
(use-package hydra
:defer t)
#+END_SRC
* Syncthing
I put lots of stuff in Syncthing, but the actual folder location
differs on my different computers. This function resolves to the
Syncthing directory:
#+BEGIN_SRC emacs-lisp
(defcustom syncthing-path (expand-file-name "~/Sync")
"The absolute path to the Syncthing directory"
:type 'directory)
(defun syncthing-directory (&optional path)
(f-join syncthing-path (s-chop-prefix (f-path-separator) (or path ""))))
#+END_SRC
* Emacs Lisp
** Packages
Some helpful ELisp packages:
- [[https://github.com/kiwanami/emacs-deferred][deferred]] is an async API library
- [[https://github.com/magnars/s.el][s.el]] is a string manipulation library
- [[https://github.com/magnars/dash.el][dash.el]] is a list manipulation library
- [[https://github.com/rejeep/f.el][f.el]] is a file manipulation library
- [[https://github.com/tkf/emacs-request][request]] is an HTTP library
#+BEGIN_SRC emacs-lisp
(use-package deferred
:commands (deferred:next
deferred:nextc
deferred:error
deferred:cancel
deferred:watch
deferred:wait
deferred:$
deferred:loop
deferred:parallel
deferred:earlier
deferred:call
deferred:apply
deferred:process
deferred:process-buffer
deferred:wait-idle
deferred:url-retrieve
deferred:url-get
deferred:url-post
deferred:new
deferred:succeed
deferred:fail
deferred:callback
deferred:callback-post
deferred:errorback
deferred:errorback-post
deferred:try
deferred:timeout
deferred:process))
(use-package s
:defer t
:init
(add-hook 'emacs-startup-hook (lambda () (require 's)))
:config
(defun camel-case (&optional arg)
(interactive "P")
(let* ((bounds (bounds-of-thing-at-point 'symbol))
(word (buffer-substring (car bounds) (cdr bounds)))
(camel (if arg (s-upper-camel-case word)
(s-lower-camel-case word))))
(delete-region (car bounds) (cdr bounds))
(insert camel))))
(use-package dash
:defer t
:init
(add-hook 'emacs-startup-hook (lambda () (require 'dash))))
(use-package dash-functional
:defer t
:init
(add-hook 'emacs-startup-hook (lambda () (require 'dash-functional))))
(use-package f
:defer t
:init
(add-hook 'emacs-startup-hook (lambda () (require 'f)))
(autoload 'f-join "f"))
(use-package request
:commands (request request-deferred))
(use-package ht
:defer t)
#+END_SRC
** Editing Elisp
#+BEGIN_SRC emacs-lisp
(general-def '(normal motion) emacs-lisp-mode-map "C-c C-c" #'eval-defun :keymaps 'override)
(general-def '(normal motion insert) lisp-interaction-mode-map "C-c C-c" #'eval-print-last-sexp :keymaps 'override)
(add-hook 'ielm-mode-hook 'smartparens-strict-mode)
#+END_SRC
** Load path
For machine or user specific libraries:
#+BEGIN_SRC emacs-lisp
(add-to-list 'load-path (expand-file-name "~/site-lisp"))
#+END_SRC
And for global ones:
#+BEGIN_SRC emacs-lisp
(add-to-list 'load-path "/usr/local/share/emacs/site-lisp")
#+END_SRC
** Utilities
Reading a file as a string:
#+BEGIN_SRC emacs-lisp
(defun read-file (path)
"Returns the contents of the file as a string"
(with-temp-buffer
(insert-file-contents path)
(buffer-string)))
#+END_SRC
Opening a file as sudo:
#+BEGIN_SRC emacs-lisp
(defun sudo-find-file (file-name)
"Like find file, but opens the file as root."
(interactive "F")
(let ((tramp-file-name (concat "/sudo::" (expand-file-name file-name))))
(find-file tramp-file-name)))
#+END_SRC
Recursive =assoc= for nested alists:
#+BEGIN_SRC emacs-lisp
(defun assoc-recursive (alist &rest keys)
"Recursively find KEYs in ALIST."
(while keys
(setq alist (cdr (assoc (pop keys) alist))))
alist)
#+END_SRC
Format a millis timestamp into human-readable form:
#+BEGIN_SRC emacs-lisp
(defun format-epoch-millis (millis)
(interactive "nTimestamp: ")
(message (format-time-string "%F %r" (/ millis 1000))))
#+END_SRC
The same but for seconds:
#+BEGIN_SRC emacs-lisp
(defun format-epoch-seconds (seconds)
(interactive "nTimestamp: ")
(message (format-time-string "%F %r" seconds)))
#+END_SRC
Checking if a buffer contains a string:
#+BEGIN_SRC emacs-lisp
(defun buffer-contains-substring (string)
(save-excursion
(save-match-data
(goto-char (point-min))
(search-forward string nil t))))
#+END_SRC
Pretty-print JSON:
#+BEGIN_SRC emacs-lisp
(defun pprint-json (raw-json)
(with-temp-buffer
(insert raw-json)
(json-pretty-print (point-min) (point-max))
(buffer-substring (point-min) (point-max))))
#+END_SRC
Load environment variables into Emacs from a shell script:
#+BEGIN_SRC emacs-lisp
(cl-defun extract-vars-from-env-file (file &key dir)
"Extracts an alist of variable name to value from
a bash script that exports environment variables."
(let ((file (expand-file-name file))
(var-re "\\(.+?\\)=\\(.+\\)$")
(env '()))
(with-temp-buffer
(cd (expand-file-name (or dir (file-name-directory file))))
(insert (shell-command-to-string (concat "source "
(shell-quote-argument file)
" > /dev/null && env")))
(goto-char (point-min))
(save-match-data
(while (re-search-forward var-re nil t)
(push (cons (match-string 1) (match-string 2)) env))))
env))
(defun source-env-file (file)
(interactive "fFile: \n")
(let ((env (extract-vars-from-env-file file)))
(dolist (binding env)
(setenv (car binding) (cdr binding)))))
(cl-defmacro with-env-from-file (file &rest body)
(declare (indent 1))
(let ((env-var (make-symbol "the-env"))
(path-var (make-symbol "the-path")))
`(let* ((,env-var (extract-vars-from-env-file ,file))
(,path-var (assoc "PATH" ,env-var))
(exec-path
(if ,path-var
(append (split-string (cdr ,path-var) ":") exec-path)
exec-path))
(process-environment
(append
(mapcar
(lambda (elt) (format "%s=%s" (car elt) (cdr elt)))
,env-var)
process-environment)))
,@body)))
(cl-defun call-with-env-from-file (file callback &key dir)
(let* ((env (extract-vars-from-env-file file :dir dir))
(path (assoc "PATH" env))
(exec-path
(if path
(append (split-string (cdr path) ":") exec-path)
exec-path))
(process-environment
(append (mapcar (lambda (elt) (format "%s=%s" (car elt) (cdr elt))) env)
process-environment)))
(funcall callback)))
(defmacro with-env (env &rest body)
(declare (indent 1))
`(let* ((process-environment
(append
(mapcar
(lambda (elt) (format "%s=%s" (car elt) (cdr elt)))
,env)
process-environment)))
,@body))
#+END_SRC
Convenience macro to run some code in a particular default-directory:
#+BEGIN_SRC emacs-lisp
(defmacro with-default-directory (dir &rest body)
(declare (indent 1))
`(let ((default-directory ,dir))
,@body))
#+END_SRC
#+BEGIN_SRC emacs-lisp
(defun random-alnum (&optional n)
(let* ((n-chars (or n 1))
(alnum "abcdefghijklmnopqrstuvwxyz0123456789")
(result ""))
(dotimes (_ n-chars result)
(let ((i (% (abs (random)) (length alnum))))
(setq result
(concat result (substring alnum i (1+ i))))))))
#+END_SRC
A handy function to colorize a buffer with ANSI escape characters in it:
#+BEGIN_SRC emacs-lisp
(defun ansi-color (&optional begin end)
(interactive)
(let ((begin (or begin (point-min)))
(end (or end (point-max)))
(inhibit-read-only t))
(ansi-color-apply-on-region begin end)))
#+END_SRC
** Persisting variables between session
The idea behind this is pretty simple - variables get persisted in ~/.emacs.d/<persisted-vars-file> as a plist of (variable-name variable-value).
#+BEGIN_SRC emacs-lisp
(defvar persisted-vars-file "~/.emacs.d/persisted-vars")
#+END_SRC
This function retrieves the plist of persisted variables or nil if it doesn't exist:
#+BEGIN_SRC emacs-lisp
(defun get-persisted-plist ()
(let ((file (expand-file-name persisted-vars-file)))
(when (file-exists-p file)
(let ((vars-plist-str (read-file file)))
(unless (string= vars-plist-str "")
(car (read-from-string vars-plist-str)))))))
#+END_SRC
This function retrieves a persisted variable:
#+BEGIN_SRC emacs-lisp
(defun get-persisted-var (var-name)
"Retrieves the value of persisted variable `var-name`, or nil if not found"
(let ((vars-plist (get-persisted-plist)))
(when vars-plist
(plist-get vars-plist var-name))))
#+END_SRC
And this function persists a variable:
#+BEGIN_SRC emacs-lisp
(defun persist-variable (var-name value)
(let ((file (expand-file-name persisted-vars-file))
(vars-plist (get-persisted-plist)))
(if vars-plist
(progn
(plist-put vars-plist var-name value)
(write-region
(prin1-to-string vars-plist) nil file))
(let ((vars-plist `(,var-name ,value)))
(write-region
(prin1-to-string vars-plist) nil file)))))
#+END_SRC
** Process handling
Some utilities for calling out to other processes.
#+BEGIN_SRC emacs-lisp
(defun make-process-sentinel (success err)
"Makes a process sentinel that calls `success` on success and `err` on error"
(lambda (proc event)
(cond ((string-match-p "finished" event) (funcall success))
(t (funcall err)))))
(defun make-success-err-msg-sentinel (buf success-msg err-msg &optional kill-on-err)
(make-process-sentinel
(lambda ()
(message success-msg)
(kill-buffer buf))
(lambda ()
(message err-msg)
(when kill-on-err
(kill-buffer buf)))))
#+END_SRC
A function to call a process passing some string as stdin and returning the process output:
#+BEGIN_SRC emacs-lisp
(defun make-process-fn (program &rest args)
"Returns a function that, when called, call `program` with arguments `args`,
passing the function argument as stdin"
(lambda (&optional input)
(with-temp-buffer
(if input
(progn
(insert input)
(apply #'call-process-region (point-min) (point-max) program t t nil args))
(apply #'call-process program nil t nil args))
(buffer-substring-no-properties (point-min) (point-max)))))
#+END_SRC
The same function but for commands that need to run in a shell:
#+BEGIN_SRC emacs-lisp
(defun make-shell-fn (program &rest args)
"Returns a function that, when called, calls `program` in a shell
with arguments `args`, passing the function argument as stdin"
(lambda (&optional input)
(let ((cmd (combine-and-quote-strings `(,program ,@args))))
(with-temp-buffer
(if input
(progn
(insert input)
(call-shell-region (point-min) (point-max) cmd t t))
(call-process-shell-command cmd nil t))
(buffer-substring-no-properties (point-min) (point-max))))))
#+END_SRC
Running a shell command as sudo:
#+BEGIN_SRC emacs-lisp
(defun sudo-shell-command (command)
(with-temp-buffer
(cd "/sudo::/")
(shell-command command)))
#+END_SRC
** Buffer switch hooks
I want to be able to run code whenever I switch to a buffer running certain modes. The code to run is stored in a alist mapping mode names to lists of code to run (stored as a raw data structure to be eval'ed):
#+BEGIN_SRC emacs-lisp
(defvar buffer-mode-hooks '())
#+END_SRC
To add a new hook, push the code to run onto the correct list:
#+BEGIN_SRC emacs-lisp
(defun add-buffer-mode-hook (mode fn)
(if-let ((existing-entry (assoc mode buffer-mode-hooks)))
(push fn (cdr existing-entry))
(let ((new-entry `(,mode . (,fn))))
(push new-entry buffer-mode-hooks))))
#+END_SRC
Whenever the buffer changes, look up the major-mode to see if there is any code to run:
#+BEGIN_SRC emacs-lisp
(defun run-buffer-mode-hooks ()
(when-let ((entry (assoc major-mode buffer-mode-hooks)))
(dolist (fn (cdr entry))
(funcall fn))))
(add-hook 'buffer-list-update-hook #'run-buffer-mode-hooks)
#+END_SRC
** Aliases
#+BEGIN_SRC emacs-lisp
(defalias 'doc 'describe-symbol)
#+END_SRC
** Miscellaneous
#+BEGIN_SRC emacs-lisp
(setq warning-suppress-types
'((undo discard-info)))
#+END_SRC
* Org Mode
Notes, agenda, calendar, blogging, journaling, etc.
Loaded early to [[https://github.com/raxod502/straight.el#the-wrong-version-of-my-package-was-loaded][avoid a version clash]].
First, a function to get my notes directory:
#+BEGIN_SRC emacs-lisp
(defun org-directory (&optional path)
"Returns the directory for my org notes, appends PATH if given"
(f-join (expand-file-name "~/org")
(s-chop-prefix (f-path-separator) (or path ""))))
#+END_SRC
#+BEGIN_SRC emacs-lisp
(use-package org
:straight org-plus-contrib
:commands (org-element-map
org-agenda
org-capture)
:mode (("\\.org\\'" . org-mode))
:init
(jdormit/define-prefix "o" "org")
(leader-def-key "oa" 'org-agenda)
(leader-def-key "oc" 'org-capture)
:config
;; Disable expensive hooks when building agenda buffer
(advice-add 'org-get-agenda-file-buffer :around
(lambda (oldfn &rest args)
(let ((find-file-hook '())
(org-mode-hook '())
(after-change-major-mode-hook '()))
(apply oldfn args))))
(defun agenda-files (&optional file)
(let ((agenda-dir (org-directory)))
(if file
(concat (file-name-as-directory agenda-dir) file)
agenda-dir)))
(setq org-src-fontify-natively t
org-ellipsis ""
org-directory (org-directory)
org-link-elisp-confirm-function 'y-or-n-p
org-startup-with-inline-images t
org-return-follows-link t
org-log-done 'time
org-file-apps '(("log" . emacs)
(auto-mode . emacs)
(directory . emacs)
("\\.pdf\\'" . emacs)
("\\.mm\\'" . default)
("\\.x?html?\\'" . default))
org-agenda-files (list (agenda-files))
org-capture-templates `(("L" "Lola task" entry
(file+headline ,(agenda-files "todo.org") "Lola")
"* TODO %i%?")
("p" "Personal task" entry
(file+headline ,(agenda-files "todo.org") "Personal")
"* TODO %i%?")
("n" "Note" entry
(file ,(agenda-files "notes.org"))
"* %^{Description}\n%i%?")
("l" "Log" entry
(file ,(agenda-files "log.org"))
"* %<%Y-%m-%d %H:%M:%S>%?"))
org-refile-use-outline-path 'file
org-refile-targets `((org-agenda-files :level . 0)
(,(agenda-files "notes.org") :level . 1)
(,(agenda-files "todo.org") :level . 1))
org-todo-keywords '((sequence
"TODO(t)"
"IN PROGRESS(i)"
"BLOCKED(b)"
"|"
"DONE(d)"
"CANCELLED(c)"))
org-agenda-todo-ignore-scheduled 'future
org-agenda-tags-todo-honor-ignore-options t
org-agenda-span 'day
org-agenda-custom-commands
'(("L" "Lola" ((tags-todo "@lola")))
("t" "TODOs"
((agenda)
(alltodo)))))
(defun setup-org-mode ()
(require 'org-attach)
(org-display-inline-images nil t)
(org-redisplay-inline-images)
(auto-fill-mode))
(add-hook 'org-mode-hook #'setup-org-mode)
:general
(normal org-mode-map
"T" #'org-insert-todo-heading
"K" #'org-move-subtree-up
"J" #'org-move-subtree-down
"<return>" #'org-return
"TAB" #'org-cycle
"SPC" leader-map
"gn" #'org-next-link
"gp" #'org-previous-link)
(org-mode-map "C-c e" #'org-preview-latex-fragment)
(org-mode-map "C-c C-l" #'org-insert-link)
("C-c l" #'org-store-link))
#+END_SRC
** Evil-Org
Set up evil keybindings for Org mode:
#+BEGIN_SRC emacs-lisp
(use-package evil-org
:after (evil org)
:hook ((org-mode . evil-org-mode)
(org-agenda-mode . evil-org-mode))
:config
(add-hook 'evil-org-mode-hook
(lambda ()
(evil-org-set-key-theme
'(textobjects
insert
navigation
additional
shift
todo))
(general-def 'insert org-mode-map [backspace] 'org-delete-backward-char)))
(require 'evil-org-agenda)
(evil-org-agenda-set-keys)
(general-def 'motion org-agenda-mode-map "SPC" leader-map)
(general-def 'motion org-agenda-mode-map "gs" 'org-save-all-org-buffers)
(general-def '(normal motion) evil-org-mode-map
"C-S-j" nil
"C-S-k" nil
"C-S-h" nil
"C-S-l" nil))
#+END_SRC
** Org-mode hydra
A helpful agenda-mode hydra:
#+BEGIN_SRC emacs-lisp
(with-eval-after-load 'org-agenda
(defhydra hydra-org-agenda (:pre (setq which-key-inhibit t)
:post (setq which-key-inhibit nil)
:hint none)
"
Org agenda (_q_uit)
^Clock^ ^Visit entry^ ^Date^ ^Other^
^-----^---- ^-----------^------------ ^----^----------- ^-----^---------
_ci_ in _SPC_ in other window _ds_ schedule _gr_ reload
_co_ out _TAB_ & go to location _dd_ set deadline _._ go to today
_cq_ cancel _RET_ & del other windows _dt_ timestamp _gd_ go to date
_cj_ jump _o_ link _+_ do later ^^
^^ ^^ _-_ do earlier ^^
^^ ^^ ^^ ^^
^View^ ^Filter^ ^Headline^ ^Toggle mode^
^----^-------- ^------^--------------- ^--------^------- ^-----------^----
_vd_ day _ft_ by tag _ht_ set status _tf_ follow
_vw_ week _fr_ refine by tag _hk_ kill _tl_ log
_vt_ fortnight _fc_ by category _hr_ refile _ta_ archive trees
_vm_ month _fh_ by top headline _hA_ archive _tA_ archive files
_vy_ year _fx_ by regexp _h:_ set tags _tr_ clock report
_vn_ next span _fd_ delete all filters _hp_ set priority _td_ diaries
_vp_ prev span ^^ ^^ ^^
_vr_ reset ^^ ^^ ^^
^^ ^^ ^^ ^^
"
;; Entry
("hA" org-agenda-archive-default)
("hk" org-agenda-kill)
("hp" org-agenda-priority)
("hr" org-agenda-refile)
("h:" org-agenda-set-tags)
("ht" org-agenda-todo)
;; Visit entry
("o" link-hint-open-link :exit t)
("<tab>" org-agenda-goto :exit t)
("TAB" org-agenda-goto :exit t)
("SPC" org-agenda-show-and-scroll-up)
("RET" org-agenda-switch-to :exit t)
;; Date
("dt" org-agenda-date-prompt)
("dd" org-agenda-deadline)
("+" org-agenda-do-date-later)
("-" org-agenda-do-date-earlier)
("ds" org-agenda-schedule)
;; View
("vd" org-agenda-day-view)
("vw" org-agenda-week-view)
("vt" org-agenda-fortnight-view)
("vm" org-agenda-month-view)
("vy" org-agenda-year-view)
("vn" org-agenda-later)
("vp" org-agenda-earlier)
("vr" org-agenda-reset-view)
;; Toggle mode
("ta" org-agenda-archives-mode)
("tA" (org-agenda-archives-mode 'files))
("tr" org-agenda-clockreport-mode)
("tf" org-agenda-follow-mode)
("tl" org-agenda-log-mode)
("td" org-agenda-toggle-diary)
;; Filter
("fc" org-agenda-filter-by-category)
("fx" org-agenda-filter-by-regexp)
("ft" org-agenda-filter-by-tag)
("fr" org-agenda-filter-by-tag-refine)
("fh" org-agenda-filter-by-top-headline)
("fd" org-agenda-filter-remove-all)
;; Clock
("cq" org-agenda-clock-cancel)
("cj" org-agenda-clock-goto :exit t)
("ci" org-agenda-clock-in :exit t)
("co" org-agenda-clock-out)
;; Other
("q" nil :exit t)
("gd" org-agenda-goto-date)
("." org-agenda-goto-today)
("gr" org-agenda-redo))
(general-def '(normal visual motion insert emacs) org-agenda-mode-map "." 'hydra-org-agenda/body))
#+END_SRC
** Exporting
*** HTML
Export to HTML:
#+BEGIN_SRC emacs-lisp
(use-package htmlize
:commands htmlize-buffer)
#+END_SRC
Don't put section numbers in front of headers:
#+BEGIN_SRC emacs-lisp
(setq org-export-with-section-numbers nil)
#+END_SRC
Disable the preamble and postamble:
#+BEGIN_SRC emacs-lisp
(setq org-html-preamble nil
org-html-postamble nil)
#+END_SRC
Redefine org-html-src-block to wrap code blocks in <pre><code> and language class for use by highlight.js:
#+BEGIN_SRC emacs-lisp
(defun org-html-src-block (src-block _contents info)
"Transcode a SRC-BLOCK element from Org to HTML.
CONTENTS holds the contents of the item. INFO is a plist holding
contextual information."
(if (org-export-read-attribute :attr_html src-block :textarea)
(org-html--textarea-block src-block)
(let* ((lang (org-element-property :language src-block))
(code (org-html-format-code src-block info))
(label (let ((lbl (and (org-element-property :name src-block)
(org-export-get-reference src-block info))))
(if lbl (format " id=\"%s\"" lbl) "")))
(klipsify (and (plist-get info :html-klipsify-src)
(member lang '("javascript" "js"
"ruby" "scheme" "clojure" "php" "html")))))
(if (not lang) (format "<pre><code class=\"example\"%s>\n%s</code></pre>" label code)
(format "<div class=\"org-src-container\">\n%s%s\n</div>"
;; Build caption.
(let ((caption (org-export-get-caption src-block)))
(if (not caption) ""
(let ((listing-number
(format
"<span class=\"listing-number\">%s </span>"
(format
(org-html--translate "Listing %d:" info)
(org-export-get-ordinal
src-block info nil #'org-html--has-caption-p)))))
(format "<label class=\"org-src-name\">%s%s</label>"
listing-number
(org-trim (org-export-data caption info))))))
;; Contents.
(if klipsify
(format "<pre><code class=\"src src-%s\"%s%s>%s</code></pre>"
lang
label
(if (string= lang "html")
" data-editor-type=\"html\""
"")
code)
(format "<pre><code class=\"src %s src-%s\"%s>%s</code></pre>"
lang lang label code)))))))
#+END_SRC
*** Github-flavored markdown
#+BEGIN_SRC emacs-lisp
(use-package ox-gfm
:after (org)
:hook (org-mode . (lambda () (require 'ox-gfm))))
#+END_SRC
*** Jira
#+BEGIN_SRC emacs-lisp
(use-package ox-jira
:after (org)
:straight (:host github :repo "stig/ox-jira.el" :branch "trunk")
:hook (org-mode . (lambda () (require 'ox-jira))))
#+END_SRC
** org-babel
Literate programming!
#+BEGIN_SRC emacs-lisp
(add-hook 'org-mode-hook
(lambda ()
(org-babel-do-load-languages
'org-babel-load-languages
'((emacs-lisp . t)
(python . t)
(shell . t)
(clojure . t)
(lisp . t)
(scheme . t)
(java . t)
(js . t)
(dot . t)
(ditaa . t)
(ledger . t)
(sql . t)
(jq . t)
(restclient . t)
(plantuml . t)))))
#+END_SRC
Get rid of the confirmation prompt:
#+BEGIN_SRC emacs-lisp
(setq org-confirm-babel-evaluate nil)
#+END_SRC
Display inline images after executing a source block:
#+BEGIN_SRC emacs-lisp
(add-hook 'org-babel-after-execute-hook
(lambda ()
(org-display-inline-images nil t)
(org-redisplay-inline-images)))
#+END_SRC
Enable async source block execution. Note: this execute the contents of the source block in a separate Emacs process, so blocks that rely on anything defined in init.org or the current Emacs process won't work as expected.
#+BEGIN_SRC emacs-lisp
(use-package ob-async
:straight (ob-async :host github :repo "astahlman/ob-async")
:defer t
:hook (org-mode . (lambda () (require 'ob-async))))
#+END_SRC
Filter out the "u" from unicode results in org tabels returned from Python source blocks:
#+BEGIN_SRC emacs-lisp
(with-eval-after-load 'ob-python
(defun org-babel-python-table-or-string (results)
"Convert RESULTS into an appropriate elisp value.
If the results look like a list or tuple, then convert them into an
Emacs-lisp table, otherwise return the results as a string."
(let ((res (org-babel-script-escape results)))
(if (listp res)
(mapcar (lambda (el)
(cond
((eq el 'None) org-babel-python-None-to)
((listp el) (-filter (lambda (m) (not (eq m 'u))) el))
(t el)))
res)
res))))
#+END_SRC
** Images
#+BEGIN_SRC emacs-lisp
(setq org-image-actual-width nil)
#+END_SRC
** org-scratch
It's very useful to open a new org buffer for a quick org-babel exploration.
#+BEGIN_SRC emacs-lisp
(defun org-scratch (&optional make-new)
"Switches to an org-mode buffer not associated with any file.
If called with a prefix argument, always creates a new buffer;
otherwise, switches to the existing *org-scratch* buffer if it exists."
(interactive "P")
(let ((org-scratch-buffer-name
(generate-new-buffer-name
"*org-scratch*"
(unless make-new "*org-scratch*"))))
(switch-to-buffer org-scratch-buffer-name)
(org-mode)))
(leader-def-key "os" #'org-scratch)
#+END_SRC
** org-gcal
Integrate Google calendar with org-mode:
#+BEGIN_SRC emacs-lisp
(defun get-calendar-file (name)
(org-directory name))
(defun org-gcal-sync-advice (oldfn &rest args)
(deferred:nextc (apply oldfn args)
(lambda (_)
(dolist (entry org-gcal-fetch-file-alist)
(let ((file (cdr entry)))
(with-current-buffer (find-file-noselect file)
(save-buffer)))))))
(use-package org-gcal
:commands (org-gcal-sync
org-gcal-fetch
org-gcal-post-at-point
org-gcal-delete-at-point
org-gcal-request-token)
:config
(advice-add 'org-gcal-sync :around #'org-gcal-sync-advice)
(setq org-gcal-client-id (password-store-get "lola-org-gcal-client-id")
org-gcal-client-secret (password-store-get "lola-org-gcal-client-secret")
org-gcal-fetch-file-alist `(("jeremydormitzer@lola.com" . ,(get-calendar-file "lola-gcal.org"))
("jeremy.dormitzer@gmail.com" . ,(get-calendar-file "personal-gcal.org"))
("lut2o2moohg6qkdsto1qfq7th4@group.calendar.google.com" . ,(get-calendar-file "j-n-gcal.org")))
org-gcal-notify-p nil))
(defun org-agenda-redo-and-fetch-gcal (&optional all)
(interactive "P")
(let ((cb (if all #'org-agenda-redo-all #'org-agenda-redo)))
(deferred:nextc (org-gcal-fetch) cb)))
(with-eval-after-load 'org-agenda
(general-def '(normal motion) org-agenda-mode-map "gR" #'org-agenda-redo-and-fetch-gcal))
#+END_SRC
** Utilities
A function to get the TITLE property of the current org buffer:
#+BEGIN_SRC emacs-lisp
(defun org-get-title (&optional contents)
(let ((raw (or contents
(buffer-substring (point-min) (point-max)))))
(with-temp-buffer
(insert raw)
(car
(org-element-map
(org-element-parse-buffer)
'keyword
(lambda (el)
(when (string-match-p "TITLE"
(org-element-property :key el))
(org-element-property :value el))))))))
#+END_SRC
** org-present
An ultra-minimalist presentation mode for Org:
#+BEGIN_SRC emacs-lisp
(use-package org-present
:commands (org-present)
:config
(defun org-present-on ()
(org-present-big)
(org-display-inline-images)
(org-present-hide-cursor)
(display-line-numbers-mode -1)
(org-present-read-only))
(defun org-present-off ()
(org-present-small)
(org-present-show-cursor)
(unless org-startup-with-inline-images
(org-remove-inline-images))
(display-line-numbers-mode)
(org-present-read-write))
(add-hook 'org-present-mode-hook #'org-present-on)
(add-hook 'org-present-mode-quit-hook #'org-present-off)
;; Redefine org-present to call org-present-narrow after running hooks
(defun org-present ()
"init."
(interactive)
(setq org-present-mode t)
(org-present-add-overlays)
(run-hooks 'org-present-mode-hook)
(org-present-narrow)
(org-present-run-after-navigate-functions))
:init
(general-def '(normal visual motion emacs) org-present-mode-keymap
"<left>" #'org-present-prev
"<right>" #'org-present-next
"C-k" #'org-present-prev
"C-j" #'org-present-next
"q" #'org-present-quit))
#+END_SRC
** org-cliplink
Intelligently inserts an org-mode link from the clipboard.
#+BEGIN_SRC emacs-lisp
(use-package org-cliplink
:after (org)
:commands (org-cliplink
org-cliplink-clipboard-content)
:general
(org-mode-map "C-c C-S-L" #'org-cliplink))
#+END_SRC
** org-board
[[https://github.com/scallywag/org-board][Org-board]] is a bookmarking/archiving system built on Org mode:
#+BEGIN_SRC emacs-lisp
(use-package org-board
:after (org)
:config
;; Org-capture setup
(defvar org-capture-bookmark-last-url nil)
(defvar org-capture-bookmark-last-title nil)
(defun org-capture-bookmark-get-url ()
(let* ((clip (org-cliplink-clipboard-content))
(parsed (url-generic-parse-url clip)))
(if (url-type parsed)
clip
(read-string "Bookmark URL: "))))
(defun org-capture-bookmark-get-title (url)
(or (org-cliplink-retrieve-title-synchronously url)
(read-string "Bookmark title: ")))
(defun bookmark-file (title)
(org-directory (format "%s.org" (org-roam--get-new-id title))))
(defun org-capture-bookmark-file ()
(let* ((url (org-capture-bookmark-get-url))
(title (org-capture-bookmark-get-title url)))
(setq org-capture-bookmark-last-url url)
(setq org-capture-bookmark-last-title title)
(bookmark-file title)))
(defun org-capture-bookmark-link ()
(format "[[%s][%s]]"
org-capture-bookmark-last-url
org-capture-bookmark-last-title))
(defun org-capture-bookmark-title ()
org-capture-bookmark-last-title)
(defun save-bookmark (url)
(save-excursion
(goto-char (point-min))
(when (search-forward "* Bookmark" nil t)
(org-board-new url)
(wallabag-add-entry url
(cl-function
(lambda (&key data &allow-other-keys)
(let ((entry-url (format "%s/view/%s"
wallabag-base-url
(alist-get 'id data))))
(message "Added bookmark to Wallabag: %s" entry-url))))))))
(defun org-capture-bookmark-after-finalize ()
"Runs `org-board-new' on the captured entry.
Also saves to Wallabag."
(let ((success (not org-note-abort))
(key (plist-get org-capture-plist :key))
(desc (plist-get org-capture-plist :description)))
(when (and success
(equal key "b")
(equal desc "Bookmark")
org-capture-bookmark-last-url)
(save-bookmark org-capture-bookmark-last-url)
(setq org-capture-bookmark-last-url nil)
(setq org-capture-bookmark-last-title nil))))
(add-hook 'org-capture-prepare-finalize-hook
#'org-capture-bookmark-after-finalize)
(add-to-list 'org-capture-templates
'("b" "Bookmark" plain
(file org-capture-bookmark-file)
"#+TITLE: %(org-capture-bookmark-title)\n\n- tags :: [[file:deft/bookmarks.org][Bookmarks]]\n- source :: %(org-capture-bookmark-link)\n%?\n* Bookmark"))
;; Org-protocol setup
(defun make-org-protocol-bookmark (url title)
(with-temp-buffer
(let ((filename (bookmark-file title)))
(save-excursion
(insert (concat (format "#+TITLE: %s\n\n" title)
"- tags :: [[file:deft/bookmarks.org][Bookmarks]]\n"
(format "- source :: [[%s][%s]]\n\n" url title)
"* Bookmark"))
(write-file filename)
(save-bookmark url)
(save-buffer)))))
(defun bookmark-via-org-protocol (url)
(org-cliplink-retrieve-title (url-unhex-string url) #'make-org-protocol-bookmark))
(with-eval-after-load 'org-protocol
(add-to-list 'org-protocol-protocol-alist
'("Bookmark"
:protocol "bookmark"
:function bookmark-via-org-protocol
:kill-client t)))
(add-to-list 'org-board-wget-switches "--recursive")
(add-to-list 'org-board-wget-switches "--level=1")
(add-to-list 'org-board-wget-switches "--span-hosts")
;; Use w3m instead of eww to open org-board archived links
(advice-add 'org-board-open-with :around
(lambda (oldfn filename-string arg &rest args)
(cond
((not (file-exists-p filename-string)) 1)
((and filename-string
(or (and arg (eq org-board-default-browser 'system))
(and (not arg) (eq org-board-default-browser 'eww))))
(let ((filename (concat "file://"
(s-chop-prefix "file://"
filename-string))))
(w3m filename t)
0))
(:else (apply oldfn filename-string arg args)))))
:general
(org-mode-map "C-c b" org-board-keymap))
#+END_SRC
** org-rifle
Quickly find stuff in Org files:
#+BEGIN_SRC emacs-lisp
(use-package helm-org-rifle
:commands (helm-org-rifle
helm-org-rifle-agenda-files
helm-org-rifle-current-buffer
helm-org-rifle-directories
helm-org-rifle-files
helm-org-rifle-org-directory
helm-org-rifle-occur
helm-org-rifle-occur-agenda-files
helm-org-rifle-occur-current-buffer
helm-org-rifle-occur-directories
helm-org-rifle-occur-files
helm-org-rifle-occur-org-directory)
:init
(defvar helm-org-rifle-commands-map (make-sparse-keymap))
(general-def helm-org-rifle-commands-map
"r" #'helm-org-rifle
"a" #'helm-org-rifle-agenda-files
"b" #'helm-org-rifle-current-buffer
"d" #'helm-org-rifle-directories
"f" #'helm-org-rifle-files
"o" #'helm-org-rifle-org-directory
"R" #'helm-org-rifle-occur
"A" #'helm-org-rifle-occur-agenda-files
"B" #'helm-org-rifle-occur-current-buffer
"D" #'helm-org-rifle-occur-directories
"F" #'helm-org-rifle-occur-files
"O" #'helm-org-rifle-occur-org-directory)
(leader-def-key "or" helm-org-rifle-commands-map)
(jdormit/define-prefix "or" "org-rifle")
:config
(defun around-helm-org-rifle (oldfn &rest args)
(if (and (boundp 'centaur-tabs-mode) centaur-tabs-mode)
(progn
(centaur-tabs-mode -1)
(apply oldfn args)
(centaur-tabs-mode 1))
(apply oldfn args)))
(advice-add 'helm-org-rifle :around #'around-helm-org-rifle))
#+END_SRC
** Org Noter
[[https://github.com/weirdNox/org-noter][Org Noter]] lets me take org-mode notes on PDFs, epubs, and DocView files:
#+BEGIN_SRC emacs-lisp
(use-package org-noter
:commands org-noter
:general
(normal org-noter-doc-mode-map
"i" #'org-noter-insert-note
"q" #'org-noter-kill-session
"C-M-n" #'org-noter-sync-next-note
"C-M-p" #'org-noter-sync-prev-note
"M-." #'org-noter-sync-current-page-or-chapter
"M-i" #'org-noter-insert-precise-note
"M-n" #'org-noter-sync-next-page-or-chapter
"M-p" #'org-noter-sync-prev-page-or-chapter
"C-M-." #'org-noter-sync-current-note))
#+END_SRC
** Org Roam
[[https://org-roam.readthedocs.io/en/develop/][Org-roam]] is another backlink package for org-mode:
#+BEGIN_SRC emacs-lisp
(use-package org-roam
:straight (:host github :repo "jethrokuan/org-roam")
:commands
(org-roam
org-roam-today
org-roam-find-file
org-roam-insert
org-roam-show-graph
org-roam--get-new-id)
:custom
(org-roam-directory (org-directory))
:init
(defvar org-roam-map (make-sparse-keymap))
(jdormit/define-prefix "on" "org-roam")
(which-key-add-key-based-replacements "C-c n" "org-roam")
(which-key-add-major-mode-key-based-replacements
'org-mode "gn" "org-roam")
(with-eval-after-load 'org
(org-roam-mode))
:config
(add-hook 'org-roam-backlinks-mode-hook #'olivetti-mode)
:general
(leader-map "fo" #'org-roam-find-file
"of" #'org-roam-find-file
"on" org-roam-map)
(org-roam-map "l" #'org-roam
"t" #'org-roam-today
"f" #'org-roam-find-file
"i" #'org-roam-insert
"g" #'org-roam-show-graph)
(normal org-mode-map
"gr" org-roam-map)
(normal org-roam-backlinks-mode-map
"<return>" #'org-open-at-point
"q" #'quit-window)
("C-c n" org-roam-map))
#+END_SRC
** org-journal
[[https://github.com/bastibe/org-journal][org-journal]] is a package that provides some convenience functions
around keeping a daily Org-mode journal. I also set it up so it plays
nice with Org-roam:
#+BEGIN_SRC emacs-lisp
(use-package org-journal
:commands (org-journal-today
org-journal-new-entry)
:init
(defun org-journal-file-header-func (time)
(format "#+TITLE: %s"
(format-time-string "%Y-%m-%d" time)))
(defun org-journal-today ()
(interactive)
(org-journal-new-entry t))
(defun org-journal-capture-func ()
(org-journal-new-entry t)
(goto-char (point-min))
;; Account for the #+TITLE
(forward-line))
(jdormit/define-prefix "oj" "org-journal")
(leader-def-key "ojn" #'org-journal-new-entry)
(leader-def-key "ojt" #'org-journal-today)
:config
(add-to-list 'org-capture-templates
'("j" "Journal entry" entry (function org-journal-capture-func)
"* %(format-time-string org-journal-time-format)\n%?"))
:custom
(org-journal-file-type 'daily)
(org-journal-dir (org-directory))
(org-journal-file-format "%Y-%m-%d.org")
(org-journal-file-header 'org-journal-file-header-func)
(org-journal-carryover-items "")
:general
(org-roam-map "t" 'org-journal-today))
#+END_SRC
** org-super-agenda
#+BEGIN_SRC emacs-lisp
(use-package org-super-agenda
:after (org)
:init
(setq org-super-agenda-groups
'((:name "In progress"
:todo "IN PROGRESS")
(:name "Lola"
:tag "@lola")
(:name "unifyDB"
:tag "@unifydb")
(:name "Personal"
:tag "@personal")))
(org-super-agenda-mode)
:config
(setq org-super-agenda-header-map (make-sparse-keymap)))
#+END_SRC
** Org-Jira
Jira in Emacs:
#+BEGIN_SRC emacs-lisp
(use-package org-jira
:after (org)
:init
(setq jiralib-url "https://lola.atlassian.net"
org-jira-working-dir (org-directory "jira")
org-jira-jira-status-to-org-keyword-alist '(("To Do" . "TODO")
("Blocked" . "BLOCKED")
("In Progress" . "IN PROGRESS")
("Code Review" . "IN PROGRESS")
("Awaiting Release" . "IN PROGRESS")
("Done" . "DONE")
("Won't Fix" . "CANCELLED")))
(add-to-list 'org-agenda-files org-jira-working-dir)
(add-to-list 'org-super-agenda-groups
`(:name "Jira" :file-path ,org-jira-working-dir)
t))
#+END_SRC
* Doom themes
#+BEGIN_SRC emacs-lisp
(use-package doom-themes)
(use-package doom-modeline)
(doom-modeline-mode 1)
#+END_SRC
* Ewal theme
#+BEGIN_SRC emacs-lisp
(use-package doom-ewal-themes
:straight (doom-ewal-themes
:host github
:repo "jdormit/doom-ewal-themes"
:files ("themes" :defaults)))
#+END_SRC
* Customization File
I don't want anything to write to my init.el, so save customizations in a separate file:
#+BEGIN_SRC emacs-lisp
(setq custom-file (expand-file-name "custom.el" user-emacs-directory))
(load custom-file t)
#+END_SRC
* Path
`exec-path-from-shell` uses Bash to set MANPATH, PATH, and exec-path from those defined in the user's shell config. This won't work on Windows.
#+BEGIN_SRC emacs-lisp
(use-package exec-path-from-shell
:if (memq window-system '(mac ns x))
:config
(setq exec-path-from-shell-variables '("PATH" "MANPATH" "LEDGER_FILE" "LOLA_HOME"
"MODELS_HOME" "LOLA_TRAVEL_SERVICE_HOME" "WORKON_HOME"
"PIPENV_VERBOSITY" "PIPENV_DONT_LOAD_ENV"
"PIPENV_MAX_DEPTH" "PYENV_ROOT" "KOPS_STATE_STORE"
"PLAID_CLIENT_ID" "PLAID_SECRET" "PLAID_ENVIRONMENT")
exec-path-from-shell-check-startup-files nil)
(exec-path-from-shell-initialize))
#+END_SRC
* Autocompletion
There seems to be [[https://github.com/company-mode/company-mode/issues/68][some contention]] about whether autocomplete or company are better autocomplete packages. I'm going with company for now because the maintainer seems nicer...
#+BEGIN_SRC emacs-lisp
(use-package company
:commands (company-complete
company-mode)
:hook ((prog-mode . company-mode))
:config
(setq company-idle-delay 0.3))
(general-def "C-M-i" #'company-complete)
(general-def "M-<tab>" #'company-complete)
#+END_SRC
* Multiple cursors
#+BEGIN_SRC emacs-lisp
(use-package evil-mc
:defer 0
:config
(global-evil-mc-mode)
(general-def '(normal visual)
"gs" evil-mc-cursors-map
"M-n" #'evil-mc-make-and-goto-next-cursor
"M-p" #'evil-mc-make-and-goto-prev-cursor
"C-n" #'evil-mc-make-and-goto-next-match
"C-t" #'evil-mc-skip-and-goto-next-match
"C-p" #'evil-mc-make-and-goto-prev-match))
#+END_SRC
#+BEGIN_SRC emacs-lisp
(use-package evil-multiedit
:defer 0
:config
(evil-multiedit-default-keybinds))
#+END_SRC
* Transient
A framework for creating Magit-style popups:
#+BEGIN_SRC emacs-lisp
(use-package transient
:commands (define-transient-command))
#+END_SRC
* Magit
Magit is objectively the best Git interface.
#+BEGIN_SRC emacs-lisp
(use-package magit
:commands (magit-status
magit-blame
magit-find-file
magit-name-local-branch))
#+END_SRC
#+BEGIN_SRC emacs-lisp
(jdormit/define-prefix "g" "git")
(leader-def-key
"gs" #'magit-status
"gg" #'magit-file-dispatch
"gd" #'magit-dispatch
"gf" #'magit-find-file)
#+END_SRC
** Forge
[[https://github.com/magit/forge][Forge]] is an extension for Magit that lets it interact with code forges (e.g. GitHub).
#+BEGIN_SRC emacs-lisp
(use-package forge
:after (magit)
:config
(add-to-list 'forge-alist '("git.jeremydormitzer.com"
"git.jeremydormitzer.com/api/v1"
"git.jeremydormitzer.com"
forge-gitea-repository))
(with-eval-after-load 'evil-magit
(general-def '(normal motion) magit-mode-map
"yu" #'forge-copy-url-at-point-as-kill))
:general
((normal motion visual) forge-topic-list-mode-map
"y" #'forge-copy-url-at-point-as-kill
"q" #'quit-window)
:custom
(forge-owned-accounts '((jdormit . (remote-name "jdormit")))))
#+END_SRC
** evil-magit
Evil keybindings for magit!
#+BEGIN_SRC emacs-lisp
(use-package evil-magit
:after (evil magit)
:config
(with-eval-after-load 'magit
(require 'evil-magit))
:general
('normal magit-mode-map "SPC" leader-map))
#+END_SRC
** Transient
#+BEGIN_SRC emacs-lisp
(setq transient-default-level 7)
#+END_SRC
* git-link
Open files in Git forges (GitHub, GitLab, etc.):
#+BEGIN_SRC emacs-lisp
(use-package git-link
:commands (git-link)
:init
(defun git-link-copy ()
(interactive)
(let ((git-link-open-in-browser nil))
(call-interactively 'git-link)))
(leader-def-key "gl" #'git-link)
(leader-def-key "gy" #'git-link-copy)
:custom
(git-link-open-in-browser t)
(git-link-remote-alist '(("git.sr.ht" git-link-sourcehut)
("github" git-link-github)
("bitbucket" git-link-bitbucket)
("gitorious" git-link-gitorious)
("gitlab" git-link-gitlab)
("visualstudio\\|azure" git-link-azure)
("git.jeremydormitzer.com" git-link-bitbucket))))
#+END_SRC
* with-editor
A utility from the author of Magit to run shell commands using the current Emacs instance as $EDITOR.
#+BEGIN_SRC emacs-lisp
(with-eval-after-load 'with-editor
(shell-command-with-editor-mode)
(add-hook 'shell-mode-hook #'with-editor-export-editor)
(add-hook 'term-exec-hook #'with-editor-export-editor)
(add-hook 'eshell-mode-hook #'with-editor-export-editor))
#+END_SRC
* Password Store
Interfacing with Pass, the "standard Unix password manager". This should also be loaded before `exec-path-from-shell`.
#+BEGIN_SRC emacs-lisp
(defun password-store-synchronize ()
(interactive)
(with-editor-async-shell-command "pass git pull && pass git push"))
(use-package password-store
:defer t
:if (executable-find "pass")
:config
(setq password-store-password-length 20)
:init
(leader-def-key "P" 'password-store-copy)
(epa-file-enable))
(use-package pass
:if (executable-find "pass")
:commands pass
:general
(normal pass-mode-map "S" #'password-store-synchronize))
(leader-def-key "ap" #'pass)
(setq auth-sources '("~/.authinfo" password-store))
#+END_SRC
* Init File
A function to reload my init file. It reloads the major mode after the init file is loaded to rebind keymappings.
#+BEGIN_SRC emacs-lisp
(defun reload-init-file ()
(interactive)
(load-file "~/.emacs.d/init.el")
(funcall major-mode))
#+END_SRC
And another one to edit it:
#+BEGIN_SRC emacs-lisp
(defun find-init-file ()
(interactive)
(find-file "~/.emacs.d/init.org"))
#+END_SRC
* Keybindings
These are general keybindings; those specific to certain packages are in the sections for those packages.
In some modes I want vanilla Emacs bindings:
#+BEGIN_SRC emacs-lisp
(with-eval-after-load 'evil
(add-to-list 'evil-emacs-state-modes 'benchmark-init/tabulated-mode)
(add-to-list 'evil-emacs-state-modes 'benchmark-init/tree-mode)
(add-to-list 'evil-emacs-state-modes 'cider-stacktrace-mode)
(add-to-list 'evil-emacs-state-modes 'geiser-debug-mode)
(add-to-list 'evil-emacs-state-modes 'undo-tree-visualizer-mode)
(add-to-list 'evil-emacs-state-modes 'makey-key-mode)
(add-to-list 'evil-emacs-state-modes 'term-mode))
#+END_SRC
** Visual line navigation
#+BEGIN_SRC emacs-lisp
(general-def 'motion "j" #'evil-next-visual-line)
(general-def 'motion "k" #'evil-previous-visual-line)
(general-def 'motion "j" #'evil-next-visual-line)
(general-def 'motion "k" #'evil-previous-visual-line)
#+END_SRC
** M-x
#+BEGIN_SRC emacs-lisp
(leader-def-key "SPC" 'execute-extended-command)
#+END_SRC
** Eval-ing
#+BEGIN_SRC emacs-lisp
(leader-def-key ":" #'eval-expression)
#+END_SRC
** Init file commands
#+BEGIN_SRC emacs-lisp
(jdormit/define-prefix "." "dotfile")
(leader-def-key ".r" 'reload-init-file)
(leader-def-key ".f" 'find-init-file)
#+END_SRC
** Commands about files
#+BEGIN_SRC emacs-lisp
(jdormit/define-prefix "f" "files")
(leader-def-key "ff" 'find-file)
(leader-def-key "fd" 'dired)
(leader-def-key "fs" 'sudo-find-file)
(leader-def-key "ft" 'auto-revert-tail-mode)
(leader-def-key "fp" 'find-file-at-point)
(general-def '(normal motion visual) "gf" 'find-file-at-point)
#+END_SRC
** Window commands
#+BEGIN_SRC emacs-lisp
(jdormit/define-prefix "w" "window")
(leader-def-key "w/" 'split-window-right)
(leader-def-key "w-" 'split-window-below)
(leader-def-key "wm" 'delete-other-windows)
(leader-def-key "wd" 'delete-window)
(leader-def-key "ws" 'window-swap-states)
#+END_SRC
** Buffer commands
A function to switch to previous buffer from [[http://emacsredux.com/blog/2013/04/28/switch-to-previous-buffer/][this blog post]]:
#+BEGIN_SRC emacs-lisp
(defun switch-to-previous-buffer ()
"Switch to previously open buffer.
Repeated invocations toggle between the two most recently open buffers."
(interactive)
(switch-to-buffer (other-buffer (current-buffer) 1)))
(leader-def-key "TAB" 'switch-to-previous-buffer)
#+END_SRC
A function to kill all buffers except the current one from [[https://www.emacswiki.org/emacs/KillingBuffers#toc2][EmacsWiki]]:
#+BEGIN_SRC emacs-lisp
(defun kill-other-buffers ()
"Kill all other buffers."
(interactive)
(mapc 'kill-buffer (delq (current-buffer) (buffer-list))))
#+END_SRC
#+BEGIN_SRC emacs-lisp
(jdormit/define-prefix "b" "buffer")
(leader-def-key "bb" #'switch-to-buffer)
(leader-def-key "bn" #'next-buffer)
(leader-def-key "bp" #'previous-buffer)
(leader-def-key "bd" #'kill-buffer)
(leader-def-key "bm" #'kill-other-buffers)
(leader-def-key "br" #'rename-buffer)
#+END_SRC
** Frame commands
#+BEGIN_SRC emacs-lisp
(jdormit/define-prefix "F" "frame")
(leader-def-key "Fn" #'make-frame-command)
(leader-def-key "Fo" #'other-frame)
(leader-def-key "Fm" #'delete-other-frames)
(leader-def-key "Fd" #'delete-frame)
#+END_SRC
** Running shell commands
#+BEGIN_SRC emacs-lisp
(leader-def-key "!" 'shell-command)
(leader-def-key "|" 'shell-command-on-region)
(general-def 'normal shell-mode-map "q" #'bury-buffer)
#+END_SRC
** Toggles
Like in Spacemacs, put all toggle commands behind a prefix:
#+BEGIN_SRC emacs-lisp
(jdormit/define-prefix "t" "toggle")
#+END_SRC
Toggles about line truncation:
#+BEGIN_SRC emacs-lisp
(leader-def-key "tt" 'toggle-truncate-lines)
(leader-def-key "tT" 'visual-line-mode)
(leader-def-key "tf" 'auto-fill-mode)
#+END_SRC
Toggle lisp debugging:
#+BEGIN_SRC emacs-lisp
(leader-def-key "td" 'toggle-debug-on-error)
#+END_SRC
** Shells/REPLs
Emacs has a shell for every mood!
#+BEGIN_SRC emacs-lisp
(jdormit/define-prefix "s" "shells/REPLs")
(leader-def-key "ss" 'shell)
(leader-def-key "si" 'ielm)
(leader-def-key "se" 'eshell)
(leader-def-key "sa" 'ansi-term)
#+END_SRC
** Applications
#+BEGIN_SRC emacs-lisp
(jdormit/define-prefix "a" "applications")
#+END_SRC
** Help Buffers
#+BEGIN_SRC emacs-lisp
(general-def 'motion help-mode-map "TAB" #'forward-button)
#+END_SRC
** Code commands
#+BEGIN_SRC emacs-lisp
(jdormit/define-prefix "c" "code")
(leader-def-key "cd" #'xref-find-definitions)
(leader-def-key "cr" #'xref-find-references)
(leader-def-key "ca" #'xref-find-apropos)
(general-def 'normal "M-." #'xref-find-definitions)
#+END_SRC
** Process list
#+BEGIN_SRC emacs-lisp
(general-def '(normal motion visual) process-menu-mode-map
"d" #'process-menu-delete-process)
#+END_SRC
** Registers
#+BEGIN_SRC emacs-lisp
(jdormit/define-prefix "r" "registers")
(leader-def-key "r" ctl-x-r-map)
#+END_SRC
* Evil-numbers
Handy functions to increment/decrement numbers at point:
#+BEGIN_SRC emacs-lisp
(use-package evil-numbers
:straight (:host github :repo "janpath/evil-numbers")
:general
((normal visual motion) "g+" 'evil-numbers/inc-at-pt)
((normal visual motion) "g-" 'evil-numbers/dec-at-pt)
((normal visual motion) "g C-+" 'evil-numbers/inc-at-pt-incremental)
((normal visual motion) "g C--" 'evil-numbers/dec-at-pt-incremental))
#+END_SRC
* Winner
Winner is a minor mode that keeps an undo/redo history of the window configuration:
#+BEGIN_SRC emacs-lisp
(winner-mode 1)
(leader-def-key "wn" #'winner-redo)
(leader-def-key "wp" #'winner-undo)
#+END_SRC
* Buffer move
A handy package to shift buffers between open windows:
#+BEGIN_SRC emacs-lisp
(use-package buffer-move
:general
("C-S-j" #'buf-move-down)
("C-S-k" #'buf-move-up)
("C-S-h" #'buf-move-left)
("C-S-l" #'buf-move-right))
#+END_SRC
* Info
#+BEGIN_SRC emacs-lisp
(use-package info
:init
(defhydra hydra-info (:color blue
:hint nil)
"
Info-mode:
^^_]_ forward (next logical node) ^^_l_ast (←) _u_p (↑) _f_ollow reference _T_OC
^^_[_ backward (prev logical node) ^^_r_eturn (→) _m_enu (↓) (C-u for new window) _i_ndex _d_irectory
^^_n_ext (same level only) ^^_H_istory _g_oto (C-u for new window) _,_ next index item _c_opy node name
^^_p_rev (same level only) _<_/_t_op _b_eginning of buffer virtual _I_ndex _C_lone buffer
regex _s_earch (_S_ case sensitive) ^^_>_ final _e_nd of buffer ^^ _a_propos
_1_ .. _9_ Pick first .. ninth item in the node's menu.
"
("]" Info-forward-node)
("[" Info-backward-node)
("n" Info-next)
("p" Info-prev)
("s" Info-search)
("S" Info-search-case-sensitively)
("l" Info-history-back)
("r" Info-history-forward)
("H" Info-history)
("t" Info-top-node)
("<" Info-top-node)
(">" Info-final-node)
("u" Info-up)
("^" Info-up)
("m" Info-menu)
("g" Info-goto-node)
("b" beginning-of-buffer)
("e" end-of-buffer)
("f" Info-follow-reference)
("i" Info-index)
("," Info-index-next)
("I" Info-virtual-index)
("T" Info-toc)
("d" Info-directory)
("c" Info-copy-current-node-name)
("C" clone-buffer)
("a" info-apropos)
("1" Info-nth-menu-item)
("2" Info-nth-menu-item)
("3" Info-nth-menu-item)
("4" Info-nth-menu-item)
("5" Info-nth-menu-item)
("6" Info-nth-menu-item)
("7" Info-nth-menu-item)
("8" Info-nth-menu-item)
("9" Info-nth-menu-item)
("?" Info-summary "Info summary")
("h" Info-help "Info help")
("q" Info-exit "Info exit")
("C-g" nil "cancel" :color blue))
:general
(Info-mode-map "C-/" 'hydra-info/body))
#+END_SRC
* xref
After I select an xref reference, I want the xref buffer closed:
#+BEGIN_SRC emacs-lisp
(defun xref-goto-xref-and-quit ()
(interactive)
(xref-goto-xref t))
(general-def 'normal xref--xref-buffer-mode-map "RET"
#'xref-goto-xref-and-quit :keymaps 'override)
#+END_SRC
Don't prompt for an identifier for these xref commands:
#+BEGIN_SRC emacs-lisp
(setq xref-prompt-for-identifier
'(not xref-find-definitions
xref-find-definitions-other-window
xref-find-definitions-other-frame
xref-find-references))
#+END_SRC
Some keybindings:
#+BEGIN_SRC emacs-lisp
(general-def "M-r" #'xref-find-references)
#+END_SRC
* IBuffer
#+BEGIN_SRC emacs-lisp
(use-package ibuffer
:straight (:type built-in)
:init
(defhydra hydra-ibuffer-main (:color pink :hint nil)
"
^Navigation^ | ^Mark^ | ^Actions^ | ^View^
-^----------^-+-^----^--------+-^-------^--------+-^----^-------
_k_: ʌ | _m_: mark | _D_: delete | _g_: refresh
_RET_: visit | _u_: unmark | _S_: save | _s_: sort
_j_: v | _*_: specific | _a_: all actions | _/_: filter
-^----------^-+-^----^--------+-^-------^--------+-^----^-------
"
("j" ibuffer-forward-line)
("RET" ibuffer-visit-buffer :color blue)
("k" ibuffer-backward-line)
("m" ibuffer-mark-forward)
("u" ibuffer-unmark-forward)
("*" hydra-ibuffer-mark/body :color blue)
("D" ibuffer-do-delete)
("S" ibuffer-do-save)
("a" hydra-ibuffer-action/body :color blue)
("g" ibuffer-update)
("s" hydra-ibuffer-sort/body :color blue)
("/" hydra-ibuffer-filter/body :color blue)
("o" ibuffer-visit-buffer-other-window "other window" :color blue)
("q" quit-window "quit ibuffer" :color blue)
("." nil "toggle hydra" :color blue))
(defhydra hydra-ibuffer-mark (:color teal
:columns 5
:after-exit (hydra-ibuffer-main/body))
"Mark"
("*" ibuffer-unmark-all "unmark all")
("M" ibuffer-mark-by-mode "mode")
("m" ibuffer-mark-modified-buffers "modified")
("u" ibuffer-mark-unsaved-buffers "unsaved")
("s" ibuffer-mark-special-buffers "special")
("r" ibuffer-mark-read-only-buffers "read-only")
("/" ibuffer-mark-dired-buffers "dired")
("e" ibuffer-mark-dissociated-buffers "dissociated")
("h" ibuffer-mark-help-buffers "help")
("z" ibuffer-mark-compressed-file-buffers "compressed")
("b" hydra-ibuffer-main/body "back" :color blue))
(defhydra hydra-ibuffer-action (:color teal
:columns 4
:after-exit
(if (eq major-mode 'ibuffer-mode)
(hydra-ibuffer-main/body)))
"Action"
("A" ibuffer-do-view "view")
("E" ibuffer-do-eval "eval")
("F" ibuffer-do-shell-command-file "shell-command-file")
("I" ibuffer-do-query-replace-regexp "query-replace-regexp")
("H" ibuffer-do-view-other-frame "view-other-frame")
("N" ibuffer-do-shell-command-pipe-replace "shell-cmd-pipe-replace")
("M" ibuffer-do-toggle-modified "toggle-modified")
("O" ibuffer-do-occur "occur")
("P" ibuffer-do-print "print")
("Q" ibuffer-do-query-replace "query-replace")
("R" ibuffer-do-rename-uniquely "rename-uniquely")
("T" ibuffer-do-toggle-read-only "toggle-read-only")
("U" ibuffer-do-replace-regexp "replace-regexp")
("V" ibuffer-do-revert "revert")
("W" ibuffer-do-view-and-eval "view-and-eval")
("X" ibuffer-do-shell-command-pipe "shell-command-pipe")
("b" nil "back"))
(defhydra hydra-ibuffer-sort (:color amaranth :columns 3)
"Sort"
("i" ibuffer-invert-sorting "invert")
("a" ibuffer-do-sort-by-alphabetic "alphabetic")
("v" ibuffer-do-sort-by-recency "recently used")
("s" ibuffer-do-sort-by-size "size")
("f" ibuffer-do-sort-by-filename/process "filename")
("m" ibuffer-do-sort-by-major-mode "mode")
("b" hydra-ibuffer-main/body "back" :color blue))
(defhydra hydra-ibuffer-filter (:color amaranth :columns 4)
"Filter"
("m" ibuffer-filter-by-used-mode "mode")
("M" ibuffer-filter-by-derived-mode "derived mode")
("n" ibuffer-filter-by-name "name")
("c" ibuffer-filter-by-content "content")
("e" ibuffer-filter-by-predicate "predicate")
("f" ibuffer-filter-by-filename "filename")
(">" ibuffer-filter-by-size-gt "size")
("<" ibuffer-filter-by-size-lt "size")
("/" ibuffer-filter-disable "disable")
("b" hydra-ibuffer-main/body "back" :color blue))
:general
((normal motion visual insert emacs) ibuffer-mode-map "." 'hydra-ibuffer-main/body))
#+END_SRC
* Speedbar
Speedbar is cool but having it open in a separate frame is annoying. This makes it open in a side window in the same frame:
#+BEGIN_SRC emacs-lisp
(use-package sr-speedbar
:commands (sr-speedbar-toggle
sr-speedbar-open
sr-speedbar-select-window
sr-speedbar-exist-p)
:general
(speedbar-mode-map "q" #'sr-speedbar-close))
(defun switch-to-speedbar ()
(interactive)
(unless (sr-speedbar-exist-p)
(sr-speedbar-open))
(sr-speedbar-select-window))
(leader-def-key "S" #'switch-to-speedbar)
#+END_SRC
* Trailing whitespace
[[https://github.com/lewang/ws-butler][ws-butler]] deletes trailing whitespace on lines that you touch, but
leaves other lines alone:
#+BEGIN_SRC emacs-lisp
(use-package ws-butler
:straight (ws-butler :host github :repo "lewang/ws-butler")
:hook ((prog-mode . ws-butler-mode)))
#+END_SRC
* Whitespace Visualation
#+BEGIN_SRC emacs-lisp
(setq whitespace-line-column 80
whitespace-style '(face lines-tail))
(leader-def-key "tw" #'whitespace-mode)
#+END_SRC
* Line Numbers
Toggle line numbers:
#+BEGIN_SRC emacs-lisp
(setq display-line-numbers-type 'visual)
(leader-def-key "tn" 'display-line-numbers-mode)
#+END_SRC
Toggle line numbering mode (normal or relative):
#+BEGIN_SRC emacs-lisp
(defun toggle-line-number-mode ()
(interactive)
(when display-line-numbers
(if (eq display-line-numbers 'visual)
(progn
(setq display-line-numbers t)
(setq display-line-numbers-type t))
(progn
(setq display-line-numbers 'visual)
(setq display-line-numbers-type 'visual)))))
(leader-def-key "tr" #'toggle-line-number-mode)
#+END_SRC
Display line numbers by default in code and org-mode buffers:
#+BEGIN_SRC emacs-lisp
(add-hook 'prog-mode-hook #'display-line-numbers-mode)
(add-hook 'org-mode-hook #'display-line-numbers-mode)
#+END_SRC
* Amx
A better M-x.
#+BEGIN_SRC emacs-lisp
(use-package amx
:config
(amx-mode))
#+END_SRC
* Olivetti Mode
Olivetti is a minor mode for a nice writing environment.
#+BEGIN_SRC emacs-lisp
(use-package olivetti
:config
(setq-default olivetti-body-width 100)
(setq olivetti-body-width 100)
:commands olivetti-mode)
(leader-def-key "to" 'olivetti-mode)
#+END_SRC
* Winum
This package includes functions to switch windows by number.
#+BEGIN_SRC emacs-lisp
(defun winum-assign-0-to-dired-sidebar ()
(when (equal major-mode 'dired-sidebar-mode) 10))
(use-package winum
:config
(winum-mode)
(add-to-list 'winum-assign-functions #'winum-assign-0-to-dired-sidebar)
(leader-def-key "0" 'winum-select-window-0-or-10)
(leader-def-key "1" 'winum-select-window-1)
(leader-def-key "2" 'winum-select-window-2)
(leader-def-key "3" 'winum-select-window-3)
(leader-def-key "4" 'winum-select-window-4)
(leader-def-key "5" 'winum-select-window-5)
(leader-def-key "6" 'winum-select-window-6)
(leader-def-key "7" 'winum-select-window-7)
(leader-def-key "8" 'winum-select-window-8)
(leader-def-key "9" 'winum-select-window-9)
:custom
(winum-scope 'frame-local))
#+END_SRC
I don't want which-key display "lambda" for the descriptions of these, so set a custom display function. This is [[https://github.com/syl20bnr/spacemacs/blob/master/layers/+distributions/spacemacs-bootstrap/packages.el#L312][stolen from Spacemacs]].
#+BEGIN_SRC emacs-lisp
(push '(("\\(.*\\) 0" . "select-window-0") . ("\\1 0..9" . "window 0..9"))
which-key-replacement-alist)
(push '((nil . "select-window-[1-9]") . t) which-key-replacement-alist)
#+END_SRC
* Backups and Autosaves
Store backups and autosaves in a centralized place. This should really be the default...
#+BEGIN_SRC emacs-lisp
(make-directory (expand-file-name "~/.emacs.d/autosaves") t)
(setq auto-save-file-name-transforms '((".*" "~/.emacs.d/autosaves" t)))
(setq backup-directory-alist '(("." . "~/.emacs.d/backups")))
#+END_SRC
* Smartparens/Parinfer
Smartparens enables structured editing of s-expressions and other pairs:
#+BEGIN_SRC emacs-lisp
(use-package smartparens
:hook ((prog-mode . smartparens-strict-mode)
(eshell-mode . smartparens-strict-mode)
(vterm-mode . smartparens-mode)
(geiser-repl-mode . smartparens-strict-mode)
(inferior-python-mode . smartparens-strict-mode))
:init
(defhydra hydra-smartparens (:hint nil)
"
Moving^^^^ Slurp & Barf^^ Wrapping^^ Sexp juggling^^^^ Destructive
------------------------------------------------------------------------------------------------------------------------
[_a_] beginning [_n_] down [_h_] bw slurp [_R_] rewrap [_S_] split [_t_] transpose [_c_] change inner [_w_] copy
[_e_] end [_N_] bw down [_H_] bw barf [_u_] unwrap [_s_] splice [_A_] absorb [_C_] change outer
[_f_] forward [_p_] up [_l_] slurp [_U_] bw unwrap [_r_] raise [_E_] emit [_k_] kill [_g_] quit
[_b_] backward [_P_] bw up [_L_] barf [_(__{__[_] wrap (){}[] [_j_] join [_o_] convolute [_K_] bw kill [_q_] quit"
;; Moving
("a" sp-beginning-of-sexp)
("e" sp-end-of-sexp)
("f" sp-forward-sexp)
("b" sp-backward-sexp)
("n" sp-down-sexp)
("N" sp-backward-down-sexp)
("p" sp-up-sexp)
("P" sp-backward-up-sexp)
;; Slurping & barfing
("h" sp-backward-slurp-sexp)
("H" sp-backward-barf-sexp)
("l" sp-forward-slurp-sexp)
("L" sp-forward-barf-sexp)
;; Wrapping
("R" sp-rewrap-sexp)
("u" sp-unwrap-sexp)
("U" sp-backward-unwrap-sexp)
("(" sp-wrap-round)
("{" sp-wrap-curly)
("[" sp-wrap-square)
;; Sexp juggling
("S" sp-split-sexp)
("s" sp-splice-sexp)
("r" sp-raise-sexp)
("j" sp-join-sexp)
("t" sp-transpose-sexp)
("A" sp-absorb-sexp)
("E" sp-emit-sexp)
("o" sp-convolute-sexp)
;; Destructive editing
("c" sp-change-inner :exit t)
("C" sp-change-enclosing :exit t)
("k" sp-kill-sexp)
("K" sp-backward-kill-sexp)
("w" sp-copy-sexp)
("q" nil)
("g" nil))
:config
(require 'smartparens-config)
(show-smartparens-global-mode t)
(defun sp-wrap-double-quotes (&optional arg)
(interactive "P")
(sp-wrap-with-pair "\""))
(defun sp-wrap-single-quotes (&optional arg)
(interactive "P")
(sp-wrap-with-pair "\'"))
(setq sp-ignore-modes-list
(delete 'minibuffer-inactive-mode sp-ignore-modes-list))
(sp-local-pair 'minibuffer-inactive-mode "\'" nil :actions nil)
:general
(prog-mode-map "C-c p" 'hydra-smartparens/body)
(normal prog-mode-map "g p" 'hydra-smartparens/body)
(normal "g[" 'sp-wrap-square)
(normal "g(" 'sp-wrap-round)
(normal "g{" 'sp-wrap-curly)
(normal "g\"" 'sp-wrap-double-quotes)
(normal "g\'" 'sp-wrap-single-quotes))
(use-package evil-smartparens
:after (evil smartparens)
:hook ((smartparens-enabled . evil-smartparens-mode)))
(jdormit/define-prefix "l" "lisp")
(jdormit/define-prefix "lw" "wrap")
(leader-def-key "lwr" 'sp-wrap-round)
(leader-def-key "lws" 'sp-wrap-square)
(leader-def-key "lwc" 'sp-wrap-curly)
(leader-def-key "ls" 'sp-forward-slurp-sexp)
(leader-def-key "lb" 'sp-forward-barf-sexp)
#+END_SRC
Enable ES6 arrow functions in web-mode ("borrowed" from [[https://github.com/Fuco1/smartparens/issues/823#issuecomment-403019519][this GitHub comment]]):
#+BEGIN_SRC emacs-lisp
(with-eval-after-load 'smartparens
(defun sp-after-equals-p (_id action _context)
(when (memq action '(insert navigate))
(sp--looking-back-p "=>" 2)))
(defun sp-after-equals-skip-p (ms mb _me)
(when (eq ms ">")
(save-excursion
(goto-char mb)
(sp--looking-back-p "=" 1))))
(sp-local-pair '(web-mode) "<" nil
:unless '(:add sp-after-equals-p)
:skip-match 'sp-after-equals-skip-p))
#+END_SRC
Parinfer infers parens from indentation and vice-versa. Currently
disabled since it turned out to be more annoying than good...
#+BEGIN_SRC emacs-lisp
(use-package parinfer
:disabled
:init
(leader-def-key "lt" 'parinfer-toggle-mode)
(setq parinfer-extensions '(defaults
pretty-parens
evil
smart-tab
smart-yank))
:hook ((clojure-mode . parinfer-mode)
(emacs-lisp-mode . parinfer-mode)
(common-lisp-mode . parinfer-mode)
(scheme-mode . parinfer-mode)
(lisp-mode . parinfer-mode)))
#+END_SRC
* jq
The JSON multitool.
#+BEGIN_SRC emacs-lisp
(use-package jq-mode
:commands (jq-mode jq-interactively))
#+END_SRC
* link-hint
A very helpful package that provides jump-to-link functionality:
#+BEGIN_SRC emacs-lisp
(use-package link-hint
:commands (link-hint-open-link
link-hint-copy-link)
:init
(jdormit/define-prefix "ol" "link-hint")
(leader-def-key "oll" #'link-hint-open-link)
(leader-def-key "olc" #'link-hint-copy-link))
#+END_SRC
* Projectile
#+BEGIN_SRC emacs-lisp
(use-package projectile
:hook ((after-init . projectile-mode))
:commands (projectile-mode
projectile-find-file
projectile-grep
projectile-switch-project
projectile-project-root)
:config
(jdormit/define-prefix "p" "projectile")
(leader-def-key "p" projectile-command-map))
(defmacro with-projectile-root (&rest body)
`(with-temp-buffer
(when (projectile-project-root)
(cd (projectile-project-root)))
,@body))
#+END_SRC
* Perspective
A package that groups buffers/windows into workspaces per project:
#+BEGIN_SRC emacs-lisp
(use-package perspective
:commands (persp-mode)
:hook ((after-init . persp-mode))
:init
(jdormit/define-prefix "v" "perspective")
:config
(leader-def-key "v" perspective-map)
(defun switch-to-previous-buffer ()
"Switch to previously open buffer.
Repeated invocations toggle between the two most recently open buffers."
(interactive)
(persp-switch-to-buffer (other-buffer (current-buffer) 1)))
:custom
(persp-sort 'created)
:general
([remap kill-buffer] #'persp-kill-buffer*)
([remap switch-to-buffer] #'persp-switch-to-buffer*)
([remap counsel-switch-buffer] #'persp-counsel-switch-buffer)
([remap ivy-switch-buffer] #'persp-ivy-switch-buffer))
(use-package persp-projectile
:after (perspective projectile)
:demand t
:config
(projectile-persp-bridge counsel-projectile)
(projectile-persp-bridge counsel-projectile-switch-project)
:general
([remap projectile-switch-project] #'projectile-persp-switch-project))
#+END_SRC
* Mode line
* UI
Get rid of the janky buttons:
#+BEGIN_SRC emacs-lisp
(tool-bar-mode -1)
#+END_SRC
And the menu bar:
#+BEGIN_SRC emacs-lisp
(menu-bar-mode -1)
#+END_SRC
And the ugly scroll bars:
#+BEGIN_SRC emacs-lisp
(set-scroll-bar-mode nil)
#+END_SRC
Use =variable-pitch-mode= in text modes:
#+BEGIN_SRC emacs-lisp
(add-hook 'text-mode-hook (lambda () (variable-pitch-mode)))
(add-hook 'w3m-mode-hook (lambda () (variable-pitch-mode)))
#+END_SRC
Always use =buffer-face-mode= in code and text buffers:
#+BEGIN_SRC emacs-lisp
(add-hook 'prog-mode-hook #'buffer-face-mode)
(add-hook 'text-mode-hook #'buffer-face-mode)
#+END_SRC
Display the column number in programming modes:
#+BEGIN_SRC emacs-lisp
(add-hook 'prog-mode-hook #'column-number-mode)
#+END_SRC
Disable the bell:
#+BEGIN_SRC emacs-lisp
(setq ring-bell-function 'ignore)
#+END_SRC
Render stuff differently based on whether or not we are graphical:
#+BEGIN_SRC emacs-lisp
(defun graphical-setup ()
(when (display-graphic-p (selected-frame))
(message "Running graphically")))
(defun non-graphical-setup ()
(when (not (display-graphic-p (selected-frame)))
(message "Running in terminal")
(menu-bar-mode -1)))
(defun do-graphical-non-graphical-setup ()
(graphical-setup)
(non-graphical-setup))
(add-hook 'window-setup-hook #'do-graphical-non-graphical-setup)
#+END_SRC
Try to make the background normal colored in the terminal:
#+BEGIN_SRC emacs-lisp
(defvar no-background-in-tty-faces '(default line-number magit-section-highlight))
(defun on-frame-open (frame)
(unless (display-graphic-p frame)
(menu-bar-mode -1)
(dolist (face no-background-in-tty-faces)
(set-face-background face "unspecified" frame))))
(mapc #'on-frame-open (frame-list))
(add-hook 'after-make-frame-functions #'on-frame-open)
(defun on-after-init ()
(unless (display-graphic-p (selected-frame))
(dolist (face no-background-in-tty-faces)
(set-face-background face "unspecified" (selected-frame)))))
(add-hook 'window-setup-hook #'on-after-init)
#+END_SRC
UI-related keybindings:
#+BEGIN_SRC emacs-lisp
(jdormit/define-prefix "u" "UI")
(leader-def-key "ut" #'customize-themes)
(leader-def-key "uf" #'customize-face)
(leader-def-key "uc" #'display-time-mode)
(leader-def-key "ub" #'display-battery-mode)
#+END_SRC
* Window handling
Following [[https://github.com/nex3/perspective-el#some-musings-on-emacs-window-layouts][some excellent advice]] from the author of perspective.el
about making Emacs' window handling saner:
#+BEGIN_SRC emacs-lisp
(setq display-buffer-alist
'((".*" (display-buffer-reuse-window display-buffer-same-window))))
(setq display-buffer-reuse-frames t) ; reuse windows in other frames
(setq even-window-sizes nil) ; display-buffer: avoid resizing
#+END_SRC
* Centaur tabs
[[https://github.com/ema2159/centaur-tabs][Centaur tabs]] is a package that gives Emacs buffer tabs similar to those in Atom or VS Code:
#+BEGIN_SRC emacs-lisp
(use-package centaur-tabs
:commands (centaur-tabs-mode
centaur-tabs-local-mode
centaur-tabs-mode-on-p)
:init
(setq centaur-tabs-set-icons t
centaur-tabs-gray-out-icons 'buffer
centaur-tabs-height 30
centaur-tabs-set-bar 'under
x-underline-at-descent-line t
centaur-tabs-set-modified-marker t
centaur-tabs-show-navigation-buttons t
centaur-tabs-down-tab-text ""
centaur-tabs-backward-tab-text ""
centaur-tabs-forward-tab-text ""
centaur-tabs-close-button ""
centaur-tabs-modified-marker ""
centaur-tabs-cycle-scope 'tabs
centaur-tabs-label-fixed-length 20)
(leader-def-key "uT" #'centaur-tabs-mode)
(centaur-tabs-mode)
:config
(centaur-tabs-group-by-projectile-project)
;; Custom buffer groups
(defun centaur-tabs-projectile-buffer-groups ()
"Return the list of group names BUFFER belongs to."
(if centaur-tabs-projectile-buffer-group-calc
(symbol-value 'centaur-tabs-projectile-buffer-group-calc)
(set (make-local-variable 'centaur-tabs-projectile-buffer-group-calc)
(cond
((or (get-buffer-process (current-buffer))
(memq major-mode '(comint-mode compilation-mode))) '("Term"))
((string-equal "*" (substring (buffer-name) 0 1)) '("Misc"))
((condition-case _err
(projectile-project-root)
(error nil)) (list (projectile-project-name)))
((memq major-mode '(emacs-lisp-mode python-mode emacs-lisp-mode c-mode
c++-mode javascript-mode js-mode
js2-mode makefile-mode
lua-mode vala-mode)) '("Coding"))
((memq major-mode '(nxhtml-mode html-mode
mhtml-mode css-mode)) '("HTML"))
((memq major-mode '(org-journal-mode)) '("Journal"))
((memq major-mode '(org-mode calendar-mode diary-mode)) '("Org"))
((memq major-mode '(dired-mode)) '("Dir"))
(t '("Other"))))
(symbol-value 'centaur-tabs-projectile-buffer-group-calc)))
;; Don't show tabs for certain types of buffers
(advice-add 'centaur-tabs-hide-tab :around
(lambda (oldfn buf &rest args)
(if (with-current-buffer buf
(or (eq major-mode 'vuiet-mode)
(eq major-mode 'dired-sidebar-mode)))
t
(apply oldfn buf args))))
;; Only show tabs in buffers visiting files
(advice-add 'centaur-tabs-line :around
(lambda (oldfn &rest args)
(if (buffer-file-name)
(apply oldfn args)
(setq header-line-format nil))))
;; Enable prefix argument for tab switching keybindings
(advice-add 'centaur-tabs-forward :around
(lambda (oldfn &rest args)
(if (numberp current-prefix-arg)
(dotimes (_ current-prefix-arg)
(apply oldfn args))
(apply oldfn args))))
(advice-add 'centaur-tabs-backward :around
(lambda (oldfn &rest args)
(if (numberp current-prefix-arg)
(dotimes (_ current-prefix-arg)
(apply oldfn args))
(apply oldfn args))))
;; Use Org-mode titles for tab names when possible
(advice-add 'centaur-tabs-buffer-tab-label :around
(lambda (oldfn tab &rest args)
(if-let ((title (or (and
(fboundp 'org-roam-db--get-titles)
(car
(org-roam-db--get-titles
(buffer-file-name (car tab)))))
(org-get-title
(with-current-buffer (car tab)
(buffer-substring (point-min)
(min (point-max) 200)))))))
(if (> centaur-tabs-label-fixed-length 0)
(centaur-tabs-truncate-string centaur-tabs-label-fixed-length
(format " %s" title))
(format " %s" title))
(apply oldfn tab args))))
;; Add a cache to speed up icon rendering for huge groups
(defvar centaur-tabs-icon-cache (make-hash-table :test 'equal)
"A cache holding icons generated for centaur-tabs mode tabs.")
(advice-add 'centaur-tabs-icon :around
(lambda (oldfn tab face selected &rest args)
(let ((key (list tab face selected)))
(or (gethash key centaur-tabs-icon-cache)
(puthash key
(apply oldfn tab face selected args)
centaur-tabs-icon-cache)))))
:general
((normal motion visual) "g t" #'centaur-tabs-forward)
((normal motion visual) "g T" #'centaur-tabs-backward)
(leader-map "pt" #'centaur-tabs-counsel-switch-group)
:hook
(git-commit-mode . (lambda ()
(when (centaur-tabs-mode-on-p)
(centaur-tabs-local-mode)))))
#+END_SRC
* Frame parameters
Functions to change the frame size:
#+BEGIN_SRC emacs-lisp
(defun jdormit/set-frame-size (width height)
(interactive "nWidth: \nnHeight: ")
(if (display-graphic-p)
(set-frame-size (selected-frame) width height)
(message "Not running graphically")))
(defun jdormit/set-frame-width (width)
(interactive "nWidth: ")
(jdormit/set-frame-size width (frame-height)))
(defun jdormit/set-frame-height (height)
(interactive "nHeight: ")
(jdormit/set-frame-size (frame-width) height))
#+END_SRC
Keybindings:
#+BEGIN_SRC emacs-lisp
(leader-def-key "Fw" 'jdormit/set-frame-width)
(leader-def-key "Fh" 'jdormit/set-frame-height)
(leader-def-key "Fs" 'jdormit/set-frame-size)
#+END_SRC
* Transpose Frame
A handy utility that reverse the current frame window split (vertical to horizontal or vice-versa):
#+BEGIN_SRC emacs-lisp
(use-package transpose-frame
:defer t
:init
(leader-def-key "wt" 'transpose-frame))
#+END_SRC
* EShell
Easy keybinding to open EShell:
#+BEGIN_SRC emacs-lisp
(defun open-eshell (&optional arg)
(interactive "P")
(if (and (fboundp 'projectile-project-root)
(projectile-project-root))
(projectile-run-eshell arg)
(eshell arg)))
(leader-def-key "'" 'open-eshell)
#+END_SRC
Make EShell's tab completion work like Bash's:
#+BEGIN_SRC emacs-lisp
(setq eshell-cmpl-cycle-completions nil)
#+END_SRC
Add additional useful modules:
#+BEGIN_SRC emacs-lisp
(with-eval-after-load 'esh-module
(add-to-list 'eshell-modules-list 'eshell-tramp))
(with-eval-after-load 'eshell
(require 'esh-module))
#+END_SRC
Prefer Lisp commands to external programs:
#+BEGIN_SRC emacs-lisp
(setq eshell-prefer-lisp-functions t
eshell-prefer-lisp-variables t)
#+END_SRC
Enable password caching:
#+BEGIN_SRC emacs-lisp
(setq password-cache t
password-cache-expiry 120)
#+END_SRC
Destroy shell buffers created by eshell when the process dies::
#+BEGIN_SRC emacs-lisp
(setq eshell-destroy-buffer-when-process-dies t)
#+END_SRC
Visual programs:
#+BEGIN_SRC emacs-lisp
(defun eshell-setup ()
(add-to-list 'eshell-visual-commands "crawl")
(add-to-list 'eshell-visual-commands "ssh")
(add-to-list 'eshell-visual-commands "watch")
(add-to-list 'eshell-visual-subcommands '("kubectl" "exec"))
(add-to-list 'eshell-visual-subcommands '("k" "exec"))
(add-to-list 'eshell-visual-subcommands '("docker" "build"))
(add-to-list 'eshell-visual-subcommands '("docker" "push")))
(add-hook 'eshell-mode-hook #'eshell-setup)
#+END_SRC
And a function to run any program visually:
#+BEGIN_SRC emacs-lisp
(defun eshell/v (&rest args)
(apply #'eshell-exec-visual args))
#+END_SRC
Load .dir-locals.el when switching directories:
#+BEGIN_SRC emacs-lisp
(add-hook 'eshell-mode-hook #'hack-dir-local-variables-non-file-buffer)
(add-hook 'eshell-after-prompt-hook #'hack-dir-local-variables-non-file-buffer)
#+END_SRC
A function to properly clear the eshell:
#+BEGIN_SRC emacs-lisp
(defun clear-eshell (&optional prefix)
(interactive)
(let ((input (eshell-get-old-input)))
(eshell/clear-scrollback)
(eshell-emit-prompt)
(insert input)))
(add-hook 'eshell-mode-hook
(lambda () (general-def eshell-mode-map "C-c C-o" #'clear-eshell)))
#+END_SRC
Compilation-shell-minor-mode:
#+BEGIN_SRC emacs-lisp
(add-hook 'eshell-mode-hook #'compilation-shell-minor-mode)
#+END_SRC
** Prompt
#+BEGIN_SRC emacs-lisp
(defun jdormit-eshell-prompt ()
(let ((branch (magit-name-local-branch "HEAD")))
(format "%s%s"
(if branch (format "(%s) " branch) "")
(concat (abbreviate-file-name (eshell/pwd))
" "
(propertize
(if (= (user-uid) 0) "#" "λ")
'face `(:foreground "#859900"))
" "))))
(setq jdormit-eshell-prompt-regex "^[^#λ\n]* [#λ] ")
(setq eshell-prompt-function 'jdormit-eshell-prompt)
(setq eshell-prompt-regexp jdormit-eshell-prompt-regex)
#+END_SRC
* Flycheck
Syntax checking etc.:
#+BEGIN_SRC emacs-lisp
(use-package flycheck
:init
(defhydra hydra-flycheck
(:pre (flycheck-list-errors)
:post (quit-windows-on "*Flycheck errors*")
:hint nil)
"Errors"
("f" flycheck-error-list-set-filter "Filter")
("j" flycheck-next-error "Next")
("k" flycheck-previous-error "Previous")
("gg" flycheck-first-error "First")
("G" (progn (goto-char (point-max)) (flycheck-previous-error)) "Last")
("q" nil))
:config
(setq-default flycheck-disabled-checkers '(emacs-lisp emacs-lisp-checkdoc))
(global-flycheck-mode)
:general
((normal motion visual) flycheck-mode-map "ze" 'hydra-flycheck/body))
#+END_SRC
* Tabs
#+BEGIN_SRC emacs-lisp
(defun disable-tab-insert ()
(interactive)
(setq indent-tabs-mode nil))
#+END_SRC
* aggressive-indent-mode
Like [[help:electric-indent-mode][electric-indent-mode]] but reindents after every change:
#+BEGIN_SRC emacs-lisp
(use-package aggressive-indent
:hook ((clojure-mode . aggressive-indent-mode)
(emacs-lisp-mode . aggressive-indent-mode)
(lisp-mode . aggressive-indent-mode)
(scheme-mode . aggressive-indent-mode)))
#+END_SRC
* Indentation guides
#+BEGIN_SRC emacs-lisp
(use-package highlight-indent-guides
:commands highlight-indent-guides-mode
:hook ((python-mode . highlight-indent-guides-mode)
(yaml-mode . highlight-indent-guides-mode))
:init
(leader-def-key "th" #'highlight-indent-guides-mode)
:custom
(highlight-indent-guides-method 'character)
(highlight-indent-guides-auto-character-face-perc 7)
(highlight-indent-guides-responsive 'stack)
(highlight-indent-guides-auto-stack-character-face-perc 10))
#+END_SRC
* JSON
#+BEGIN_SRC emacs-lisp
(use-package json-mode
:mode (("\\.json\\'" . json-mode))
:config
(setq js-indent-level 2))
(use-package json-navigator
:commands (json-navigator-navigator
json-navigator-navigate-after-point
json-navigator-navigate-region))
(use-package tree-mode
:general
(normal tree-mode-map
"D" #'tree-mode-delete-tree
"k" #'tree-mode-previous-node
"j" #'tree-mode-next-node
"l" #'tree-mode-next-sib
"h" #'tree-mode-previous-sib
"u" #'tree-mode-goto-parent
"r" #'tree-mode-goto-root
"gr" #'tree-mode-reflesh
"E" #'tree-mode-expand-level
"e" #'tree-mode-toggle-expand
"s" #'tree-mode-sort-by-tag
"/" #'tree-mode-keep-match
"!" #'tree-mode-collapse-other-except)
:hook
((json-navigator-mode . tree-minor-mode)))
(defun json-pprint ()
(interactive)
(let ((begin (if (region-active-p) (region-beginning) (point-min)))
(end (if (region-active-p) (region-end) (point-max))))
(if (executable-find "jq")
(shell-command-on-region begin end "jq ." nil t)
(json-pretty-print begin end))))
(general-def json-mode-map "C-M-\\" 'json-pprint)
#+END_SRC
* JavaScript
Some formatting stuff:
#+BEGIN_SRC emacs-lisp
(setq js-indent-level 4)
#+END_SRC
#+BEGIN_SRC emacs-lisp
(use-package web-mode
:mode (("\\.html\\'" . web-mode)
("\\.js\\'" . web-mode)
("\\.jsx\\'" . web-mode)
("\\.mako\\'" . web-mode)
("\\.jinja2\\'" . web-mode)
("\\.hbs\\'" . web-mode))
:config
(setq web-mode-engines-alist
'(("django" . "\\.jinja2\\'")))
(add-hook 'web-mode-hook
(lambda ()
(when (equal web-mode-content-type "javascript")
(web-mode-set-content-type "jsx"))
(when (or (equal web-mode-content-type "javascript")
(equal web-mode-content-type "jsx"))
(lsp-deferred))))
(add-hook 'web-mode-hook #'disable-tab-insert)
:custom
(web-mode-enable-auto-pairing nil))
#+END_SRC
Use nvm to manage node versions:
#+BEGIN_SRC emacs-lisp
(use-package nvm
:straight (nvm :host github :repo "rejeep/nvm.el")
:commands (nvm-use nvm-use-for nvm-use-for-buffer))
#+END_SRC
A command to format JS via prettier:
#+BEGIN_SRC emacs-lisp
(defun prettier ()
(interactive)
(let ((start (if (use-region-p) (region-beginning) (point-min)))
(end (if (use-region-p) (region-end) (point-max)))
(parser (cond
((eq major-mode 'graphql-mode) "graphql")
(t "babel"))))
(shell-command-on-region start end (concat "prettier --parser " parser) nil t)))
#+END_SRC
** NVM
Manage node version via NVM within Emacs:
#+BEGIN_SRC emacs-lisp
(use-package nvm
:commands (nvm-use
nvm-use-for
nvm-use-for-buffer
nvm--installed-versions)
:init
(defun nvm (version)
(interactive (list
(completing-read "Node version: "
(mapcar #'car
(nvm--installed-versions)))))
(nvm-use version)))
#+END_SRC
* Java
#+BEGIN_SRC emacs-lisp
(use-package lsp-java
:hook ((java-mode . lsp-deferred)))
#+END_SRC
* Kotlin
#+BEGIN_SRC emacs-lisp
(use-package kotlin-mode
:mode (("\\.kt\\'" . kotlin-mode))
:config
(with-eval-after-load 'lsp
(when (executable-find "kotlin-language-server")
(add-hook 'kotlin-mode-hook #'lsp-deferred))))
#+END_SRC
* Groovy
Used for Jenkins configuration scripts and probably other things.
#+BEGIN_SRC emacs-lisp
(use-package groovy-mode
:commands (groovy-mode)
:mode (("\\.groovy\\'" . groovy-mode))
:init
(add-to-list 'interpreter-mode-alist
'("groovy" . groovy-mode))
:custom
(groovy-indent-offset 2))
#+END_SRC
* Typescript
#+BEGIN_SRC emacs-lisp
(use-package typescript-mode
:mode ("\\.ts\\'")
:config
(with-eval-after-load 'lsp
(add-hook 'typescript-mode-hook 'lsp-deferred)))
#+END_SRC
* LSP Mode
Emacs support for the Language Server Protocol
#+BEGIN_SRC emacs-lisp
(use-package lsp-mode
:defer t
:init
(defhydra hydra-lsp (:exit t :hint nil)
"
Buffer^^ Server^^ Symbol
-------------------------------------------------------------------------------------
[_f_] format [_M-r_] restart [_d_] declaration [_i_] implementation [_o_] documentation
[_m_] imenu [_S_] shutdown [_D_] definition [_t_] type [_r_] rename
[_x_] execute action [_M-s_] describe session [_R_] references [_s_] signature"
("d" lsp-find-declaration)
("D" lsp-ui-peek-find-definitions)
("R" lsp-ui-peek-find-references)
("i" lsp-ui-peek-find-implementation)
("t" lsp-find-type-definition)
("s" lsp-signature-help)
("o" lsp-describe-thing-at-point)
("r" lsp-rename)
("f" lsp-format-buffer)
("m" lsp-ui-imenu)
("x" lsp-execute-code-action)
("M-s" lsp-describe-session)
("M-r" lsp-restart-workspace)
("S" lsp-shutdown-workspace))
:general
(lsp-mode-map "C-c h" 'hydra-lsp/body)
((normal visual motion) lsp-mode-map "K" #'lsp-describe-thing-at-point)
:hook
((lsp-mode . (lambda ()
(let ((lsp-keymap-prefix "gl"))
(lsp-enable-which-key-integration)))))
:config
(setq lsp-prefer-flymake nil)
(general-def '(normal visual motion) "gl" lsp-command-map)
:commands lsp-mode lsp lsp-deferred
:custom
(lsp-enable-snippet nil)
(lsp-eldoc-render-all nil)
(lsp-headerline-breadcrumb-enable nil)
(lsp-file-watch-threshold 100000))
(use-package lsp-ui
:after (lsp-mode)
:custom
(lsp-ui-sideline-enable t)
(lsp-ui-sideline-show-symbol t)
(lsp-ui-sideline-show-hover t)
(lsp-ui-sideline-show-code-actions t)
(lsp-ui-sideline-update-mode 'point)
(lsp-ui-doc-alignment 'window)
(lsp-ui-doc-header t)
(lsp-ui-doc-position 'top)
(lsp-ui-doc-background '((t (:inherit region))))
(lsp-ui-doc-header '((t (:inherit lsp-face-highlight-write))))
(lsp-ui-sideline-current-symbol '((t (:inherit font-lock-constant-face
:weight ultra-bold)))))
(with-eval-after-load 'lsp-clients
(defun lsp-typescript-javascript-tsx-jsx-activate-p (filename major-mode)
"Checks if the javascript-typescript language server should be enabled
based on FILE-NAME and MAJOR-MODE"
(or (member major-mode '(typescript-mode typescript-tsx-mode js-mode js2-mode rjsx-mode))
(and (eq major-mode 'web-mode)
(or (string-suffix-p ".tsx" filename t)
(string-suffix-p ".jsx" filename t)
(string-suffix-p ".js" filename t))))))
#+END_SRC
* Python
** General
#+BEGIN_SRC emacs-lisp
(leader-def-key "sp" #'run-python)
(add-hook 'python-mode-hook #'disable-tab-insert)
#+END_SRC
ISort is a Python utility to sort imports:
#+BEGIN_SRC emacs-lisp
(use-package py-isort
:commands (py-isort-buffer py-isort-region)
:config
(setq py-isort-options '("-m=3"))
:general
(python-mode-map "C-c C-i" #'py-isort-buffer))
#+END_SRC
Run black on the current buffer:
#+BEGIN_SRC emacs-lisp
(general-def 'normal python-mode-map "C-M-\\" #'format-all-buffer)
#+END_SRC
[[https://github.com/naiquevin/sphinx-doc.el][sphinx-doc.el]] automatically generates doc strings for Python functions (and updates existing ones!):
#+BEGIN_SRC emacs-lisp
(use-package sphinx-doc
:hook ((python-mode . sphinx-doc-mode)))
#+END_SRC
** Dev environment/IDE stuff
Support pyvenv within Emacs:
#+BEGIN_SRC emacs-lisp
(use-package pyvenv
:defer 0
:commands (pyvenv-mode
pyvenv-tracking-mode
pyvenv-workon
pyvenv-activate
pyvenv-track-virtualenv)
:config
(pyvenv-mode)
(pyvenv-tracking-mode))
(defun eshell/workon (name)
(pyvenv-workon name))
(defun eshell/activate (dir)
(pyvenv-activate dir))
(defun eshell/deactivate ()
(pyvenv-deactivate))
#+END_SRC
And support pyenv (NOT pyvenv) to change Python versions:
#+BEGIN_SRC emacs-lisp
(use-package pyenv-mode
:defer t)
#+END_SRC
Use the LSP python client:
#+BEGIN_SRC emacs-lisp
(use-package lsp-pyright
:hook (python-mode . (lambda ()
(require 'lsp-pyright)
(lsp-deferred)))
:custom
(lsp-pyright-use-library-code-for-types t)
:general
(python-mode-map "C-c C-d" #'lsp-describe-thing-at-point)
('normal python-mode-map "K" #'lsp-describe-thing-at-point))
#+END_SRC
Override the flycheck python-mypy checker to run in the right
directory:
#+BEGIN_SRC emacs-lisp
(flycheck-define-checker python-mypy
"Mypy syntax and type checker. Requires mypy>=0.580.
See URL `http://mypy-lang.org/'."
:command ("mypy"
"--show-column-numbers"
(config-file "--config-file" flycheck-python-mypy-config)
(option "--cache-dir" flycheck-python-mypy-cache-dir)
source-original)
:error-patterns
((error line-start (file-name) ":" line (optional ":" column)
": error:" (message) line-end)
(warning line-start (file-name) ":" line (optional ":" column)
": warning:" (message) line-end)
(info line-start (file-name) ":" line (optional ":" column)
": note:" (message) line-end))
:modes python-mode
;; Ensure the file is saved, to work around
;; https://github.com/python/mypy/issues/4746.
:predicate flycheck-buffer-saved-p
:working-directory (lambda (checker)
(projectile-compilation-dir)))
#+END_SRC
** Autoflake
[[https://pypi.org/project/autoflake/][Autoflake]] is a tool that removes unused imports and variables from Python code:
#+BEGIN_SRC emacs-lisp
(defvar autoflake-args '()
"Arguments to pass to the autoflake command.
See URL `https://pypi.org/project/autoflake' for options.")
(defun autoflake (arg)
(interactive "P")
(let ((autoflake-cmd (or (executable-find "autoflake")
(error "Autoflake executable not found")))
(file (cond
((or arg (not buffer-file-name))
(read-file-name "Run autoflake on file: "))
(buffer-file-name buffer-file-name)
(:else (error "Invalid file for autoflake")))))
(apply #'call-process autoflake-cmd nil nil nil
(append autoflake-args (list "--in-place" file)))))
#+END_SRC
** Testing
[[https://github.com/wbolster/emacs-python-pytest][python-pytest.el]] integrates Pytest with Emacs:
#+BEGIN_SRC emacs-lisp
(use-package python-pytest
:commands (python-pytest-popup
python-pytest--current-defun)
:general
(python-mode-map "C-c t p" #'python-pytest-popup)
((normal motion visual) python-pytest-mode-map "g r" #'python-pytest-repeat)
((normal motion visual) python-pytest-mode-map "q" #'quit-window))
#+END_SRC
And borrowing some functions from python-pytest, we can get some nosetests support as well:
#+BEGIN_SRC emacs-lisp
(defvar nosetests-args ""
"Additional args to pass to nosetests")
(defun run-nose (args &optional debug)
"Runs nosetests with `args'
If `debug' is non-nil, run in a GUD PDB session."
(let* ((nosetests-cmd (executable-find "nosetests"))
(cmdline (format "%s %s" nosetests-cmd args)))
(when (not nosetests-cmd)
(user-error "Nosetests command not found"))
(if debug
(realgud:pdb cmdline)
(compile cmdline))))
(defun run-nose-reading-args (arg nose-args &optional debug)
"Runs nosetests with default args or prompts for args with prefix
If `debug' is non-nil, run in a GUD PDB session."
(let ((args (if arg
(read-string "Nosetests arguments: "
nil nil nosetests-args)
nosetests-args)))
(run-nose (format "%s %s" args nose-args) debug)))
(defun run-nose-in-project (arg nose-args &optional debug)
"Runs nosetests from the project root
If `debug' is non-nil, run in a GUD PDB session."
(let ((dir (or (projectile-project-root)
default-directory)))
(with-temp-buffer
(cd dir)
(run-nose-reading-args arg nose-args debug))))
(defun nosetests-all (arg)
"Runs nosetests on all project tests, prompting for the tests directory"
(interactive "P")
(let ((test-dir (read-file-name "Test directory: "
(or (projectile-project-root)
default-directory)
nil t "tests" #'directory-name-p)))
(run-nose-in-project arg (directory-file-name test-dir))))
(defun nosetests-debug-all (arg)
"Runs nosetests in a GUD session on all project tests"
(interactive "P")
(let ((test-dir (read-file-name "Test directory: "
(or (projectile-project-root)
default-directory)
nil t "tests" #'directory-name-p)))
(run-nose-in-project arg (directory-file-name test-dir) t)))
(defun nosetests-module (arg module)
"Runs nosetests in the module of the current file"
(interactive (list current-prefix-arg
(read-file-name "Run nosetests on module: "
nil nil t
(file-name-directory
(buffer-file-name))
#'directory-name-p)))
(run-nose-in-project arg (directory-file-name module)))
(defun nosetests-debug-module (arg module)
"Runs nosetests in a GUD session in the module of the current file"
(interactive (list current-prefix-arg
(read-file-name "Run nosetests on module: "
nil nil t
(file-name-directory
(buffer-file-name))
#'directory-name-p)))
(run-nose-in-project arg (directory-file-name module) t))
(defun nosetests-file (arg file)
"Runs nosetests on the current file"
(interactive (list current-prefix-arg
(read-file-name "Run nosetests on file: "
nil nil t (buffer-file-name))))
(run-nose-in-project arg file))
(defun nosetests-debug-file (arg file)
"Runs nosetests in a GUD session on the current file"
(interactive (list current-prefix-arg
(read-file-name "Run nosetests on file: "
nil nil t (buffer-file-name))))
(run-nose-in-project arg file t))
(defun nosetests-def (arg def)
"Runs nosetests on the enclosing function at point"
(interactive (list current-prefix-arg
(python-pytest--current-defun)))
(run-nose-in-project arg (format "%s:%s"
(buffer-file-name)
def)))
(defun nosetests-debug-def (arg def)
"Runs nosetests in a GUD session on the enclosing function at point"
(interactive (list current-prefix-arg
(python-pytest--current-defun)))
(run-nose-in-project arg
(format "%s:%s"
(buffer-file-name)
def)
t))
(defvar nosetests-map (make-sparse-keymap)
"Keymap for nosetests")
(defvar nosetests-debug-map (make-sparse-keymap)
"Keymap for debugging nosetests")
(general-def python-mode-map "C-c t n" nosetests-map)
(general-def nosetests-map "a" #'nosetests-all)
(general-def nosetests-map "m" #'nosetests-module)
(general-def nosetests-map "f" #'nosetests-file)
(general-def nosetests-map "d" #'nosetests-def)
(general-def nosetests-map "b" nosetests-debug-map)
(general-def nosetests-debug-map "a" #'nosetests-debug-all)
(general-def nosetests-debug-map "m" #'nosetests-debug-module)
(general-def nosetests-debug-map "f" #'nosetests-debug-file)
(general-def nosetests-debug-map "d" #'nosetests-debug-def)
(which-key-add-major-mode-key-based-replacements
'python-mode "C-c t n" "nosetests")
(which-key-add-major-mode-key-based-replacements
'python-mode "C-c t n b" "debug")
#+END_SRC
* Hy
Python but Lispy!
#+BEGIN_SRC emacs-lisp
(defun run-hy ()
(interactive)
(run-lisp (expand-file-name "~/.virtualenvs/hy/bin/hy")))
#+END_SRC
* Go
Basic support:
#+BEGIN_SRC emacs-lisp
(use-package go-mode
:mode (("\\.go\\'" . go-mode)))
#+END_SRC
LSP support - requires [[https://github.com/sourcegraph/go-langserver][go-langserver]].
#+BEGIN_SRC emacs-lisp
(add-hook 'go-mode-hook #'lsp-deferred)
#+END_SRC
* Clojure
Start with clojure-mode:
#+BEGIN_SRC emacs-lisp
(use-package clojure-mode
:mode (("\\.clj\\'" . clojure-mode)
("\\.cljs\\'" . clojurescript-mode)
("\\.cljc\\'" . clojurec-mode)
("\\.edn\\'" . clojure-mode))
:config
(define-clojure-indent
(defroutes 'defun)
(GET 2)
(POST 2)
(PUT 2)
(DELETE 2)
(HEAD 2)
(ANY 2)
(OPTIONS 2)
(PATCH 2)
(rfn 2)
(let-routes 1)
(context 2)
(:= 3)
(:+ 3)
(match 1)
(for-all 2)
(checking 2)
(let-flow 1)))
#+END_SRC
Add flycheck support:
#+BEGIN_SRC emacs-lisp
(use-package flycheck-clj-kondo
:after (clojure-mode)
:if (executable-find "clj-kondo")
:ensure t)
#+END_SRC
Sprinkle in some CIDER:
#+BEGIN_SRC emacs-lisp
(use-package cider
:commands (cider-mode cider-jack-in cider-jack-in-clojurescript)
:config
(setq cider-known-endpoints
'(("local" "localhost" "4005"))
cider-prompt-for-symbol nil)
(general-def cider-mode-map "C-c t" cider-test-commands-map)
(add-hook 'cider-repl-mode-hook 'smartparens-strict-mode)
:hook ((clojure-mode . cider-mode)
(clojurescript-mode . cider-mode)
(clojurec-mode . cider-mode))
:general
(cider-stacktrace-mode-map "SPC" leader-map)
('normal cider-mode-map "M-." #'cider-find-var)
(cider-repl-mode-map "C-c C-l" #'cider-repl-clear-buffer))
(defun jdormit/cider-setup ()
(local-set-key (kbd "C-c M-b") 'cider-debug-defun-at-point))
(add-hook 'cider-mode-hook 'jdormit/cider-setup)
#+END_SRC
Add some handy hydras:
#+BEGIN_SRC emacs-lisp
(use-package cider-hydra
:after (cider)
:hook ((cider-mode . cider-hydra-mode)))
#+END_SRC
Clj-refactor adds magical refactoring abilities:
#+BEGIN_SRC emacs-lisp
(use-package clj-refactor
:init
(defun clj-refactor-setup ()
(interactive)
(clj-refactor-mode 1)
(cljr-add-keybindings-with-prefix "C-c r"))
:config
(setq cljr-auto-sort-ns t)
:hook ((clojure-mode . clj-refactor-setup)))
#+END_SRC
Add support for running Org-mode Clojure source blocks with [[https://github.com/borkdude/babashka][Babashka]]:
#+BEGIN_SRC emacs-lisp
(with-eval-after-load 'ob-clojure
(defcustom org-babel-clojure-backend nil
"Backend used to evaluate Clojure code blocks."
:group 'org-babel
:type '(choice
(const :tag "inf-clojure" inf-clojure)
(const :tag "cider" cider)
(const :tag "slime" slime)
(const :tag "bb" bb)
(const :tag "Not configured yet" nil)))
(defun elisp->clj (in)
(cond
((listp in) (concat "[" (s-join " " (mapcar #'elisp->clj in)) "]"))
(t (format "%s" in))))
(defun ob-clojure-eval-with-bb (expanded params)
"Evaluate EXPANDED code block with PARAMS using babashka."
(unless (executable-find "bb")
(user-error "Babashka not installed"))
(let* ((stdin (let ((stdin (cdr (assq :stdin params))))
(when stdin
(elisp->clj
(org-babel-ref-resolve stdin)))))
(input (cdr (assq :input params)))
(file (make-temp-file "ob-clojure-bb" nil nil expanded))
(command (concat (when stdin (format "echo %s | " (shell-quote-argument stdin)))
(format "bb %s -f %s"
(cond
((equal input "edn") "")
((equal input "text") "-i")
(t ""))
(shell-quote-argument file))))
(result (shell-command-to-string command)))
(s-trim result)))
(defun org-babel-execute:clojure (body params)
"Execute a block of Clojure code with Babel."
(let* ((org-babel-clojure-backend (or (cdr (assq :backend params))
org-babel-clojure-backend))
(org-babel-clojure-backend (when org-babel-clojure-backend
(intern org-babel-clojure-backend))))
(unless org-babel-clojure-backend
(user-error "You need to customize org-babel-clojure-backend"))
(let* ((expanded (org-babel-expand-body:clojure body params))
(result-params (cdr (assq :result-params params)))
result)
(setq result
(cond
((eq org-babel-clojure-backend 'inf-clojure)
(ob-clojure-eval-with-inf-clojure expanded params))
((eq org-babel-clojure-backend 'cider)
(ob-clojure-eval-with-cider expanded params))
((eq org-babel-clojure-backend 'slime)
(ob-clojure-eval-with-slime expanded params))
((eq org-babel-clojure-backend 'bb)
(ob-clojure-eval-with-bb expanded params))))
(org-babel-result-cond result-params
result
(condition-case nil (org-babel-script-escape result)
(error result))))))
(customize-set-variable 'org-babel-clojure-backend 'bb))
(add-hook 'org-mode-hook (lambda () (require 'ob-clojure)))
#+END_SRC
* Scheme
Tell emacs about file extensions which should activate scheme-mode:
#+BEGIN_SRC emacs-lisp
(add-to-list 'auto-mode-alist '("\\.guile\\'" . scheme-mode))
(add-to-list 'auto-mode-alist '("\\.rkt\\'" . scheme-mode))
#+END_SRC
[[http://www.nongnu.org/geiser/geiser_1.html][Geiser]] is a Scheme IDE for Emacs that supports a bunch of common Scheme implementations.
#+BEGIN_SRC emacs-lisp
(use-package geiser
:commands (run-geiser)
:config
(setq geiser-active-implementations
(cl-reduce (lambda (acc val)
(if (executable-find (symbol-name val))
(cons val acc)
acc))
'(guile racket chicken chez mit chibi gambit)
:initial-value nil))
:general
(geiser-mode-map 'normal
"M-." 'geiser-edit-symbol-at-point
"M-," 'geiser-pop-symbol-stack)
(geiser-debug-mode-map "SPC" leader-map))
#+END_SRC
And a handy shortcut to hop into a Geiser REPL:
#+BEGIN_SRC emacs-lisp
(leader-def-key "sg" 'run-geiser)
#+END_SRC
* Common Lisp
[[https://common-lisp.net/project/slime/][SLIME]] is a set of modes and utilities for writing Common Lisp in Emacs. [[https://www.quicklisp.org/beta/][Quicklisp]] is the de-facto Common Lisp package manager. It comes with some Emacs bindings.
#+BEGIN_SRC emacs-lisp
(use-package slime-company
:after (slime))
(use-package slime
:commands (slime)
:config
(setq inferior-lisp-program
(executable-find "sbcl")
slime-contribs '(slime-repl
slime-fancy
slime-company))
(when (file-exists-p
(expand-file-name "~/quicklisp/slime-helper.el"))
(load (expand-file-name "~/quicklisp/slime-helper.el")))
(add-hook 'slime-repl-mode-hook 'smartparens-strict-mode)
(add-to-list 'browse-url-browser-function
'("lispworks.com/documentation/HyperSpec" . w3m-browse-url))
:general
((normal motion visual insert emacs) slime-mode-map "M-." #'slime-edit-definition))
(add-to-list 'auto-mode-alist '("\\.cl\\'" . lisp-mode))
#+END_SRC
Keyboard shortcut to start a SLIME REPL:
#+BEGIN_SRC emacs-lisp
(leader-def-key "sc" 'slime)
#+END_SRC
* Haskell
#+BEGIN_SRC emacs-lisp
(defun jdormit/haskell-setup ()
(local-set-key (kbd "C-c M-j") 'interactive-haskell-mode))
(use-package haskell-mode
:mode (("\\.hs\\'" . haskell-mode)))
(add-hook 'haskell-mode-hook 'jdormit/haskell-setup)
#+END_SRC
* PHP
#+BEGIN_SRC emacs-lisp
(use-package php-mode
:mode "\\.php\\'")
(use-package mmm-mode
:after php-mode)
#+END_SRC
Geben is an interface to XDebug allowing debugging PHP in Emacs:
#+BEGIN_SRC emacs-lisp
(use-package geben
:commands (geben))
#+END_SRC
Some keybindings to start and end Geben:
#+BEGIN_SRC emacs-lisp
(general-def php-mode-map "C-c C-d" #'geben)
(general-def php-mode-map "C-c C-q" #'geben-end)
#+END_SRC
LSP for PHP requires [[https://github.com/felixfbecker/php-language-server][php-language-server]] to be installed in ~/.composer:
#+BEGIN_SRC emacs-lisp
(add-hook 'php-mode-hook #'lsp-deferred)
#+END_SRC
* YAML
#+BEGIN_SRC emacs-lisp
(use-package yaml-mode
:mode ("//.yml//'"))
#+END_SRC
* Pharen
[[https://pharen.org][Pharen]] is a Lisp that compiles to PHP. It looks a lot like Clojure.
#+BEGIN_SRC emacs-lisp
(add-to-list 'auto-mode-alist '("\\.phn\\'" . clojure-mode))
#+END_SRC
* Bash
Use LSP if [[https://github.com/mads-hartmann/bash-language-server][bash-language-server]] is installed.
#+BEGIN_SRC emacs-lisp
(when (executable-find "bash-language-server")
(add-hook 'sh-mode-hook #'lsp-deferred))
#+END_SRC
* Ruby
#+BEGIN_SRC emacs-lisp
(add-hook 'ruby-mode-hook #'lsp-deferred)
#+END_SRC
* Rust
#+BEGIN_SRC emacs-lisp
(use-package rust-mode
:mode "\\.rs\\'"
:general
(rust-mode-map "C-c <tab>" #'rust-format-buffer)
:config
(add-hook 'rust-mode-hook #'lsp-deferred))
(use-package cargo
:after (rust-mode)
:config
(add-hook 'rust-mode-hook #'cargo-minor-mode))
#+END_SRC
* Elixir
#+BEGIN_SRC emacs-lisp
(use-package elixir-mode
:defer t)
(use-package alchemist
:hook ((elixir-mode . alchemist-mode)
(alchemist-iex-mode . company-mode)))
#+END_SRC
* XML
Set up hideshow for nXML mode:
#+BEGIN_SRC emacs-lisp
(add-hook 'nxml-mode-hook #'hs-minor-mode)
(add-to-list 'hs-special-modes-alist
'(nxml-mode
"<!--\\|<[^/>]*[^/]>" ;; regexp for start block
"-->\\|</[^/>]*[^/]>" ;; regexp for end block
"<!--"
nxml-forward-element
nil))
#+END_SRC
A function to format XML using tidy or xmllint if available, falling back to sgml-pretty-print:
#+BEGIN_SRC emacs-lisp
(defun xml-pretty-print ()
(interactive)
(let ((start (if (region-active-p) (region-beginning) (point-min)))
(end (if (region-active-p) (region-end) (point-max))))
(cond
((executable-find "tidy")
(shell-command-on-region start end "tidy -wrap 88 -q -i -xml" nil t))
((executable-find "xmllint")
(shell-command-on-region start end "xmllint --format -" nil t))
(t (sgml-pretty-print start end)))))
(general-def nxml-mode-map "C-M-\\" #'xml-pretty-print)
#+END_SRC
* CSVs
#+BEGIN_SRC emacs-lisp
(use-package csv-mode
:mode "\\.csv\\'")
#+END_SRC
* Markdown
#+BEGIN_SRC emacs-lisp
(use-package markdown-mode
:commands (markdown-mode gfm-mode)
:mode (("README\\.md\\'" . gfm-mode)
("\\.md\\'" . markdown-mode)
("\\.markdown\\'" . markdown-mode))
:init (setq markdown-command "pandoc"))
#+END_SRC
Edit-indirect allows markdown-mode to edit source blocks in separate buffers:
#+BEGIN_SRC emacs-lisp
(use-package edit-indirect
:defer t)
#+END_SRC
* Terraform
#+BEGIN_SRC emacs-lisp
(use-package terraform-mode
:mode "\\.tf\\'")
#+END_SRC
* IELM
Enable lexical scoping in IELM:
#+BEGIN_SRC emacs-lisp
(add-hook 'ielm-mode-hook
#'(lambda ()
(interactive)
(setq lexical-binding t)))
#+END_SRC
* Ledger Mode
This mode requires that [[https://github.com/ledger/ledger][ledger]] be installed on the system.
#+BEGIN_SRC emacs-lisp
(use-package ledger-mode
:mode "\\.ledger\\'"
:config
(add-to-list 'evil-emacs-state-modes 'ledger-report-mode)
(general-def ledger-report-mode-map "SPC" leader-map)
(add-hook 'ledger-mode-hook
(lambda ()
(variable-pitch-mode 0)))
(add-hook 'ledger-report-mode-hook
(lambda ()
(variable-pitch-mode 0))))
#+END_SRC
** Importing
#+BEGIN_SRC emacs-lisp
(defvar ledger-file (expand-file-name "~/journal.ledger"))
(defvar bank-alist
'(("DCU Checking" . ((acct . "Assets:Checking")
(fid . "9999")))
("DCU Savings" . ((acct . "Assets:Savings")
(fid . "9999")))
("DCU Visa" . ((acct . "Liabilities:DCU Visa")
(fid . "9999")))
("Chase Visa" . ((acct . "Liabilities:Chase Visa")))))
(defun ledger-import-ofx (bank file)
(interactive
(list
(completing-read "Bank: "
(mapcar #'car bank-alist))
(read-file-name "OFX file: ")))
(if-let ((ledger-autosync (executable-find "ledger-autosync")))
(let* ((bank-def (alist-get bank bank-alist))
(acct (alist-get 'acct bank-def))
(fid (alist-get 'fid bank-def))
(cmd (concat
ledger-autosync
(if fid (concat " --fid " fid) "")
" --account '" acct "'"
" '" file "'"))
(output (shell-command-to-string cmd)))
(find-file ledger-file)
(goto-char (point-max))
(insert "\n")
(insert output)
(ledger-sort-region (point-min) (point-max))
(ledger-post-align-postings (point-min) (point-max)))
(error "Unable to find ledger-autosync")))
#+END_SRC
* PDFs
#+BEGIN_SRC emacs-lisp
(use-package pdf-tools
:mode ("\\.pdf\\'" . pdf-view-mode)
:init
(defhydra hydra-pdftools (:color blue :hint nil)
"
╭───────────┐
Move History Scale/Fit Annotations Search/Link Do │ PDF Tools │
╭──────────────────────────────────────────────────────────────────┴───────────╯
^^_g_^^ _B_ ^↧^ _+_ ^ ^ [_al_] list [_s_] search [_u_] revert buffer
^^^↑^^^ ^↑^ _H_ ^↑^ ↦ _W_ ↤ [_am_] markup [_o_] outline [_i_] info
^^_p_^^ ^ ^ ^↥^ _0_ ^ ^ [_at_] text [_F_] link [_d_] dark mode
^^^↑^^^ ^↓^ ╭─^─^─┐ ^↓^ ╭─^ ^─┐ [_ad_] delete [_f_] search link
_h_ ←pag_e_→ _l_ _N_ │ _P_ │ _-_ _b_ [_aa_] dired
^^^↓^^^ ^ ^ ╰─^─^─╯ ^ ^ ╰─^ ^─╯ [_y_] yank
^^_n_^^ ^ ^ _r_eset slice box
^^^↓^^^
^^_G_^^
--------------------------------------------------------------------------------
"
("\\" hydra-master/body "back")
("<ESC>" nil "quit")
("al" pdf-annot-list-annotations)
("ad" pdf-annot-delete)
("aa" pdf-annot-attachment-dired)
("am" pdf-annot-add-markup-annotation)
("at" pdf-annot-add-text-annotation)
("y" pdf-view-kill-ring-save)
("+" pdf-view-enlarge :color red)
("-" pdf-view-shrink :color red)
("0" pdf-view-scale-reset)
("H" pdf-view-fit-height-to-window)
("W" pdf-view-fit-width-to-window)
("P" pdf-view-fit-page-to-window)
("n" pdf-view-next-page-command :color red)
("p" pdf-view-previous-page-command :color red)
("d" pdf-view-dark-minor-mode)
("b" pdf-view-set-slice-from-bounding-box)
("r" pdf-view-reset-slice)
("g" pdf-view-first-page)
("G" pdf-view-last-page)
("e" pdf-view-goto-page)
("o" pdf-outline)
("s" pdf-occur)
("i" pdf-misc-display-metadata)
("u" pdf-view-revert-buffer)
("F" pdf-links-action-perform)
("f" pdf-links-isearch-link)
("B" pdf-history-backward :color red)
("N" pdf-history-forward :color red)
("l" image-forward-hscroll :color red)
("h" image-backward-hscroll :color red))
:config
(pdf-tools-install)
:general
(pdf-view-mode-map "SPC" leader-map)
(normal pdf-view-mode-map "." #'hydra-pdftools/body)
(normal pdf-view-mode-map "F" #'pdf-links-action-perform)
(normal pdf-view-mode-map "C-o" #'pdf-history-backward))
#+END_SRC
* EPubs
#+BEGIN_SRC emacs-lisp
(defun jdormit/nov-config ()
(olivetti-mode)
(nov-render-document))
(use-package nov
:mode ("\\.epub\\'" . nov-mode)
:config
(setq nov-text-width 80)
(add-hook 'nov-mode-hook 'jdormit/nov-config)
:general
('normal nov-mode-map "r" 'nov-render-document)
('normal nov-mode-map "=" 'nov-view-source)
('normal nov-mode-map "+" 'nov-view-content-source)
('normal nov-mode-map "m" 'nov-display-metadata)
('normal nov-mode-map "n" 'nov-next-document)
('normal nov-mode-map "]" 'nov-next-document)
('normal nov-mode-map "p" 'nov-previous-document)
('normal nov-mode-map "[" 'nov-previous-document)
('normal nov-mode-map "t" 'nov-goto-toc)
('normal nov-mode-map "RET" 'nov-browse-url)
('normal nov-mode-map "<follow-link>" 'mouse-face)
('normal nov-mode-map "<mouse-2>" 'nov-browse-url)
('normal nov-mode-map "TAB" 'shr-next-link)
('normal nov-mode-map "M-TAB" 'shr-previous-link)
('normal nov-mode-map "<backtab>" 'shr-previous-link)
('normal nov-mode-map "SPC" 'nov-scroll-up)
('normal nov-mode-map "S-SPC" 'nov-scroll-down)
('normal nov-mode-map "DEL" 'nov-scroll-down)
('normal nov-mode-map "<home>" 'beginning-of-buffer)
('normal nov-mode-map "<end>" 'end-of-buffer)
('normal nov-mode-map "SPC" leader-map))
#+END_SRC
** Org mode links
First, keep a reference to filename of the .epub before nov.el blows it away:
#+BEGIN_SRC emacs-lisp
(defvar nov-epub-file)
(add-hook 'nov-mode-hook
#'(lambda ()
(message "epub file: " buffer-file-name)
(setq nov-epub-file buffer-file-name)))
#+END_SRC
That reference lets us construct a link back to the .epub:
#+BEGIN_SRC emacs-lisp
(defun org-epub-store-link ()
(when (eq major-mode 'nov-mode)
(let ((epub-name (alist-get 'title nov-metadata)))
(org-store-link-props
:type "file"
:link (concat "file://" nov-epub-file)))))
(add-hook 'org-store-link-functions 'org-epub-store-link)
#+END_SRC
* Dashboard
Instead of the *GNU Emacs* buffer on startup, display a cool dashboard:
#+BEGIN_SRC emacs-lisp
(use-package dashboard
:general
(dashboard-mode-map "SPC" leader-map)
:config
(with-eval-after-load 'evil
(add-to-list 'evil-emacs-state-modes 'dashboard-mode))
(setq dashboard-items '((recents . 5)
(projects . 5))
dashboard-startup-banner 'official
dashboard-set-heading-icons t
dashboard-set-file-icons t
dashboard-set-navigator t))
(dashboard-setup-startup-hook)
#+END_SRC
And hide some files from the recentf list:
#+BEGIN_SRC emacs-lisp
(with-eval-after-load 'recentf
(setq recentf-exclude '(".*gcal.org"
#'backup-file-name-p)))
#+END_SRC
* Email
I use [[https://www.djcbsoftware.nl/code/mu/mu4e.html][mu/mu4e]] as my email client.
First, add it to the load path:
#+BEGIN_SRC emacs-lisp
(defvar mu4e-load-path)
(setq mu4e-load-path
(cond
((file-exists-p "/usr/local/share/emacs/site-lisp/mu/mu4e")
"/usr/local/share/emacs/site-lisp/mu/mu4e")
((file-exists-p "/usr/share/emacs/site-lisp/mu4e")
"/usr/share/emacs/site-lisp/mu4e")))
(add-to-list 'load-path mu4e-load-path)
#+END_SRC
Then set up autoloads on the entry functions:
#+BEGIN_SRC emacs-lisp
(autoload 'mu4e (concat mu4e-load-path "/mu4e.el") nil t)
(autoload 'mu4e-update-index (concat mu4e-load-path "/mu4e.el") nil t)
#+END_SRC
Then configure it:
#+begin_src emacs-lisp
(with-eval-after-load 'mu4e
(setq
;; General
mu4e-maildir (expand-file-name "~/.mail")
mu4e-completing-read-function 'completing-read
mu4e-attachment-dir (expand-file-name "~/Downloads")
mu4e-change-filenames-when-moving t
user-mail-address "jeremy.dormitzer@gmail.com"
mu4e-view-show-images t
mu4e-headers-skip-duplicates t
mail-user-agent 'mu4e-user-agent
;; Custom actions
mu4e-view-actions '(("capture message" . mu4e-action-capture-message)
("view as pdf" . mu4e-action-view-as-pdf)
("show this thread" . mu4e-action-show-thread)
("View in browser" . mu4e-action-view-in-browser))
;; Bookmarked searches
mu4e-bookmarks '((:name "Inbox"
:query (concat "maildir:/jeremy-dormitzer-gmail-com/Inbox"
" OR maildir:/jeremydormitzer-lola-com/Inbox")
:key ?i)
(:name "Unread messages"
:query "flag:unread AND NOT flag:trashed"
:key ?u)
(:name "Today's messages"
:query "date:today..now"
:key ?t)
(:name "Last 7 days"
:query "date:7d..now"
:key ?p))
;; Getting mail
mu4e-get-mail-command "mbsync -a"
;; Sending mail
send-mail-function #'message-send-mail-with-sendmail
message-send-mail-function #'message-send-mail-with-sendmail
sendmail-program (executable-find "msmtp")
;; Let Gmail handle putting sent messages in the sent folder
mu4e-sent-messages-behavior 'delete
;; Move to trash folder instead of adding trash flag for Gmail mailboxes
mu4e-move-to-trash-patterns '("jeremy-dormitzer-gmail-com" "jeremydormitzer-lola-com")
;; HTML email rendering
shr-use-colors nil
;; Make sure mu4e knows about my different accounts
mu4e-context-policy 'ask
mu4e-compose-context-policy 'ask
mu4e-contexts
`(,(make-mu4e-context
:name "Lola Gmail"
:match-func (lambda (msg)
(when msg
(or
(mu4e-message-contact-field-matches msg
:to
"lola.com")
(string-match-p
"jeremydormitzer-lola-com"
(mu4e-message-field msg :path)))))
:vars '((user-mail-address . "jeremydormitzer@lola.com")
(mu4e-compose-signature . "Best regards, \nJeremy Dormitzer \nLola.com")
(message-signature-insert-empty-line . t)
(mu4e-sent-folder . "/jeremydormitzer-lola-com/Sent")
(mu4e-drafts-folder . "/jeremydormitzer-lola-com/Drafts")
(mu4e-refile-folder . "/jeremydormitzer-lola-com/Archive")
(mu4e-trash-folder . "/jeremydormitzer-lola-com/Trash")
(message-sendmail-extra-arguments
. ("-a" "jeremydormitzer-lola-com"))))
,(make-mu4e-context
:name "Personal Gmail"
:match-func (lambda (msg)
(when msg
(string-match-p
"jeremy-dormitzer-gmail-com"
(mu4e-message-field msg :path))))
:vars '((user-mail-address . "jeremy.dormitzer@gmail.com")
(mu4e-compose-signature . nil)
(mu4e-sent-folder . "/jeremy-dormitzer-gmail-com/Sent")
(mu4e-drafts-folder . "/jeremy-dormitzer-gmail-com/Drafts")
(mu4e-refile-folder . "/jeremy-dormitzer-gmail-com/Archive")
(mu4e-trash-folder . "/jeremy-dormitzer-gmail-com/Trash")
(message-sendmail-extra-arguments
. ("-a" "jeremy-dormitzer-gmail-com"))))))
;; Custom mark function to mark messages matching the current message
(defun mu4e-mark-matching-pred (msg from)
(mu4e-message-contact-field-matches msg :from from))
(defun mu4e-mark-matching-input ()
(let* ((msg (mu4e-message-at-point t)))
(if (not msg)
(error "No message at point")
(cdr (mu4e-message-field msg :from)))))
(setq mu4e-headers-custom-markers
'(("Older than"
(lambda
(msg date)
(time-less-p
(mu4e-msg-field msg :date)
date))
(lambda nil
(mu4e-get-time-date "Match messages before: ")))
("Newer than"
(lambda
(msg date)
(time-less-p date
(mu4e-msg-field msg :date)))
(lambda nil
(mu4e-get-time-date "Match messages after: ")))
("Bigger than"
(lambda
(msg bytes)
(>
(mu4e-msg-field msg :size)
(* 1024 bytes)))
(lambda nil
(read-number "Match messages bigger than (Kbytes): ")))
("Matching current message from: field"
(lambda (msg from)
(mu4e-message-contact-field-matches msg :from from))
(lambda ()
(let* ((msg (mu4e-message-at-point t)))
(if (not msg)
(error "No message at point")
(cdar (mu4e-message-field msg :from))))))))
(add-hook 'mu4e-compose-pre-hook
(lambda ()
(set
(make-local-variable '*should-delete-trailing-whitespace*)
nil))))
#+end_src
Support sending attachments from Dired:
#+BEGIN_SRC emacs-lisp
(with-eval-after-load 'dired
(require 'gnus-dired)
;; make the `gnus-dired-mail-buffers' function also work on
;; message-mode derived modes, such as mu4e-compose-mode
(defun gnus-dired-mail-buffers ()
"Return a list of active message buffers."
(let (buffers)
(save-current-buffer
(dolist (buffer (buffer-list t))
(set-buffer buffer)
(when (and (derived-mode-p 'message-mode)
(null message-sent-message-via))
(push (buffer-name buffer) buffers))))
(nreverse buffers)))
(setq gnus-dired-mail-mode 'mu4e-user-agent)
(add-hook 'dired-mode-hook 'turn-on-gnus-dired-mode))
#+END_SRC
Support sending rich-text emails via Markdown:
#+BEGIN_SRC emacs-lisp
(defvar *message-md-pandoc-html-template*
"<html>
<head>
<meta charset=\"utf-8\" />
</head>
<body>
$body$
</body>
</html>"
"The default template used when converting markdown to HTML via pandoc.")
(defun gfm->html (gfm &optional template)
"Converts GitHub-flavored markdown to HTML via pandoc.
By default, the template `*message-md-pandoc-html-template*' is used,
but this can be overridden with the TEMPLATE argument."
(unless (executable-find "pandoc")
(error "Pandoc not found, unable to convert"))
(let ((template-file (make-temp-file "gfm->html-template"
nil ".html"
(or template *message-md-pandoc-html-template*))))
(with-temp-buffer
(insert gfm)
(unless
(= 0
(call-process-region (point-min) (point-max) "pandoc" t t nil
"--template" template-file "--quiet"
"-f" "gfm" "-t" "html"))
(error "Markdown to HTML conversion failed: %s"
(buffer-substring (point-min) (point-max))))
(buffer-substring (point-min) (point-max)))))
(defun mml-node->str (node)
"Converts a parsed MML node back to an MML string."
(let ((node-name (car node))
(node-alist (cdr node)))
(format "<#%s%s>\n%s"
node-name
(cl-reduce (lambda (acc pair)
(if (equal (car pair) 'contents)
acc
(concat acc (format " %s=%S"
(car pair)
(cdr pair)))))
node-alist
:initial-value "")
(cdr (assoc 'contents node-alist))
node-name)))
(defun multipart-message-as-string (plain html inline attachments)
"Given MML nodes PLAIN, and HTML and MML node lists INLINE and
ATTACHMENTS, constructs a multipart MML email and returns it as a
string."
(let* ((alternative (concat "<#multipart type=alternative>\n"
(mml-node->str plain) "\n"
(mml-node->str html) "\n"
"<#/multipart>"))
(related (when inline
(concat "<#multipart type=related>\n"
alternative "\n"
(cl-reduce
(lambda (acc node)
(concat acc (mml-node->str node) "\n"))
inline
:initial-value "")
"<#/multipart>")))
(mixed (when attachments
(concat "<#multipart type=mixed>\n"
(or related alternative) "\n"
(cl-reduce
(lambda (acc node)
(concat acc (mml-node->str node) "\n"))
attachments
:initial-value "")
"<#/multipart>"))))
(or mixed related alternative)))
(defun assoc-mml-node (key node)
(cdr (assoc key (cdr node))))
(defun multipart-html-message (raw)
"Creates a multipart HTML email with a text part and an html part."
(with-temp-buffer
(insert raw)
(let* ((parsed (mml-parse))
(plain (cl-reduce
(lambda (acc node)
(if (not (equal (assoc-mml-node 'type node) "text/plain"))
acc
`(part (type . "text/plain")
(contents . ,(concat (cdaddr acc)
(cdaddr node))))))
parsed
:initial-value '(part (type . "text/plain")
(contents . ""))))
(html `(part (type . "text/html")
(contents . ,(gfm->html (cdaddr plain)))))
(inline (nreverse
(cl-reduce
(lambda (acc node)
(if (not (equal (assoc-mml-node 'disposition node)
"inline"))
acc
(cons node acc)))
parsed
:initial-value nil)))
(attachments (nreverse
(cl-reduce
(lambda (acc node)
(if (not (equal (assoc-mml-node 'disposition node)
"attachment"))
acc
(cons node acc)))
parsed
:initial-value nil))))
(multipart-message-as-string plain
html
inline
attachments))))
(defun convert-message-to-markdown ()
"Convert the message in the current buffer to a multipart HTML email.
The HTML is rendered by treating the message content as Markdown."
(interactive)
(let* ((begin
(save-excursion
(goto-char (point-min))
(search-forward mail-header-separator)))
(end (point-max))
(raw-body (buffer-substring begin end)))
(undo-boundary)
(delete-region begin end)
(save-excursion
(goto-char begin)
(newline)
(insert (multipart-html-message raw-body)))))
(defun message-md-send (&optional arg)
"Convert the current buffer and send it.
If given prefix arg ARG, skips markdown conversion."
(interactive "P")
(unless arg
(convert-message-to-markdown))
(message-send))
(defun message-md-send-and-exit (&optional arg)
"Convert the current buffer and send it, then exit from mail buffer.
If given prefix arg ARG, skips markdown conversion."
(interactive "P")
(unless arg
(convert-message-to-markdown))
(message-send-and-exit))
(with-eval-after-load 'message
(define-key message-mode-map (kbd "C-c C-s") #'message-md-send)
(define-key message-mode-map (kbd "C-c C-c") #'message-md-send-and-exit))
;; Handle replies to HTML emails as well
(defun html->gfm (html)
(unless (executable-find "pandoc")
(error "Pandoc not found, unable to convert"))
(with-temp-buffer
(insert html)
(unless
(= 0
(call-process-region (point-min) (point-max) "pandoc" t t nil
"--quiet"
"-f" "html-native_divs-native_spans"
"-t" "gfm"))
(error "HTML to mardkwon conversion failed: %s"
(buffer-substring (point-min) (point-max))))
(buffer-substring (point-min) (point-max))))
(with-eval-after-load 'mu4e
(defvar message-md-rich-text-reply t)
(defvar message-md--inhibit-rich-text-reply nil)
(defun html-to-md-command (msg)
"Returns the text of MSG as Markdown."
(if (mu4e-msg-field msg :body-html)
(html->gfm (mu4e-msg-field msg :body-html))
(mu4e-msg-field msg :body-txt)))
(defun mu4e-draft-cite-original-advice (oldfn &rest args)
(let ((res (if (and message-md-rich-text-reply
(not message-md--inhibit-rich-text-reply))
(let ((mu4e-view-prefer-html t)
(mu4e-html2text-command #'html-to-md-command))
(apply oldfn args))
(apply oldfn args))))
(setq message-md--inhibit-rich-text-reply nil)
res))
(defun mu4e-compose-reply-advice (oldfn &rest args)
(when current-prefix-arg
(setq message-md--inhibit-rich-text-reply t))
(apply oldfn args))
(advice-add 'mu4e~draft-cite-original :around #'mu4e-draft-cite-original-advice)
(advice-add 'mu4e-compose-reply :around #'mu4e-compose-reply-advice))
;; Add an "X-Attachment-Id" header to MIME stuff as well as a Content-Id:
(with-eval-after-load 'mml
(defun mml-insert-mime-headers (cont type charset encoding flowed)
(let (parameters id disposition description)
(setq parameters
(mml-parameter-string
cont mml-content-type-parameters))
(when (or charset
parameters
flowed
(not (equal type mml-generate-default-type))
mml-insert-mime-headers-always)
(when (consp charset)
(error
"Can't encode a part with several charsets"))
(insert "Content-Type: " type)
(when charset
(mml-insert-parameter
(mail-header-encode-parameter "charset" (symbol-name charset))))
(when flowed
(mml-insert-parameter "format=flowed"))
(when parameters
(mml-insert-parameter-string
cont mml-content-type-parameters))
(insert "\n"))
(when (setq id (cdr (assq 'id cont)))
(insert "Content-ID: " id "\n"))
(when (setq x-attachment-id (cdr (assq 'x-attachment-id cont)))
(insert "X-Attachment-Id: " x-attachment-id "\n"))
(setq parameters
(mml-parameter-string
cont mml-content-disposition-parameters))
(when (or (setq disposition (cdr (assq 'disposition cont)))
parameters)
(insert "Content-Disposition: "
(or disposition
(mml-content-disposition type (cdr (assq 'filename cont)))))
(when parameters
(mml-insert-parameter-string
cont mml-content-disposition-parameters))
(insert "\n"))
(unless (eq encoding '7bit)
(insert (format "Content-Transfer-Encoding: %s\n" encoding)))
(when (setq description (cdr (assq 'description cont)))
(insert "Content-Description: ")
(setq description (prog1
(point)
(insert description "\n")))
(mail-encode-encoded-word-region description (point))))))
(defun message-md-insert-inline-image (image type description)
(interactive
(let* ((file (mml-minibuffer-read-file "Insert image: "))
(type (if current-prefix-arg
(or (mm-default-file-encoding file)
"application/octet-stream")
(mml-minibuffer-read-type file)))
(description (if current-prefix-arg
nil
(mml-minibuffer-read-description))))
(list file type description)))
(let ((id (format "%s%s" (s-snake-case description) (random))))
(insert (format "![%s](cid:%s)" description id))
(save-excursion
(goto-char (point-max))
(newline)
(insert
(format (concat "<#part id=\"<%s>\" x-attachment-id=%S "
"type=%S filename=%S disposition=inline description=%s>")
id id type image description)))))
;; The default message signature begins with "-- \n". To makes this
;; markdown compatible, we need to add an extra space at the end: "-- \n"
(defun message-md-add-space-to-sig-separator (&rest args)
(save-excursion
(when (search-backward "--" nil t)
(forward-char 2)
(insert " "))))
(advice-add 'message-insert-signature :after
#'message-md-add-space-to-sig-separator)
#+END_SRC
Global keybindings:
#+begin_src emacs-lisp
(leader-def-key "am" #'mu4e)
#+end_src
Keybindings within mu4e:
#+begin_src emacs-lisp
(with-eval-after-load 'mu4e
(general-def '(normal motion insert emacs) mu4e-headers-mode-map "t" #'mu4e-headers-mark-thread)
(general-def '(normal motion insert emacs) mu4e-view-mode-map "t" #'mu4e-view-mark-thread))
#+end_src
Mu4e uses shr to render HTML emails. Unfortunately the shr function
that sets faces in the rendered document has a bug: it appends the shr
faces to the existing face-list, rather than prepending the shr
face. This means that e.g. links don't actually get rendered correctly
if there is some non-link face already on the text. The fix:
#+BEGIN_SRC emacs-lisp
(with-eval-after-load 'shr
(defun shr-add-font (start end type)
(save-excursion
(goto-char start)
(while (< (point) end)
(when (bolp)
(skip-chars-forward " "))
;; Remove the APPEND argument to add-face-text-property
;; so the face ends up at the head of the face list
(add-face-text-property (point) (min (line-end-position) end) type)
(if (< (line-end-position) end)
(forward-line 1)
(goto-char end))))))
#+END_SRC
* w3m
Browsing the web from Emacs. Relies on having [[http://w3m.sourceforge.net/][w3m]] installed.
#+BEGIN_SRC emacs-lisp
(use-package w3m
:commands (w3m
w3m-browse-url
w3m-search-new-session)
:init
(setq w3m-home-page "https://start.duckduckgo.com"
w3m-search-default-engine "duckduckgo"
w3m-cookie-reject-domains '("www.wsj.com"
"www.bbc.com"
"www.nytimes.com"
"www.washingtonpost.com")
w3m-use-tab t
w3m-use-tab-line t
browse-url-browser-function '(("nytimes.com" . w3m-browse-url)
("wsj.com" . w3m-browse-url)
("." . browse-url-default-browser)))
:general
('normal w3m-mode-map "SPC" leader-map)
('(normal visual motion) w3m-mode-map "C-f" #'w3m-scroll-up-or-next-url)
('(normal visual motion) w3m-mode-map "C-b" #'w3m-scroll-down-or-previous-url)
('normal w3m-mode-map "J" #'w3m-previous-buffer)
('normal w3m-mode-map "K" #'w3m-next-buffer)
('normal w3m-mode-map "gs" #'w3m-search)
('normal w3m-mode-map "gS" #'w3m-search-new-session)
('normal w3m-mode-map "gl" #'link-hint-open-link)
('normal w3m-mode-map "gc" #'link-hint-copy-link)
('normal w3m-mode-map "zc" #'w3m-print-current-url))
(jdormit/define-prefix "aw" "w3m")
(leader-def-key "aww" 'w3m)
(leader-def-key "aws" 'w3m-search-new-session)
(leader-def-key "awb" 'w3m-browse-url)
#+END_SRC
I mostly want `browse-url-at-point` to open stuff in Firefox, but in some cases I want it within Emacs:
#+BEGIN_SRC emacs-lisp
(setq browse-url-generic-program
(cond
((executable-find "open") "open")
((executable-find "xdg-open") "xdg-open")))
(defun browse-url-at-point-w3m ()
"Opens the URL at point in w3m"
(interactive)
(let ((browse-url-browser-function 'w3m-browse-url))
(if (eq major-mode 'org-mode)
(org-open-at-point)
(browse-url-at-point))))
(leader-def-key "awB" 'browse-url-at-point-w3m)
#+END_SRC
I want to be able to set a custom "Referer" header by setting the variable `jdormit/w3m-referers`. I also want to be able to set websites that cookies will never get sent to:
#+BEGIN_SRC emacs-lisp
(defvar jdormit/w3m-referers nil)
(defvar jdormit/w3m-no-cookie-sites nil)
(defun get-referer (url referer-list)
"Retrieve the referer specified by url in referer-list"
(when (not (eq nil referer-list))
(let ((first (car referer-list))
(rest (cdr referer-list)))
(if (string-match-p (car first) url)
(cdr first)
(get-referer url rest)))))
(defun should-not-set-cookie-p (url no-cookie-sites)
"Non-nil if cookies should not be sent to url"
(when (not (eq nil no-cookie-sites))
(if (string-match-p (car no-cookie-sites) url)
t
(should-not-set-cookie-p url (cdr no-cookie-sites)))))
(advice-add 'w3m-header-arguments :around
(lambda (w3m-header-arguments &rest r)
(cl-destructuring-bind
(method url temp-file body referer content-type) r
(let ((w3m-use-cookies
(if (should-not-set-cookie-p url jdormit/w3m-no-cookie-sites)
nil
w3m-use-cookies)))
(if-let ((referer (get-referer url jdormit/w3m-referers)))
(funcall w3m-header-arguments
method
url
temp-file
body
referer
content-type)
(apply w3m-header-arguments r))))))
#+END_SRC
And here are the websites where I want custom referers and/or no cookies:
#+BEGIN_SRC emacs-lisp
(setq jdormit/w3m-referers '(("www.wsj.com" . "https://google.com")
("www.nytimes.com" . "https://google.com")
("www.newyorker.com" . "https://google.com")
("www.economist.com" . "https://google.com")))
(setq jdormit/w3m-no-cookie-sites '("www.wsj.com"
"www.nytimes.com"
"www.economist.com"
"www.newyorker.com"))
#+END_SRC
Render =<code>= and =<pre>= blocks in a different face. Which face can be set by setting or customizing the variable =w3m-code-block-face=. It defaults to ='fixed-pitch=:
#+BEGIN_SRC emacs-lisp
(setq w3m-code-open-delimiter "{{{W3M_CODE_BLOCK_BEGIN}}}")
(setq w3m-code-close-delimiter "{{{W3M_CODE_BLOCK_END}}}")
(defun w3m-filter-code-blocks (url)
(w3m-tag-code-block-filter "code")
(w3m-tag-code-block-filter "pre"))
(defun w3m-tag-code-block-filter (tag)
(goto-char (point-min))
(let ((open-tag-re (format "<%s[ \t\r\f\n]*[^>]*>" tag))
(close-tag-re (format "</%s[ \t\r\f\n]*>" tag)))
(while (re-search-forward open-tag-re nil t)
(let ((start (match-beginning 0)))
(when (re-search-forward close-tag-re nil t)
(goto-char start)
(insert w3m-code-open-delimiter)
(goto-char (+ (string-width w3m-code-open-delimiter) (match-end 0)))
(insert w3m-code-close-delimiter))))))
(defcustom w3m-code-block-face 'fixed-pitch
"Face for <code> and <pre> blocks in w3m")
(defun w3m-fontify-code-block ()
(goto-char (point-min))
(while (search-forward w3m-code-open-delimiter nil t)
(let ((begin-block-start (match-beginning 0))
(begin-block-end (match-end 0))
(code-start (match-end 0)))
(when (search-forward w3m-code-close-delimiter nil t)
(w3m-add-face-property code-start (match-beginning 0) w3m-code-block-face)
(delete-region (match-beginning 0) (match-end 0)))
(delete-region begin-block-start begin-block-end)
(goto-char (point-min)))))
;(add-hook 'w3m-fontify-before-hook 'w3m-fontify-code-block)
#+END_SRC
Add a w3m filter to handle the code block delimiters:
#+BEGIN_SRC emacs-lisp
;; (with-eval-after-load 'w3m-filter
;; (add-to-list 'w3m-filter-configuration
;; '(t "Render code blocks with a different face" ".*" w3m-filter-code-blocks)))
#+END_SRC
* Wakatime
[[https://wakatime.com/emacs][Wakatime]] is a tool that tracks how much time you spend coding and various metrics about your development activity.
It needs a helper script to work properly.
#+BEGIN_SRC emacs-lisp
(use-package wakatime-mode
:if (executable-find "wakatime")
:init
(setq wakatime-api-key (password-store-get "wakatime-api-key")
wakatime-cli-path (executable-find "wakatime"))
:config
(global-wakatime-mode)
;; global-wakatime-mode breaks recovering autosaves for some reason
(advice-add 'recover-this-file :around
(lambda (oldfn &rest args)
(let ((wakatime-was-enabled global-wakatime-mode))
(when wakatime-was-enabled
(global-wakatime-mode -1))
(apply oldfn args)
(when wakatime-was-enabled
(global-wakatime-mode))))))
#+END_SRC
* Elfeed
Elfeed is a feed reader for Emacs.
#+BEGIN_SRC emacs-lisp
(defun elfeed-setup-hook ()
(set (make-local-variable 'browse-url-browser-function)
'w3m-browse-url)
(set (make-local-variable 'jdormit/w3m-referer) "https://www.google.com")
(smartparens-mode -1)
(evil-smartparens-mode -1))
(use-package elfeed
:commands elfeed
:config
(add-hook 'elfeed-search-mode-hook 'elfeed-setup-hook)
(setq-default elfeed-search-filter "@6-months-ago +unread -hacker-news")
(add-to-list 'evil-normal-state-modes 'elfeed-search-mode)
(add-to-list 'evil-normal-state-modes 'elfeed-show-mode)
(defun elfeed-browse-url-external ()
(interactive)
(shr-browse-url t))
(setq elfeed-feeds
(list (list (password-store-get "freshrss-blog-feed-url") 'blog)
(list (password-store-get "freshrss-lisp-feed-url") 'lisp)
(list (password-store-get "freshrss-computing-feed-url") 'computing)
(list (password-store-get "freshrss-emacs-feed-url") 'emacs)
(list (password-store-get "freshrss-misc-feed-url") 'misc)
(list (password-store-get "freshrss-news-feed-url") 'news)
(list (password-store-get "freshrss-web-feed-url") 'web)
(list (password-store-get "freshrss-creative-coding-feed-url")
'creative-coding 'computing)
(list (password-store-get "freshrss-hacker-news-feed-url") 'hacker-news))
shr-use-colors nil)
(add-hook 'elfeed-new-entry-hook
(elfeed-make-tagger
:entry-link (rx "metaredux.com")
:add 'blog))
(add-hook 'elfeed-new-entry-hook
(elfeed-make-tagger
:entry-link (rx "lambdaisland.com")
:add 'blog))
(add-hook 'elfeed-new-entry-hook
(elfeed-make-tagger
:entry-link (rx "emacsredux.com")
:add 'blog))
(add-hook 'elfeed-new-entry-hook
(elfeed-make-tagger
:entry-link (rx "sachachua.com")
:add 'blog))
(add-hook 'elfeed-new-entry-hook
(elfeed-make-tagger
:entry-link (rx "mxb.dev")
:add 'blog))
(add-hook 'elfeed-new-entry-hook
(elfeed-make-tagger
:entry-link (rx "joy.recurse.com")
:add 'joy-of-computing))
(add-hook 'elfeed-new-entry-hook
(elfeed-make-tagger
;; Changelog Weekly
:entry-link (rx "kill-the-newsletter.com/alternate/cfiasax3ct12r7b9svjq")
:add 'open-source))
(add-hook 'elfeed-new-entry-hook
(elfeed-make-tagger
:entry-link (rx "jeremydormitzer.com")
:add 'my-website))
(add-hook 'elfeed-new-entry-hook
(elfeed-make-tagger
:entry-link (rx "fossegr.im")
:add 'blog))
(add-hook 'elfeed-new-entry-hook
(elfeed-make-tagger
:entry-link (rx "joshwcomeau.com")
:add 'blog))
(add-hook 'elfeed-new-entry-hook
(elfeed-make-tagger
:entry-link (rx "murilopereira.com")
:add 'blog))
(add-hook 'elfeed-new-entry-hook
(elfeed-make-tagger
:entry-link (rx "tedium.co")
:add 'tedium))
(add-hook 'elfeed-new-entry-hook
(elfeed-make-tagger
:entry-link (rx "reddit.com")
:add 'reddit))
(add-hook 'elfeed-new-entry-hook
(elfeed-make-tagger
:entry-title (rx (or "Emacs" "emacs"))
:add 'emacs))
(add-hook 'elfeed-new-entry-hook
(elfeed-make-tagger
:entry-title (rx (or "Clojure"
"Scheme"
"Lisp"
"Racket"))
:add 'lisp))
(general-def 'normal elfeed-show-mode-map "u" (elfeed-expose #'elfeed-show-tag 'unread))
:general
('normal elfeed-search-mode-map "q" 'elfeed-search-quit-window)
('normal elfeed-search-mode-map "C-r" 'elfeed-search-update--force)
('normal elfeed-search-mode-map "R" 'elfeed-search-fetch)
('normal elfeed-search-mode-map "RET" 'elfeed-search-show-entry)
('normal elfeed-search-mode-map "s" 'elfeed-search-live-filter)
('normal elfeed-search-mode-map "S" 'elfeed-search-set-filter)
('normal elfeed-search-mode-map "B" 'elfeed-search-browse-url)
('normal elfeed-search-mode-map "y" 'elfeed-search-yank)
('normal elfeed-search-mode-map "u" 'elfeed-search-tag-all-unread)
('normal elfeed-search-mode-map "r" 'elfeed-search-untag-all-unread)
('normal elfeed-search-mode-map "n" 'next-line)
('normal elfeed-search-mode-map "p" 'previous-line)
('normal elfeed-search-mode-map "+" 'elfeed-search-tag-all)
('normal elfeed-search-mode-map "-" 'elfeed-search-untag-all)
('normal elfeed-show-mode-map "d" 'elfeed-show-save-enclosure)
('normal elfeed-show-mode-map "q" 'elfeed-kill-buffer)
('normal elfeed-show-mode-map "r" 'elfeed-show-refresh)
('normal elfeed-show-mode-map "n" 'elfeed-show-next)
('normal elfeed-show-mode-map "p" 'elfeed-show-prev)
('normal elfeed-show-mode-map "s" 'elfeed-show-new-live-search)
('normal elfeed-show-mode-map "B" 'elfeed-show-visit)
('normal elfeed-show-mode-map "y" 'elfeed-show-yank)
('normal elfeed-show-mode-map "+" 'elfeed-show-tag)
('normal elfeed-show-mode-map "-" 'elfeed-show-untag)
('normal elfeed-show-mode-map "\t" 'shr-next-link)
('normal elfeed-show-mode-map [tab] 'shr-next-link)
('normal elfeed-show-mode-map "\e\t" 'shr-previous-link)
('normal elfeed-show-mode-map [backtab] 'shr-previous-link)
('normal elfeed-show-mode-map [mouse-2] 'shr-browse-url)
('normal elfeed-show-mode-map "A" 'elfeed-show-add-enclosure-to-playlist)
('normal elfeed-show-mode-map "P" 'elfeed-show-play-enclosure)
('normal elfeed-show-mode-map "<M-return>" 'elfeed-browse-url-external))
#+END_SRC
Keybinding for opening Elfeed:
#+BEGIN_SRC emacs-lisp
(leader-def-key "al" 'elfeed)
#+END_SRC
* Undo Tree
#+BEGIN_SRC emacs-lisp
(use-package undo-tree
:init
(global-undo-tree-mode)
(leader-def-key "bu" 'undo-tree-visualize)
:general
('normal "u" #'undo-tree-undo)
('normal "C-r" #'undo-tree-redo)
(undo-tree-visualizer-mode-map "SPC" leader-map))
#+END_SRC
* Emojify
Because emojis make everything better.
#+BEGIN_SRC emacs-lisp
(use-package emojify
:commands (emojify-mode
emojify-apropos-emoji
emojify-insert-emoji)
:hook
((emacs-startup . emojify-mode))
:init
(leader-def-key "te" 'emojify-mode))
#+END_SRC
* Calc
#+BEGIN_SRC emacs-lisp
(leader-def-key "ac" 'calc)
#+END_SRC
* Deadgrep
A nice Emacs UI over [[https://github.com/BurntSushi/ripgrep#installation][ripgrep]].
#+BEGIN_SRC emacs-lisp
(use-package deadgrep
:commands deadgrep
:general
(deadgrep-mode-map "SPC" leader-map))
(leader-def-key "fg" 'deadgrep)
#+END_SRC
* RCIRC
IRC in Emacs, just in case anyone actually still uses it...
Channels:
#+BEGIN_SRC emacs-lisp
(with-eval-after-load 'rcirc
(setq rcirc-server-alist
`(("znc.jeremydormitzer.com"
:port 3000
:nick "jdormit"
:user-name "jdormit"
:password ,(password-store-get "znc.jeremydormitzer.com")))))
#+END_SRC
Key bindings:
#+BEGIN_SRC emacs-lisp
(leader-def-key "ai" 'irc)
#+END_SRC
Use evil keybindings by default:
#+BEGIN_SRC emacs-lisp
(add-to-list 'evil-normal-state-modes 'rcirc-mode)
#+END_SRC
* dumb-jump
[[https://github.com/jacktasia/dumb-jump][Dumb-jump]] uses ripgrep and some algorithms based on the major-mode to implement jump-to-definition.
#+BEGIN_SRC emacs-lisp
(use-package dumb-jump
:commands (dumb-jump-go dumb-jump-prompt)
:config (dumb-jump-mode))
(leader-def-key "cj" 'dumb-jump-go)
(leader-def-key "cp" 'dumb-jump-go-prompt)
#+END_SRC
* Dictionary
This package looks up word definitions online.
#+BEGIN_SRC emacs-lisp
(use-package define-word
:commands (define-word define-word-at-point)
:general
(normal "gl" #'define-word-at-point)
(normal "gL" #'define-word))
#+END_SRC
* Gnus
An ancient newsreader.
#+BEGIN_SRC emacs-lisp
(leader-def-key "ag" 'gnus)
(general-def gnus-mode-map "SPC" leader-map)
#+END_SRC
It can read email:
#+BEGIN_SRC emacs-lisp
(setq gnus-select-method '(nnnil "")
gnus-secondary-select-methods
'((nnmaildir "jeremy@dormitzer.net"
(directory "~/.mail/jeremy-dormitzer-net"))
(nnmaildir "jeremy@getpterotype.com"
(directory "~/.mail/jeremy-getpterotype-com"))
(nnmaildir "jeremy.dormitzer@gmail.com"
(directory "~/.mail/jeremy-dormitzer-gmail-com"))
(nnmaildir "jdormitzer@hubspot.com"
(directory "~/.mail/jdormitzer-hubspot-com")))
mm-text-html-renderer 'gnus-w3m)
#+END_SRC
Or Gnus can read RSS feeds directly:
#+BEGIN_SRC emacs-lisp
;; (add-to-list 'gnus-secondary-select-methods
;; '(nnrss "http://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml"))
#+END_SRC
* Dired
** Dired-X
#+BEGIN_SRC emacs-lisp
(defun dired-x-setup ()
(require 'dired-x)
(dired-omit-mode 1))
(add-hook 'dired-mode-hook #'dired-x-setup)
#+END_SRC
** Utility functions
#+BEGIN_SRC emacs-lisp
(defun dired-counsel-find-file (&optional initial-input)
(interactive)
(let ((default-directory (dired-current-directory)))
(counsel-find-file initial-input)))
(defun dired-create-empty-file-in-current-dir (file)
(interactive (list (let ((default-directory (dired-current-directory)))
(read-file-name "Create empty file: "))))
(dired-create-empty-file file))
(defun dired-do-rename-in-current-dir (&optional arg)
(interactive "P")
(let ((default-directory (dired-current-directory)))
(dired-do-rename arg)))
#+END_SRC
** Keybindings
#+BEGIN_SRC emacs-lisp
(general-def 'normal dired-mode-map
"f" #'dired-create-empty-file-in-current-dir
[remap find-file] 'dired-counsel-find-file
[remap counsel-find-file] 'dired-counsel-find-file
[remap dired-do-rename] 'dired-do-rename-in-current-dir)
#+END_SRC
** Dired subtree
#+BEGIN_SRC emacs-lisp
(use-package dired-subtree
:init
(defun dired-counsel-find-file (&optional initial-input)
(interactive)
(let ((default-directory (dired-current-directory)))
(counsel-find-file initial-input)))
:general
(normal dired-mode-map "TAB" 'dired-subtree-toggle)
:config
(advice-add 'dired-subtree-toggle :after (lambda () (revert-buffer))))
#+END_SRC
** Dired narrow
#+BEGIN_SRC emacs-lisp
(use-package dired-narrow
:general
(normal dired-mode-map "/" #'dired-narrow))
#+END_SRC
** Dired sidebar
#+BEGIN_SRC emacs-lisp
(use-package dired-sidebar
:commands (dired-sidebar-toggle-sidebar)
:init
(leader-def-key "d" #'dired-sidebar-toggle-sidebar)
(add-hook 'dired-sidebar-mode-hook
(lambda ()
(unless (file-remote-p default-directory)
(auto-revert-mode))))
:config
(with-eval-after-load 'all-the-icons-dired
;; Display chevrons next to directories
(defun all-the-icons-icon-for-dir-with-chevron (dir &optional chevron padding initial-padding &rest arg-overrides)
"Format an icon for DIR with CHEVRON similar to tree based directories.
If PADDING is provided, it will prepend and separate the chevron
and directory with PADDING.
Produces different symbols by inspecting DIR to distinguish
symlinks and git repositories which do not depend on the
directory contents"
(let ((icon (apply 'all-the-icons-icon-for-dir dir arg-overrides))
(chevron (if chevron (all-the-icons-octicon (format "chevron-%s" chevron) :height 0.8 :v-adjust -0.1) ""))
(padding (or padding "\t")))
(format "%s%s%s%s%s" initial-padding chevron padding icon padding)))
(defun all-the-icons-dired-sidebar--refresh ()
"Display the icons of files in a dired buffer."
(all-the-icons-dired--remove-all-overlays)
(save-excursion
(goto-char (point-min))
(while (not (eobp))
(when (dired-move-to-filename nil)
(let ((file (dired-get-filename 'relative 'noerror)))
(when file
(let ((icon (if (file-directory-p file)
(all-the-icons-icon-for-dir-with-chevron file
(if (dired-subtree--is-expanded-p)
"down"
"right")
"\t"
""
:face 'all-the-icons-dired-dir-face
:v-adjust all-the-icons-dired-v-adjust)
(all-the-icons-icon-for-file file :v-adjust all-the-icons-dired-v-adjust))))
(if (member file '("." ".."))
(all-the-icons-dired--add-overlay (point) " \t")
(all-the-icons-dired--add-overlay
(point)
(concat (when (not (file-directory-p file))
" \t")
icon
"\t")))))))
(forward-line 1))))
(advice-add 'all-the-icons-dired--refresh :around
(lambda (oldfn &rest args)
(if (eq major-mode 'dired-sidebar-mode)
(apply #'all-the-icons-dired-sidebar--refresh args)
(apply oldfn args)))))
(add-to-list 'dired-sidebar-special-refresh-commands 'dired-kill-subdir)
:custom
(dired-sidebar-should-follow-file t)
(dired-sidebar-pop-to-sidebar-on-toggle-open nil)
(dired-sidebar-no-delete-other-windows t))
#+END_SRC
** All-the-icons
#+BEGIN_SRC emacs-lisp
(use-package all-the-icons)
(use-package all-the-icons-dired
:defer t
:hook ((dired-mode . (lambda ()
(unless (eq major-mode 'dired-sidebar-mode)
(all-the-icons-dired-mode))))))
#+END_SRC
** Variables
#+BEGIN_SRC emacs-lisp
(setq dired-dwim-target t)
#+END_SRC
** Dired hydra
#+BEGIN_SRC emacs-lisp
(use-package dired
:straight (:type built-in)
:init
(defhydra hydra-dired (:hint nil :color pink)
"
_+_ mkdir _v_iew _m_ark _(_ details _i_nsert-subdir wdired
_C_opy _O_ view other _U_nmark all _)_ omit-mode _$_ hide-subdir C-x C-q : edit
_D_elete _o_pen other _u_nmark _l_ redisplay _w_ kill-subdir C-c C-c : commit
_R_ename _M_ chmod _t_oggle _g_ revert buf _e_ ediff C-c ESC : abort
_Y_ rel symlink _G_ chgrp _E_xtension mark _s_ort _=_ pdiff
_S_ymlink ^ ^ _F_ind marked _._ toggle hydra \\ flyspell
_r_sync ^ ^ ^ ^ ^ ^ _?_ summary
_z_ compress-file _A_ find regexp
_Z_ compress _Q_ repl regexp
T - tag prefix
"
("\\" dired-do-ispell)
("(" dired-hide-details-mode)
(")" dired-omit-mode)
("+" dired-create-directory)
("=" diredp-ediff) ;; smart diff
("?" dired-summary)
("$" diredp-hide-subdir-nomove)
("A" dired-do-find-regexp)
("C" dired-do-copy) ;; Copy all marked files
("D" dired-do-delete)
("E" dired-mark-extension)
("e" dired-ediff-files)
("F" dired-do-find-marked-files)
("G" dired-do-chgrp)
("g" revert-buffer) ;; read all directories again (refresh)
("i" dired-maybe-insert-subdir)
("l" dired-do-redisplay) ;; relist the marked or single directory
("M" dired-do-chmod)
("m" dired-mark)
("O" dired-display-file)
("o" dired-find-file-other-window)
("Q" dired-do-find-regexp-and-replace)
("R" dired-do-rename)
("r" dired-do-rsynch)
("S" dired-do-symlink)
("s" dired-sort-toggle-or-edit)
("t" dired-toggle-marks)
("U" dired-unmark-all-marks)
("u" dired-unmark)
("v" dired-view-file) ;; q to exit, s to search, = gets line #
("w" dired-kill-subdir)
("Y" dired-do-relsymlink)
("z" diredp-compress-this-file)
("Z" dired-do-compress)
("q" nil)
("." nil :color blue))
:general
((normal visual motion insert emacs) dired-mode-map "." 'hydra-dired/body)
(dired-mode-map "SPC" leader-map))
#+END_SRC
* Crontab
Magit ships with a cool utility called =with-editor= that lets you run a shell command using the current Emacs instance as $EDITOR. This means we can define a command to edit the crontab with the current Emacs instance:
#+BEGIN_SRC emacs-lisp
(defun edit-crontab ()
(interactive)
(with-editor-async-shell-command "crontab -e"))
#+END_SRC
* Emacs Server
In case I need an =emacsclient= for some reason.
#+BEGIN_SRC emacs-lisp
(add-hook 'after-init-hook #'server-start)
#+END_SRC
* YASnippet
YASnippet is Yet Another Snippet template system.
#+BEGIN_SRC emacs-lisp
(use-package yasnippet
:hook ((text-mode . yas-minor-mode)
(prog-mode . yas-minor-mode)
(after-init . yas-reload-all))
:config
(unless (file-exists-p (expand-file-name "~/.emacs.d/snippets"))
(mkdir (expand-file-name "~/.emacs.d/snippets") t))
(setq yas-snippet-dirs
`(,(syncthing-directory "yasnippet")
,(expand-file-name "~/.emacs.d/snippets"))))
(use-package yasnippet-snippets
:after (yasnippet)
:config (yasnippet-snippets-initialize))
#+END_SRC
* mpc
An Emacs interface to MPD, the Music Player Daemon
#+BEGIN_SRC emacs-lisp
(leader-def-key "ad" #'mpc)
(with-eval-after-load 'evil
(add-to-list 'evil-emacs-state-modes 'mpc-mode))
(general-def mpc-mode-map "SPC" leader-map)
(general-def mpc-mode-map "a" #'mpc-playlist-add)
#+END_SRC
* wgrep
#+BEGIN_SRC emacs-lisp
(use-package wgrep
:defer t)
#+END_SRC
* Ivy
An alternative minibuffer completion framework:
#+BEGIN_SRC emacs-lisp
(use-package counsel
:defer 0
:config
(ivy-mode 1)
(counsel-mode 1)
(setq ivy-height 20
ivy-wrap t
ivy-use-virtual-buffers nil
ivy-count-format "%d/%d ")
(with-eval-after-load 'projectile
(setq projectile-completion-system 'ivy))
(leader-def-key "SPC" #'counsel-M-x)
(jdormit/define-prefix "i" "ivy")
(jdormit/define-prefix "iU" "ui")
(leader-def-key "ir" #'ivy-resume)
(leader-def-key "ip" #'swiper-thing-at-point)
(leader-def-key "iP" #'counsel-yank-pop)
(leader-def-key "iu" #'counsel-unicode-char)
(leader-def-key "iUt" #'counsel-load-theme)
(leader-def-key "is" #'swiper)
(leader-def-key "ia" #'swiper-all)
(leader-def-key "ff" #'counsel-find-file)
(leader-def-key "oc" #'counsel-org-capture)
(leader-def-key "bb" #'ivy-switch-buffer)
(if (executable-find "rg")
(leader-def-key "ig" #'counsel-rg)
(leader-def-key "ig" #'counsel-grep))
(defvar counsel-set-frame-font-history nil)
(defun counsel-set-frame-font (font)
(interactive (list (ivy-read "Font: " (delete-dups (font-family-list))
:require-match t
:history 'counsel-set-frame-font-history
:caller 'counsel-set-frame-font)))
(set-frame-font font))
(leader-def-key "iUf" #'counsel-set-frame-font)
(defun counsel-ibuffer-kill-buffer (x)
(kill-buffer (cdr x)))
(ivy-set-actions
'counsel-ibuffer
'(("k" counsel-ibuffer-kill-buffer "kill buffer")))
;; Function to open files without Ivy to avoid lag in really huge directories
(defun find-file-default (filename &optional wildcards)
(interactive
(let ((completing-read-function 'completing-read-default))
(find-file-read-args "Find file: "
(confirm-nonexistent-file-or-buffer))))
(funcall-interactively 'find-file filename wildcards))
:general
("C-c C-r" #'ivy-resume)
("M-x" #'counsel-M-x)
("C-x C-f" #'counsel-find-file)
("C-M-u" #'counsel-unicode-char)
("C-c P" #'counsel-yank-pop)
("C-s" #'swiper-isearch)
(help-map "f" #'counsel-describe-function)
(help-map "v" #'counsel-describe-variable)
((normal motion visual) "g/" #'swiper-thing-at-point))
(use-package ivy-hydra
:after counsel)
(use-package counsel-projectile
:after (counsel projectile)
:commands (counsel-projectile
counsel-projectile-switch-project
counsel-projectile-find-file
counsel-projectile-grep)
:init
(counsel-projectile-mode)
(leader-def-key "pp" #'counsel-projectile-switch-project)
(leader-def-key "pf" #'counsel-projectile)
(if (executable-find "rg")
(leader-def-key "pg" #'counsel-projectile-rg)
(leader-def-key "pg" #'counsel-projectile-grep)))
(use-package lsp-ivy
:after (ivy lsp)
:commands (lsp-ivy-workspace-symbol
lsp-ivy-global-workspace-symbol))
(use-package ivy-rich
:hook (ivy-mode . ivy-rich-mode))
(use-package all-the-icons-ivy-rich
:after (ivy-rich)
:demand t
:config
(all-the-icons-ivy-rich-mode 1))
(leader-def-key "cs" #'lsp-ivy-workspace-symbol)
#+END_SRC
#+BEGIN_SRC emacs-lisp
(use-package ivy-xref
:ensure t
:straight (ivy-xref :fork (:host github :repo "jdormit/ivy-xref"))
:init
;; xref initialization is different in Emacs 27 - there are two different
;; variables which can be set rather than just one
(when (>= emacs-major-version 27)
(setq xref-show-definitions-function #'ivy-xref-show-defs))
;; Necessary in Emacs <27. In Emacs 27 it will affect all xref-based
;; commands other than xref-find-definitions (e.g. project-find-regexp)
;; as well
(setq xref-show-xrefs-function #'ivy-xref-show-xrefs))
#+END_SRC
[[https://github.com/raxod502/prescient.el][Prescient.el]] adds sorting and filtering to completion candidates:
#+begin_src emacs-lisp
(use-package prescient)
(use-package ivy-prescient
:after (prescient ivy)
:config
(ivy-prescient-mode)
(prescient-persist-mode)
:custom
(ivy-prescient-enable-filtering nil)
(ivy-prescient-sort-commands '(:not swiper
swiper-isearch
counsel-imenu
counsel-M-x)))
#+end_src
* graphviz
#+BEGIN_SRC emacs-lisp
(use-package graphviz-dot-mode
:mode (("\\.dot\\'" . graphviz-dot))
:init
(with-eval-after-load 'org
(add-to-list 'org-src-lang-modes '("dot" . graphviz-dot))))
#+END_SRC
** Functions
A function that converts a lisp form into Graphviz format, e.g.:
#+BEGIN_EXAMPLE
'(a ((label . "Node A"))
(b ((label . "Node B"))
(d ((label . "Node D"))))
(c ((label . "Node C"))
(e ((label . "Node E")))))
#+END_EXAMPLE
becomes:
#+BEGIN_EXAMPLE
digraph {
a[label="Node A"];
b[label="Node B"];
c[label="Node C"];
d[label="Node D"];
e[label="Node E"];
a -> {b, c};
b -> d;
c -> e;
}
#+END_EXAMPLE
#+BEGIN_SRC emacs-lisp
(defun graphviz-make-node-string (id attrs)
"Makes a Graphviz Dot string representing a node with attributes"
(if attrs
(format
"%s[%s];"
id
(mapconcat 'identity
(mapcar
(lambda (attr)
(format "%s=\"%s\"" (car attr) (cdr attr)))
attrs)
", "))
(format "%s;" id)))
(defun graphviz-make-edge-string (id children)
"Makes a Graphviz Dot string representing the edges between id and children"
(when children
(format "%s -> {%s}"
id
(mapconcat
'identity
(mapcar
(lambda (child)
(format "%s" (car child)))
children)
"; "))))
(defun graphviz-parse-graph (graph)
"Parses a graph into nodes and edges represented in the dot language.
Returns an alist ((nodes (<node strings>)) (edges (<edge strings>)))"
(when graph
(let* ((id (car graph))
(attrs (cadr graph))
(children (cddr graph))
(child-graphs (mapcar #'graphviz-parse-graph children)))
`((nodes ,(cons (graphviz-make-node-string id attrs)
(apply #'append
(mapcar (lambda (child)
(cadr (assoc 'nodes child)))
child-graphs))))
(edges ,(let ((edge-string (graphviz-make-edge-string id children))
(child-edges (apply #'append
(mapcar (lambda (child)
(cadr (assoc 'edges child)))
child-graphs))))
(if edge-string
(cons edge-string child-edges)
child-edges)))))))
(defun graphviz-compile-graph (graph)
"Transpiles a graph defined as a lisp form to the Graphviz Dot language"
(let* ((graph (graphviz-parse-graph graph))
(nodes (cadr (assoc 'nodes graph)))
(edges (cadr (assoc 'edges graph))))
(message "%s" graph)
(format "digraph {\n %s\n %s\n}"
(mapconcat 'identity nodes "\n ")
(mapconcat 'identity edges "\n "))))
#+END_SRC
* HideShow
[[help:hs-minor-mode][hs-minor-mode]] enables comment and code-folding. It's useful almost everywhere, so just enable it:
#+BEGIN_SRC emacs-lisp
(add-hook 'prog-mode-hook (lambda () (hs-minor-mode 1)))
#+END_SRC
* Slack
#+BEGIN_SRC emacs-lisp
(use-package slack
:commands (slack-start)
:init
(setq slack-buffer-emojify t)
(setq slack-prefer-current-team t)
:config
(slack-register-team
:name "lolatravel"
:default t
:token "<redacted>"
:subscribed-channels '(1-1-2020
backend
booking-team-backend
critical-bugs
dev
devops
frontend
search-and-book
smash
southwest
work
not-work)
:full-and-display-names t)
:general
('normal slack-info-mode-map ",u" #'slack-room-update-messages)
('normal slack-mode-map
",c" 'slack-buffer-kill
",ra" 'slack-message-add-reaction
",rr" 'slack-message-remove-reaction
",rs" 'slack-message-show-reaction-users
",pl" 'slack-room-pins-list
",pa" 'slack-message-pins-add
",pr" 'slack-message-pins-remove
",mm" 'slack-message-write-another-buffer
",me" 'slack-message-edit
",md" 'slack-message-delete
",u" 'slack-room-update-messages
",2" 'slack-message-embed-mention
",3" 'slack-message-embed-channel
"\C-n" 'slack-buffer-goto-next-message
"\C-p" 'slack-buffer-goto-prev-message)
('normal slack-edit-message-mode-map
",k" #'slack-message-cancel-edit
",s" #'slack-message-send-from-buffer
",2" #'slack-message-embed-mention
",3" #'slack-message-embed-channel))
#+END_SRC
#+BEGIN_SRC emacs-lisp
(use-package alert
:commands (alert)
:init
(setq alert-default-style (if (eq system-type 'darwin)
'osx-notifier
'libnotify)))
#+END_SRC
* Matrix
#+BEGIN_SRC emacs-lisp
(use-package matrix-client
:commands matrix-client-connect
:straight (matrix-client :host github :repo "alphapapa/matrix-client.el"
:files (:defaults "logo.png" "matrix-client-standalone.el.sh")))
#+END_SRC
* EMMS
The Emacs Multi-Media System. For libtag to work, libtag must be installed on the system via the system package manager. Then Emms should be installed from source via:
#+BEGIN_SRC shell :tangle no
git clone git://git.sv.gnu.org/emms.git
cd emms
make emms-print-metadata
make
make install
#+END_SRC
#+BEGIN_SRC emacs-lisp
(add-to-list 'load-path "/usr/local/share/emacs/site-lisp/emms")
(autoload 'emms-smart-browse "emms")
(autoload 'emms-start "emms")
(autoload 'emms-stop "emms")
(autoload 'emms-pause "emms")
(autoload 'emms-next "emms")
(autoload 'emms-previous "emms")
(autoload 'emms "emms")
(autoload 'emms-play-directory-tree "emms")
(autoload 'emms-all "emms-setup")
(with-eval-after-load 'emms
(emms-all)
(emms-default-players)
(require 'emms-info-libtag)
(setq emms-info-functions
'(emms-info-libtag)
emms-source-file-default-directory
(syncthing-directory "/music"))
(jdormit/define-prefix "ae" "emms")
(with-eval-after-load 'evil
(add-to-list 'evil-emacs-state-modes 'emms-browser-mode))
(general-def emms-browser-mode-map "," leader-map)
(general-def emms-playlist-mode-map "SPC" leader-map)
(general-def emms-playlist-mode-map "," leader-map)
(leader-def-key "aeb" 'emms-smart-browse)
(leader-def-key "aes" 'emms-start)
(leader-def-key "aeS" 'emms-stop)
(leader-def-key "aeP" 'emms-pause)
(leader-def-key "aen" 'emms-next)
(leader-def-key "aep" 'emms-previous)
(leader-def-key "aee" 'emms)
(leader-def-key "aed" 'emms-play-directory-tree)
(when (eq system-type 'darwin)
(define-emms-simple-player afplay '(file)
(regexp-opt '(".mp3" ".m4a" ".aac" ".m4p"))
"afplay")
(setq emms-player-list `(,emms-player-afplay))))
#+END_SRC
* Direnv
[[https://direnv.net/][Direnv]] automatically runs bash scripts when you enter certain directories. This sets it up to work with Emacs:
#+BEGIN_SRC emacs-lisp
(defun update-cider-env ()
(direnv-update-directory-environment nrepl-project-dir))
(use-package direnv
:if (executable-find "direnv")
:config
(direnv-mode)
(add-hook 'eshell-mode-hook #'direnv-update-directory-environment)
(add-hook 'eshell-directory-change-hook
(lambda ()
(unless (file-remote-p default-directory)
(direnv-update-directory-environment)))))
#+END_SRC
* SQL
Emacs has excellent built-in SQL support.
#+BEGIN_SRC emacs-lisp
(leader-def-key "sP" #'sql-postgres)
#+END_SRC
* GraphQL
GraphQL mode for editing GraphQL queries:
#+BEGIN_SRC emacs-lisp
(use-package graphql-mode
:mode "\\.gql\\'"
:commands (graphql-mode)
:config
(defvar graphql-env-alist '()
"An alist defining available GraphQL servers
The key is any symbol and the value is a cons pair of
(graphql-url . graphql-auth-token)")
(setq graphql-variables-file "~/variables.graphql")
(defun graphql-set-env (env)
(interactive
(list
(intern
(completing-read
"GraphQL env: "
(mapcar #'car graphql-env-alist)))))
(let* ((gql-params (alist-get env graphql-env-alist))
(url (car gql-params))
(auth-token (cdr gql-params)))
(setq graphql-url url
graphql-extra-headers `(("Authorization" . ,auth-token)))))
(defun graphql (env)
"Opens a graphql-mode buffer in the specified environment"
(interactive
(list
(intern
(completing-read
"GraphQL env: "
(mapcar #'car graphql-env-alist)))))
(graphql-set-env env)
(let ((graphql-buf (get-buffer-create "*graphql-query*")))
(set-buffer graphql-buf)
(when (string= "" (buffer-substring (point-min) (point-max)))
(insert "{\n \n}")
(goto-char 5))
(graphql-mode)
(switch-to-buffer graphql-buf)))
(defun find-graphql-variables-file ()
(interactive)
(find-file graphql-variables-file))
(jdormit/define-prefix "q" "graphql")
(leader-def-key "qe" #'graphql-set-env)
(leader-def-key "qf" #'find-graphql-variables-file)
(leader-def-key "aq" #'graphql)
(general-def graphql-mode-map "C-c C-e" #'graphql-set-env)
(general-def graphql-mode-map "C-c C-v" #'find-graphql-variables-file))
#+END_SRC
GraphQL environments:
#+BEGIN_SRC emacs-lisp
(setq graphql-env-alist
`((dev . ("https://api-dev.lola.co/api/graphql" .
,(password-store-get "lola-graphql-dev-token")))
(local . ("http://localhost:7200/api/graphql" .
,(password-store-get "lola-graphql-local-token")))))
#+END_SRC
And ob-graphql for evaluating GraphQL source blocks in org-mode:
#+BEGIN_SRC emacs-lisp
(use-package ob-graphql
:defer t)
#+END_SRC
* Docker
Syntax highlighting for Dockerfiles:
#+BEGIN_SRC emacs-lisp
(use-package dockerfile-mode
:mode ("\\Dockerfile\\'"))
#+END_SRC
* Kubernetes
#+BEGIN_SRC emacs-lisp
(use-package kubernetes
:ensure t
:commands (kubernetes-overview)
:init (leader-def-key "ak" #'kubernetes-overview)
:config
(add-to-list 'evil-emacs-state-modes 'kubernetes-overview-mode)
(general-def kubernetes-overview-mode-map "SPC" leader-map))
#+END_SRC
* AWS
** Switching profiles
Switch AWS profiles:
#+BEGIN_SRC emacs-lisp
(defvar aws-profiles '("default")
"AWS profile names")
(defvar aws-current-profile nil
"Currently active AWS profile")
(defun aws-local-profile ()
(make-local-variable 'aws-current-profile))
(add-hook 'eshell-mode-hook #'aws-local-profile)
(add-hook 'vterm-mode-hook #'aws-local-profile)
(add-hook 'term-mode-hook #'aws-local-profile)
(setq aws-current-profile (getenv "AWS_PROFILE"))
(add-to-list 'aws-profiles "personal")
(add-to-list 'aws-profiles "lola-cde")
(defun aws-switch-profile (profile)
(interactive (list (completing-read "Profile: " aws-profiles)))
(setenv "AWS_PROFILE" profile)
(setq aws-current-profile profile))
#+END_SRC
** AWS-MFA
The aws-mfa command:
#+BEGIN_SRC emacs-lisp
(defun aws-mfa (mfa-token)
(interactive (list
(let ((prompt (if aws-current-profile
(format "MFA code for %s: " aws-current-profile)
"MFA code: ")))
(read-from-minibuffer prompt))))
(let ((proc (start-process "aws-mfa"
"*aws-mfa*"
"aws-mfa"
"--force")))
(set-process-sentinel
proc
(make-success-err-msg-sentinel "*aws-mfa*"
"AWS MFA succeeded"
"AWS MFA failed, check *aws-mfa* buffer for details"))
(process-send-string proc (concat mfa-token "\n"))))
(with-eval-after-load 'kubernetes
(general-def kubernetes-overview-mode-map "m" #'aws-mfa))
#+END_SRC
** S3
#+BEGIN_SRC emacs-lisp
(use-package s3ed
:commands (s3ed-mode
s3ed-find-file
s3ed-save-file)
:init
(jdormit/define-prefix "fS" "s3")
(leader-def-key "fSf" #'s3ed-find-file)
(leader-def-key "fSs" #'s3ed-save-file)
:config
(defun browse-blob-in-emacs (url bufname)
(let* ((token (password-store-get "blob-logs-token"))
(buf (generate-new-buffer bufname))
(blob (shell-command-to-string (concat "curl "
"-s "
"--cookie 'TOKEN=" token "' "
url)))
(status
(with-current-buffer buf
(insert blob)
(json-mode)
(format-all-buffer))))
(if (not (equal status "Formatting error"))
(switch-to-buffer-other-window buf)
(with-current-buffer buf
(set-buffer-modified-p nil)
(kill-buffer))
(switch-to-buffer-other-window "*format-all-errors*"))))
(defun s3ed-open-blob-log (arg)
"Opens the blob log at point via the cloudfront proxy.
If given prefix arg ARG, opens in browser, otherwise opens in Emacs."
(interactive "P")
(let ((fname-at-point (dired-file-name-at-point)))
(if (string-match
"\\(bloblogs.ops.lola.co[m]*\\)\/blobs\/\\(\\(production\\|dev\\|local\\|smoke\\|staging\\)\/[0-9]+/.*\\)"
fname-at-point)
(let* ((proxy-url (format "https://%s" (match-string 0 fname-at-point))))
(if arg
(browse-url proxy-url)
(browse-blob-in-emacs proxy-url fname-at-point)))
(error "Not in a blob logs directory"))))
:general
((normal) s3ed-mode-map "B" #'s3ed-open-blob-log))
#+END_SRC
* Prodigy
[[https://github.com/rejeep/prodigy.el][Prodigy]] gives Emacs a nice way to run services (web servers, etc.).
#+BEGIN_SRC emacs-lisp
(use-package prodigy
:commands (prodigy)
:general
('normal 'prodigy-mode-map "SPC" leader-map)
('normal 'prodigy-view-mode-map "SPC" leader-map)
:config
(setq prodigy-completion-system 'default)
(add-hook 'prodigy-view-mode-hook (lambda () (toggle-truncate-lines 1))))
(leader-def-key "aP" #'prodigy)
#+END_SRC
Add the ability to associate a file with a service instead of a buffer:
#+BEGIN_SRC emacs-lisp
(with-eval-after-load 'prodigy
(defun prodigy-service-file (service)
"Return SERVICE file.
If SERVICE file exists, use that. If not, find the first SERVICE
tag that has a file and return that."
(let ((file (prodigy-service-or-first-tag-with service :file)))
(if (functionp file)
(prodigy-callback-with-plist file service)
file)))
(defun prodigy-display-process-file-or-buffer ()
(interactive)
(when-let (service (prodigy-service-at-pos))
(if-let (file (prodigy-service-file service))
(progn
(find-file-literally file)
(prodigy-view-mode)
(auto-revert-tail-mode)
(general-define-key
:states 'normal
:keymaps 'local
"Q" #'kill-this-buffer))
(prodigy-switch-to-process-buffer service))))
(general-def 'normal prodigy-mode-map "`" #'prodigy-display-process-file-or-buffer))
#+END_SRC
And add the ability to inhibit all service output processing:
#+BEGIN_SRC emacs-lisp
(with-eval-after-load 'prodigy
(defun prodigy-start-service (service &optional callback)
"Start process associated with SERVICE unless already started.
When CALLBACK function is specified, that is called when the
process has been started.
When the process is started, a timer starts and checks every
second for `prodigy-start-tryouts' times if the process is live.
If the process is not live after `prodigy-start-tryouts' seconds,
the process is put in failed status."
(declare (indent 1))
(unless (prodigy-service-started-p service)
(let* ((default-directory
(-if-let (cwd (prodigy-service-cwd service))
(f-full cwd)
default-directory))
(name (plist-get service :name))
(sudo (plist-get service :sudo))
(command (prodigy-service-command service))
(args (prodigy-service-args service))
(exec-path (append (prodigy-service-path service) exec-path))
(env (--map (s-join "=" it) (prodigy-service-env service)))
(process-environment (append env process-environment))
(process nil)
(create-process
(lambda ()
(unless process
(setq process (apply (if sudo 'prodigy-start-sudo-process 'start-process)
(append (list name nil command) args)))))))
(-when-let (init (prodigy-service-init service))
(funcall init))
(-when-let (init-async (prodigy-service-init-async service))
(let (callbacked)
(funcall
init-async
(lambda ()
(setq callbacked t)
(funcall create-process)))
(with-timeout
(prodigy-init-async-timeout
(error "Did not callback async callback within %s seconds"
prodigy-init-async-timeout))
(while (not callbacked) (accept-process-output nil 0.005)))))
(funcall create-process)
(let ((tryout 0))
(prodigy-every 1
(lambda (next)
(setq tryout (1+ tryout))
(if (process-live-p process)
(when callback (funcall callback))
(if (= tryout prodigy-start-tryouts)
(prodigy-set-status service 'failed)
(funcall next))))))
(plist-put service :process process)
(when (not (plist-get service :inhibit-process-filter))
(set-process-filter
process
(lambda (_ output)
(run-hook-with-args 'prodigy-process-on-output-hook service output))))
(set-process-query-on-exit-flag process nil)))))
#+END_SRC
* Lola
Some functions to make my day job easier.
** Services (Prodigy)
#+BEGIN_SRC emacs-lisp
(defun call-with-venv (venv callback)
(let ((venv-dir (cond
((file-exists-p venv) venv)
((file-exists-p
(substitute-in-file-name
(format "$WORKON_HOME/%s" venv)))
(substitute-in-file-name
(format "$WORKON_HOME/%s" venv)))
(t (error "virtual environment %s does not exist" venv)))))
(call-with-env-from-file (format "%s/bin/activate" venv-dir) callback)))
(defun kill-log-buffers ()
(interactive)
(kill-matching-buffers "\\.log$" nil t)
(message "Killed log buffers"))
(cl-defun python-service-setup (venv &optional env-file &key env-dir)
(lambda (done)
(call-with-venv
venv
(if env-file
(lambda ()
(call-with-env-from-file env-file done :dir env-dir))
done))))
(defun call-with-lola-env (callback)
(let ((process-environment
(cons (format "LOLA_ENV=%s"
(completing-read
"Environment: "
'("local" "development" "staging")))
process-environment)))
(funcall callback)))
(prodigy-define-tag
:name 'lola)
(prodigy-define-service
:name "lola-server (gunicorn)"
:tags '(lola backend)
:command "bash"
:args (lambda ()
(list
"-c"
"gunicorn -c server/web/gunicorn.conf.py \
-b 127.0.0.1:7200 bin.start_web:init_and_create_flask_app\\(\\) \
>> ~/lola/logs/lola-server.log 2>&1"))
:file "~/lola/logs/lola-server.log"
:inhibit-process-filter t
:cwd "~/lola/lola-server"
:stop-signal 'int
:truncate-output t
:init-async (python-service-setup "~/.pyenv/versions/lola-server"
"~/lola/lola-server/.env"))
(prodigy-define-service
:name "lola-server celery worker"
:tags '(lola backend)
:command "python"
:args '("bin/start_celery_worker.py" "-P" "gevent" "-n" "lola-server")
:cwd "~/lola/lola-server"
:stop-signal 'int
:truncate-output t
:init-async (python-service-setup "~/.pyenv/versions/lola-server"
"~/lola/lola-server/.env"))
(prodigy-define-service
:name "travel-service"
:tags '(lola backend)
:command "bash"
:args (lambda ()
(list
"-c" "python bin/start_web.py >> ~/lola/logs/travel-svc.log 2>&1"))
:cwd "~/lola/lola-travel-service"
:file "~/lola/logs/travel-svc.log"
:inhibit-process-filter t
:stop-signal 'int
:truncate-output t
:init-async (python-service-setup "~/.pyenv/versions/travel-service"
"~/lola/lola-travel-service/.env"))
(prodigy-define-service
:name "travel-service celery worker"
:tags '(lola backend)
:command "bash"
:args (lambda ()
(list
"-c"
(concat "python "
"bin/start_celery_workers.py "
"-n " "travel-service "
"-Q "
"default,io_pool,cpu_pool,priority_io_pool,priority_cpu_pool "
">> ~/lola/logs/travel-svc-celery.log 2>&1")))
:file "~/lola/logs/travel-svc-celery.log"
:inhibit-process-filter t
:cwd "~/lola/lola-travel-service"
:stop-signal 'int
:truncate-output t
:init-async (python-service-setup "~/.pyenv/versions/travel-service"
"~/lola/lola-travel-service/.env"))
(prodigy-define-service
:name "secrets"
:tags '(lola backend)
:command "python"
:args '("bin/cmdline.py" "www")
:cwd "~/lola/secrets"
:truncate-output t
:stop-signal 'int
:init-async (python-service-setup "~/.pyenv/versions/secrets"
"~/lola/secrets/.env"))
(prodigy-define-service
:name "lola-desktop"
:tags '(lola frontend)
:command "npm"
:args '("start")
:cwd "~/lola/lola-desktop"
:port 3001
:env '(("PORT" "3001"))
:stop-signal 'int
:init-async #'call-with-lola-env)
(prodigy-define-service
:name "wallet"
:tags '(lola frontend)
:command "npm"
:args '("start")
:cwd "~/lola/wallet"
:stop-signal 'int
:env '(("PORT" "3000"))
:init-async #'call-with-lola-env)
(prodigy-define-service
:name "agent-console"
:command "npm"
:args '("start")
:cwd "~/lola/agent-console"
:stop-signal 'int
:env '(("PORT" "3002"))
:init-async (lambda (done)
(call-with-lola-env
(lambda ()
(nvm-use "v10.15.1" done)))))
(prodigy-define-service
:name "luigid"
:command "luigid"
:cwd "~/lola/data-pipeline"
:port 8082
:stop-signal 'int
:init-async (python-service-setup "data-pipeline"
"~/lola/data-pipeline/.env"))
(prodigy-define-service
:name "prometheus"
:command "prometheus"
:args '("--config.file=prometheus.yml")
:port 9090
:stop-signal 'int
:cwd "~/prometheus")
(prodigy-define-service
:name "priceline-service"
:tags '(lola backend)
:command "~/lola/python_services/priceline/bin/start.sh"
:args '("web")
:cwd "~/lola/python_services"
:stop-signal 'int
:init-async (python-service-setup "~/lola/python_services/.venv"
"~/lola/python_services/priceline/.env"
:env-dir "~/lola/python_services"))
(prodigy-define-service
:name "priceline-cars-service"
:tags '(lola backend)
:command "bash"
:args '("-c"
"priceline_cars/bin/start.sh web >> ~/lola/logs/priceline-cars.log 2>&1")
:cwd "~/lola/python_services"
:inhibit-process-filter t
:file "~/lola/logs/priceline-cars.log"
:stop-signal 'int
:init-async (python-service-setup "~/lola/python_services/.venv"
"~/lola/python_services/priceline_cars/.env"
:env-dir "~/lola/python_services"))
(prodigy-define-service
:name "threev-service"
:tags '(lola backend)
:command "~/lola/python_services/threev/bin/start.sh"
:args '("web")
:cwd "~/lola/python_services"
:stop-signal 'int
:init-async (python-service-setup "~/lola/python_services/.venv"
"~/lola/python_services/threev/.env"
:env-dir "~/lola/python_services"))
(prodigy-define-service
:name "amd-flight-service"
:tags '(lola backend)
:command "~/lola/python_services/amd_flight/bin/start.sh"
:args '("web")
:cwd "~/lola/python_services"
:stop-signal 'int
:init-async (python-service-setup "~/lola/python_services/.venv"
"~/lola/python_services/amd_flight/.env"
:env-dir "~/lola/python_services"))
(prodigy-define-service
:name "ean-hotels-service"
:tags '(lola backend)
:command "bash"
:args '("-c"
"ean_hotels/bin/start.sh web >> ~/lola/logs/ean-hotels.log 2>&1")
:cwd "~/lola/python_services"
:inhibit-process-filter t
:file "~/lola/logs/ean-hotels.log"
:stop-signal 'kill
:init-async (python-service-setup "~/lola/python_services/.venv"
"~/lola/python_services/ean_hotels/.env"
:env-dir "~/lola/python_services"))
(prodigy-define-service
:name "smp-hotels-service"
:command "bash"
:args '("-c"
"smp_hotels/bin/start.sh web >> ~/lola/logs/smp-hotels.log 2>&1")
:cwd "~/lola/python_services"
:inhibit-process-filter t
:file "~/lola/logs/smp-hotels.log"
:stop-signal 'kill
:init-async (python-service-setup "~/lola/python_services/.venv"
"~/lola/python_services/smp_hotels/.env"
:env-dir "~/lola/python_services"))
(prodigy-define-service
:name "email-template-service"
:tags '(lola backend)
:command "npm"
:args '("start")
:cwd "~/lola/email-template-service"
:env '(("PORT" "7300"))
:stop-signal 'int
:init-async (lambda (done)
(nvm-use "10.15.1" done)))
(prodigy-define-service
:name "mabl-link-agent"
:command "link-agent"
:args (lambda ()
(list "-a" (password-store-get "mabl-link-agent")
"-n" "jdormit-macbook")))
(prodigy-define-service
:name "xray-daemon"
:command "xray_mac"
:args '("-o" "-n" "us-east-1"))
(prodigy-define-service
:name "spend-service"
:tags '(lola backend)
:command "~/lola/python_services/spend/bin/start.sh"
:args '("web")
:cwd "~/lola/python_services"
:stop-signal 'int
:init-async (python-service-setup "~/lola/python_services/.venv"
"~/lola/python_services/spend/.env"
:env-dir "~/lola/python_services"))
#+END_SRC
** Services (eShell)
#+BEGIN_SRC emacs-lisp :lexical yes
(defun run-service-in-eshell (name dir cmd &optional setup)
(if-let ((buf (get-buffer name)))
(progn (when (eq major-mode 'eshell-mode)
(eshell-interrupt-process)
(while eshell-process-list))
(kill-buffer buf)))
(let ((buf (get-buffer-create name)))
(switch-to-buffer buf)
(cd dir)
(eshell-mode)
(when setup (funcall setup))
(insert cmd)
(eshell-send-input)))
(defun release-manager ()
(interactive)
(run-service-in-eshell "*release-manager*"
"~/lola/release-manager"
"pipenv run python release-manager"))
(jdormit/define-prefix "L" "lola")
(leader-def-key "Lr" #'release-manager)
#+END_SRC
** Python stuff
Run pip install in the current project:
#+BEGIN_SRC emacs-lisp
(defun pip-install (reqs)
(interactive
(list
(read-file-name "Requirements file: "
(or (projectile-project-root)
default-directory)
nil t "requirements"
(lambda (name)
(string-match-p "requirements.*"
name)))))
(with-temp-buffer
(cd (or (projectile-project-root)
default-directory))
(compile (format "pip install -r %s" reqs))))
#+END_SRC
** 1Password
#+BEGIN_SRC emacs-lisp
(defvar op-token nil
"The 1Password session token")
(defun 1pass-signin ()
(interactive)
(cl-letf* ((signin-address "team-lolatravel.1password.com")
(signin-email "jeremydormitzer@lola.com")
(secret-key (password-store-get "1pass-lola-secret-key"))
((symbol-function 'op-signin)
(make-shell-fn "pass" "team-lolatravel.1password.com" "|" "op" "signin" signin-address signin-email secret-key "--output=raw"))
(token (op-signin)))
(if (string-match-p "ERROR" token)
(error "Unable to sign in to 1Password: %s" token)
(setf op-token token)
(message (format "Signed in to 1Password with session token %s" op-token))
op-token)))
(defun op-fn (&rest args)
(lambda (&optional input)
(cl-letf* (((symbol-function 'op-function)
(apply #'make-shell-fn "op" `(,@args ,(format "--session=%s" op-token))))
(output (op-function input)))
(if (or (string-match-p "Authentication required" output)
(string-match-p "You are not currently signed in" output))
(cl-letf* ((new-token (1pass-signin))
((symbol-function 'op-function)
(apply #'make-shell-fn "op" `(,@args ,(format "--session=%s" new-token)))))
(op-function input))
output))))
(defun op-list-items ()
(cl-flet ((op-list-items-fn (op-fn "list" "items")))
(mapcar #'cdr
(mapcar (apply-partially #'assoc 'title)
(mapcar (apply-partially #'assoc 'overview)
(json-read-from-string (op-list-items-fn)))))))
(defun op-get-item (item)
(cl-flet ((op-get-item (op-fn "get" "item" item)))
(json-read-from-string (op-get-item))))
(defun op-get-item-field (item-json field-designation)
(let* ((fields (assoc-recursive item-json 'details 'fields))
(pw-field (car (seq-filter
(lambda (field)
(string= field-designation (cdr (assoc 'designation field))))
fields))))
(when pw-field (cdr (assoc 'value pw-field)))))
(defun op-copy-password (item)
(interactive
(list
(completing-read "1Password item: " (op-list-items))))
(if-let ((password (op-get-item-field (op-get-item item) "password")))
(with-temp-buffer
(insert password)
(copy-region-as-kill (point-min) (point-max))
(message "Copied password for \"%s\" to kill ring." item))
;; TODO if no password found, prompt for alternate field in record to return
(error "No password found in 1Password for \"%s\"." item)))
(leader-def-key "ao" #'op-copy-password)
#+END_SRC
** Resetting DNSResponder
#+BEGIN_SRC emacs-lisp
(defun reset-dnsresponsder ()
(interactive)
(sudo-shell-command "killall -HUP mDNSResponder"))
#+END_SRC
** Devpi
#+BEGIN_SRC emacs-lisp
(defvar devpi-indices nil
"Login profiles for devpi. An alist
where the keys are index names and
the values are alists with the keys
devpi-index-url, devpi-index-username,
and devpi-index-password-fn.")
(setq devpi-indices
`((pip .
((devpi-index-url . "https://pip.aws.lolatravel.com/pip/dev")
(devpi-index-username . "pip")
(devpi-index-password-fn . ,(lambda ()
(op-get-item-field
(op-get-item "PIP Devpi Login")
"password")))))
(jdormit .
((devpi-index-url . "https://pip.aws.lolatravel.com/jdormit/dev")
(devpi-index-username . "jdormit")
(devpi-index-password-fn . ,(lambda ()
(password-store-get "devpi-jdormit")))))))
(defun devpi-use-index (index-name)
(interactive
(list
(intern
(completing-read
"Devpi index to use: "
(mapcar #'car devpi-indices)))))
(cl-letf* ((index-alist (alist-get index-name devpi-indices))
(index-url (alist-get 'devpi-index-url index-alist))
(index-username (alist-get 'devpi-index-username index-alist))
((symbol-function 'index-password-fn) (alist-get 'devpi-index-password-fn index-alist))
(index-password (index-password-fn))
((symbol-function 'devpi-use) (make-process-fn "devpi" "use" index-url))
((symbol-function 'devpi-login)
(make-process-fn "devpi" "login" index-username "--password" index-password))
(login-result (devpi-login))
(use-result (devpi-use)))
(save-match-data
(if (and (string-match "credentials valid" login-result)
(string-match "current devpi index" use-result))
(message "Switched to Devpi index %s" index-name)
(with-current-buffer (get-buffer-create "*devpi*")
(erase-buffer)
(goto-char (point-min))
(insert login-result)
(insert use-result))
(message "Failed to switch to Devpi index %s - check the *devpi* buffer for details" index-name)))))
(defun devpi-upload ()
(interactive)
(cl-letf (((symbol-function 'devpi-upload) (make-process-fn "devpi" "upload")))
(message (devpi-upload))))
#+END_SRC
** Release notes
#+BEGIN_SRC emacs-lisp
(defvar release-notes-repo (expand-file-name
"~/lola/release-manager/release_notes")
"The directory containing the release notes repository.")
(defun lola-release-notes (staged &optional repos)
"Prints release notes for the latest release.
If called with a prefix argument, prints release notes
for the upcoming release instead."
(interactive "P")
(let* ((all-repos '("lola-server"
"lola-travel-service"
"lola-desktop"))
(repos
(or
repos
(if (y-or-n-p "Query all repos?")
all-repos
(completing-read-multiple "Repos: "
all-repos)))))
(with-env-from-file (expand-file-name
(concat release-notes-repo "/.env"))
(async-shell-command
(format
(concat release-notes-repo "/venv/bin/python "
release-notes-repo "/release_notes/query_release_notes.py "
(when staged "--staged ")
" %s")
(combine-and-quote-strings repos))
"*release-notes*"))))
#+END_SRC
* StumpWM
A handy keybinding to connect to the StumpWM SBCL process via SLIME:
#+BEGIN_SRC emacs-lisp
(defun connect-stumpwm ()
(interactive)
(slime-connect "127.0.0.1" 4004))
#+END_SRC
* Emacs Network Client
Emacs frontend for networkmanager.
#+BEGIN_SRC emacs-lisp
(use-package enwc
:config
(setq enwc-default-backend 'nm)
(add-to-list 'evil-emacs-state-modes 'enwc-mode)
(general-def enwc-mode-map "SPC" leader-map)
:init
(leader-def-key "an" #'enwc)
:commands (enwc))
#+END_SRC
* Deft
A fuzzy-finder for notes.
#+BEGIN_SRC emacs-lisp
(use-package deft
:commands (deft)
:init
(setq deft-extensions '("org" "txt" "md" "markdown" "text")
deft-recursive t
deft-directory (org-directory))
;; Still lots of notes in the old Deft directory
(with-eval-after-load 'org
(add-to-list 'org-agenda-files (org-directory "deft")))
(leader-def-key "D" #'deft)
(leader-def-key "od" #'deft)
:config
(setq deft-use-filter-string-for-filename t
deft-file-naming-rules '((noslash . "-")
(nospace . "-")
(case-fn . downcase))
deft-auto-save-interval 0)
(add-to-list 'evil-emacs-state-modes 'deft-mode))
#+END_SRC
Tell Deft to use the TITLE property for entry titles, if it exists:
#+BEGIN_SRC emacs-lisp
(with-eval-after-load 'deft
(advice-add
'deft-parse-title :around
(lambda (old-fn file contents &rest args)
(let ((title (org-get-title contents)))
(if title
title
(apply old-fn file contents args))))))
#+END_SRC
* Pollen
#+BEGIN_SRC emacs-lisp
(use-package pollen-mode
:commands (pollen-server-start pollen-server-stop)
:mode ("\\.p\\'" "\\.pp\\'" "\\.pm\\'" "\\.ptree\\'"))
#+END_SRC
* Ngrok
#+BEGIN_SRC emacs-lisp
(defun ngrok (port &optional subdomain)
(interactive "nPort: \nsSubdomain: ")
(let ((buf (get-buffer-create
(concat "*ngrok-"
(number-to-string port)
"*")))
(cmd (if (and subdomain (not (string-empty-p subdomain)))
(concat "ngrok http "
"--subdomain=" subdomain " "
(number-to-string port))
(concat "ngrok http " (number-to-string port)))))
(async-shell-command cmd buf)))
#+END_SRC
* Make
#+BEGIN_SRC emacs-lisp
(defun make ()
(interactive)
(let ((project-root (projectile-project-root)))
(if project-root
(with-temp-buffer
(cd project-root)
(async-shell-command "make"))
(error "Not in a project"))))
#+END_SRC
* Redis
#+BEGIN_SRC emacs-lisp
(defun redis-cli (&optional host port)
(interactive (list (read-string "host (default localhost): " nil nil "localhost")
(read-number "port: " 6379)))
(let ((cli-path (executable-find "redis-cli"))
(host (or host "localhost"))
(port (or port 6379)))
(if cli-path
(progn
(make-comint-in-buffer
"redis-cli"
nil
"redis-cli"
nil
"-h" host
"-p" (number-to-string port)
"--no-raw")
(switch-to-buffer "*redis-cli*"))
(error "Can't find redis-cli"))))
#+END_SRC
* Restclient
Explore APIs from within Emacs!
#+BEGIN_SRC emacs-lisp
(use-package restclient
:commands (restclient-mode))
(use-package company-restclient
:after (restclient company)
:config
(add-to-list 'company-backends 'company-restclient))
(use-package ob-restclient
:defer t
:hook (org-mode . (lambda () (require 'ob-restclient))))
#+END_SRC
* IMenu
Get a nice IMenu sidebar:
#+BEGIN_SRC emacs-lisp
(use-package imenu-list
:config
(setq imenu-list-focus-after-activation t)
:general
(imenu-list-major-mode-map "SPC" leader-map)
(imenu-list-major-mode-map "." #'imenu-list-display-entry))
(defun open-imenu-list ()
(interactive)
(if (and (fboundp 'lsp-ui-imenu)
(boundp 'lsp-mode)
lsp-mode)
(lsp-ui-imenu)
(imenu-list-smart-toggle)))
(leader-def-key "\\" #'open-imenu-list)
(leader-def-key "m" #'imenu)
#+END_SRC
* calfw-org
A fancy calendar view:
#+BEGIN_SRC emacs-lisp
(use-package calfw
:commands (cfw:open-calendar-buffer))
(use-package calfw-org
:config (require 'calfw)
:commands (cfw:open-org-calendar))
(leader-def-key "oC" #'cfw:open-org-calendar)
#+END_SRC
* hackernews
#+BEGIN_SRC emacs-lisp
(use-package hackernews
:commands (hackernews)
:config
(setq hackernews-internal-browser-function 'w3m-browse-url)
:general
('normal hackernews-mode-map "SPC" leader-map))
(leader-def-key "ah" 'hackernews)
#+END_SRC
* counsel-spotify
Spotify in Emacs!
#+BEGIN_SRC emacs-lisp
(use-package counsel-spotify
:commands (counsel-spotify-search-track
counsel-spotify-search-album
counsel-spotify-search-artist
counsel-spotify-seach-tracks-by-album
counsel-spotify-search-tracks-by-artist
counsel-spotify-play
counsel-spotify-toggle-play-pause
counsel-spotify-next
counsel-spotify-previous)
:config
(setq counsel-spotify-client-id "825add4224704126adf3912b847c86df"
counsel-spotify-client-secret "f71fb236c06b4af886658667056ef7bd"))
(jdormit/define-prefix "iS" "counsel-spotify")
(leader-def-key "iSt" #'counsel-spotify-search-track)
(leader-def-key "iSa" #'counsel-spotify-search-album)
(leader-def-key "iSA" #'counsel-spotify-search-artist)
(leader-def-key "iS SPC" #'counsel-spotify-toggle-play-pause)
(leader-def-key "iSn" #'counsel-spotify-next)
(leader-def-key "iSp" #'counsel-spotify-previous)
#+END_SRC
* Flyspell
#+BEGIN_SRC emacs-lisp
(when (executable-find "hunspell")
(setq ispell-program-name "hunspell"
ispell-really-hunspell t))
(add-hook 'text-mode-hook #'flyspell-mode)
#+END_SRC
* VTerm
A better terminal emulator for Emacs. Replaces ansi-term, not EShell.
#+BEGIN_SRC emacs-lisp
(defun eshell-exec-in-vterm (&rest args)
(let* ((program (car args))
(buf (generate-new-buffer
(concat "*" (file-name-nondirectory program) "*"))))
(with-current-buffer buf
(vterm-mode)
(vterm-send-string (concat (s-join " " args) "\n")))
(switch-to-buffer buf)))
(defun vterm-send-interactively (key)
(interactive "cSend key: ")
(vterm-send (format "%c" key)))
(use-package vterm
:if module-file-suffix
:init
(with-eval-after-load 'em-term
(defun eshell-exec-visual (&rest args)
(apply #'eshell-exec-in-vterm args)))
:config
(add-hook 'vterm-mode-hook #'compilation-shell-minor-mode)
:commands (vterm vterm-other-window vterm-mode)
:general
(vterm-mode-map "M-\\" #'vterm-send-interactively))
(defun run-vterm (&optional new-buffer)
(interactive "P")
(let ((buffer-name (when (not new-buffer) "vterm")))
(if (and buffer-name (get-buffer buffer-name))
(switch-to-buffer buffer-name)
(vterm buffer-name))))
(defun open-vterm (&optional arg)
(interactive "P")
(if (and (fboundp 'projectile-project-root)
(projectile-project-root))
(projectile-run-vterm arg)
(run-vterm arg)))
(leader-def-key "sv" 'open-vterm)
#+END_SRC
* Hide mode line
Does what it says on the box.
#+BEGIN_SRC emacs-lisp
(use-package hide-mode-line
:hook ((dired-sidebar-mode imenu-list-major-mode) . hide-mode-line-mode))
#+END_SRC
* Dash
[[https://kapeli.com/dash][Dash]] is a code browser app for MacOS. [[https://github.com/stanaka/dash-at-point][dash-at-point]] is an Emacs interface to it.
#+BEGIN_SRC emacs-lisp
(use-package dash-at-point
:if (file-exists-p "/Applications/Dash.app")
:general
((normal visual motion insert emacs) "C-c d" 'dash-at-point)
((normal visual motion insert emacs) "C-c e" 'dash-at-point-with-docset))
#+END_SRC
* Alfred
Add a function to power the [[https://orgmode.org/worg/org-contrib/alfred-org-capture.html][alfred-org-capture]] Alfred workflow:
#+BEGIN_SRC emacs-lisp
(defun make-orgcapture-frame ()
"Create a new frame and run org-capture."
(interactive)
(make-frame '((name . "Org Capture") (width . 100) (height . 24)
(top . 400) (left . 300)))
(select-frame-by-name "Org Capture")
(add-hook 'org-capture-after-finalize-hook #'delete-frame t t)
(counsel-org-capture))
#+END_SRC
* gist.el
[[https://github.com/defunkt/gist.el][Integrate with GitHub gists]] from Emacs!
#+BEGIN_SRC emacs-lisp
(use-package gist
:defer t
:config
(add-hook 'gist-mode-hook
(lambda ()
(evil-ex-define-local-cmd "w[rite]" 'gist-mode-save-buffer))))
#+END_SRC
* ffap
Add the ability to jump to a particular line number in find-file-at-point. "Borrowed" from the [[https://www.emacswiki.org/emacs/FindFileAtPoint#toc6][EmacsWiki]].
#+BEGIN_SRC emacs-lisp
(defvar ffap-file-at-point-line-number nil
"Variable to hold line number from the last `ffap-file-at-point' call.")
(defadvice ffap-file-at-point (after ffap-store-line-number activate)
"Search `ffap-string-at-point' for a line number pattern and
save it in `ffap-file-at-point-line-number' variable."
(let* ((string (ffap-string-at-point)) ;; string/name definition copied from `ffap-string-at-point'
(name
(or (condition-case nil
(and (not (string-match "//" string)) ; foo.com://bar
(substitute-in-file-name string))
(error nil))
string))
(line-number-string
(and (string-match ":[0-9]+" name)
(substring name (1+ (match-beginning 0)) (match-end 0))))
(line-number
(and line-number-string
(string-to-number line-number-string))))
(if (and line-number (> line-number 0))
(setq ffap-file-at-point-line-number line-number)
(setq ffap-file-at-point-line-number nil))))
(defadvice find-file-at-point (after ffap-goto-line-number activate)
"If `ffap-file-at-point-line-number' is non-nil goto this line."
(when ffap-file-at-point-line-number
(goto-line ffap-file-at-point-line-number)
(setq ffap-file-at-point-line-number nil)))
#+END_SRC
* Format-all-the-code
A package that bundles together common code beautifying tools for many languages (the actual formatting tools still need to be installed separately):
#+BEGIN_SRC emacs-lisp
(use-package format-all
:commands (format-all-buffer)
:config
(defun format-all--resolve-system (choices)
"Get first choice matching `format-all--system-type' from CHOICES."
(cl-dolist (choice choices)
(cond ((atom choice)
(cl-return choice))
((eql format-all--system-type (car choice))
(cl-return (cadr choice))))))
(when (executable-find "zprint")
(define-format-all-formatter zprint
(:executable "zprint")
(:install "https://github.com/kkinnear/zprint/releases")
(:languages "Clojure")
(:format (format-all--buffer-easy executable))))
:init
(leader-def-key "cf" 'format-all-buffer))
#+END_SRC
* Compiling
Enable ANSI colors in compile buffers:
#+BEGIN_SRC emacs-lisp
(autoload 'ansi-color-apply-on-region "ansi-color")
(defun local/postprocess-compilation-buffer ()
(goto-char compilation-filter-start)
(when (looking-at "\033c")
(delete-region (point-min) (match-end 0)))
(ansi-color-apply-on-region (point) (point-max)))
(add-hook 'compilation-filter-hook 'local/postprocess-compilation-buffer)
#+END_SRC
Set up some keybindings for Comint-mode compilation buffers:
#+BEGIN_SRC emacs-lisp
(general-def (normal visual motion) compilation-shell-minor-mode-map "q" #'quit-window)
(general-def (normal visual motion) compilation-shell-minor-mode-map "gr" #'recompile)
#+END_SRC
Recognize text of the form <filename>:<line number> as a compilation
error:
#+BEGIN_SRC emacs-lisp
(with-eval-after-load 'compile
(add-to-list 'compilation-error-regexp-alist '("\\([^[:space:]]+\\):\\([[:digit:]]+\\)$" 1 2)))
#+END_SRC
* Run-command
A [[https://github.com/bard/emacs-run-command][useful library]] that lets you define dynamic commands based on the
current buffer:
#+begin_src emacs-lisp
(use-package run-command
:straight (run-command :host github :repo "bard/emacs-run-command"
:fork (:host github :repo "jdormit/emacs-run-command" :branch "master"))
:init
(defvar-local run-command-local-commands nil)
(put 'run-command-local-commands 'safe-local-variable (lambda (_) t))
(defvar-local run-command-project-local-commands nil)
(put 'run-command-project-local-commands 'safe-local-variable (lambda (_) t))
:config
(defun run-command-recipe-terraform ()
(when (directory-files default-directory
nil
(rx (one-or-more any)
".tf"
eol))
(list
(list :command-name "init"
:command-line "terraform init")
(list :command-name "plan"
:command-line "terraform plan")
(list :command-name "apply"
:command-line "terraform apply")
(list :command-name "destroy"
:command-line "terraform destroy"))))
(defun run-command-recipe-local ()
(when run-command-local-commands
(mapcar (cl-function
(lambda ((name . spec))
(let ((name (if (symbolp name) (symbol-name name) name)))
(list :command-name name
:command-line spec))))
run-command-local-commands)))
(defun run-command-recipe-package-json--get-scripts (package-json-file)
"Extract NPM scripts from `package-json-file'."
(with-temp-buffer
(insert-file-contents package-json-file)
(let* ((json-data (json-parse-buffer))
(script-hash (gethash "scripts" json-data))
(scripts '()))
(when script-hash
(maphash (lambda (key _value) (push key scripts)) script-hash))
scripts)))
(defun run-command-recipe-package-json ()
(when-let* ((project-dir
(locate-dominating-file default-directory "package.json"))
(scripts
(run-command-recipe-package-json--get-scripts (concat project-dir "package.json")))
(script-runner
(if (file-exists-p (concat project-dir "yarn.lock")) "yarn" "npm")))
(mapcar (lambda (script)
(list :command-name script
:command-line (concat script-runner " run " script)
:display script
:working-dir project-dir))
scripts)))
(defun makefile-target-list-default (makefile)
"Return the target list for MAKEFILE by parsing it."
(let (targets)
(with-temp-buffer
(insert-file-contents makefile)
(goto-char (point-min))
(while (re-search-forward "^\\([^: \n]+\\) *:\\(?: \\|$\\)" nil t)
(let ((str (match-string 1)))
(unless (string-match "^\\." str)
(push str targets)))))
(nreverse targets)))
(defun run-command-recipe-makefile ()
(when-let* ((project-dir (locate-dominating-file default-directory "Makefile"))
(makefile (concat project-dir "Makefile"))
(targets (makefile-target-list-default makefile)))
(mapcar (lambda (target)
(list :command-name target
:command-line (concat "make " target)
:display target
:working-dir project-dir))
targets)))
(defun run-command-recipe-project ()
(when (projectile-project-root)
(mapcar (lambda (cmd)
(when-let ((cmd-val (symbol-value
(intern
(format "projectile-project-%s-cmd" cmd)))))
(list :command-name cmd
:command-line cmd-val
:working-dir (projectile-compilation-dir))))
'("test" "run" "compilation" "configure" "install" "package"))))
(defun run-command-recipe-project-local ()
(when (and run-command-project-local-commands (projectile-project-root))
(mapcar (cl-function
(lambda ((name . spec))
(let ((name (if (symbolp name) (symbol-name name) name)))
(list :command-name name
:command-line spec
:working-dir (projectile-compilation-dir)))))
run-command-project-local-commands)))
(defun run-command-recipe-executables ()
(let* ((buffer-file (buffer-file-name))
(executable-p (and buffer-file (file-executable-p buffer-file))))
(list
(when executable-p
(list
:command-name "run-buffer-file"
:command-line buffer-file
:display "Run this buffer's file"))
(when (and executable-p (executable-find "entr"))
(list
:command-name "run-buffer-file-watch"
:command-line (format "echo %s | entr -c /_" buffer-file)
:display "Run this buffer's file (re-run on each save)")))))
(defun run-command-recipe-obelix ()
(when-let* ((config (or (locate-dominating-file default-directory "obelix.json")
(locate-dominating-file default-directory "obelix.edn")))
(dir (file-name-directory config)))
(list
(list :command-name "build"
:command-line "obelix build"
:working-dir dir)
(list :command-name "serve"
:command-line "obelix serve"
:working-dir dir))))
(defun run-command-recipe-kustomize ()
(when-let* ((kustomization (locate-dominating-file default-directory "kustomization.yaml"))
(dir (file-name-directory kustomization)))
(list
(list :command-name "build"
:command-line "kustomize build --enable_alpha_plugins"
:working-dir dir))))
(defun run-command-recipe-sops ()
(when (save-excursion
(goto-char (point-min))
(search-forward-regexp "sops:" nil t))
(list
(list :command-name "edit"
:command-line (format "sops %s" (buffer-file-name))))))
(defun run-command-recipe-pytest ()
(when (and (derived-mode-p 'python-mode)
(= 0 (call-process "python" nil nil nil "-c" "import pytest")))
(let ((test-file-p (string-match-p "test" (or (buffer-file-name) ""))))
(list
(when (and (projectile-project-root)
(file-exists-p (concat (file-name-as-directory (projectile-project-root))
"tests")))
(list :command-name "test all"
:command-line "pytest tests"
:working-dir (projectile-project-root)))
(when test-file-p
(list :command-name "test this file"
:command-line (format "pytest %s" (buffer-file-name))))
(when (and test-file-p (python-info-current-defun))
(list :command-name "test this function"
:command-line (format "pytest %s::%s"
(buffer-file-name)
(replace-regexp-in-string
"\\."
"::"
(python-info-current-defun)))))))))
(defun run-command-recipe-web-ext ()
(when-let* ((_ (executable-find "web-ext"))
(manifest (locate-dominating-file default-directory "manifest.json"))
(dir (file-name-directory manifest)))
(list
(list :command-name "run"
:command-line "web-ext run"
:working-dir dir)
(list :command-name "build"
:command-line "web-ext build"
:working-dir dir))))
:general
(leader-map "\"" #'run-command)
(run-command-term-minor-mode-map [remap recompile] #'run-command-term-recompile)
:custom
(run-command-run-method 'async-shell)
(run-command-recipes '(run-command-recipe-terraform
run-command-recipe-local
run-command-recipe-package-json
run-command-recipe-makefile
run-command-recipe-project
run-command-recipe-project-local
run-command-recipe-executables
run-command-recipe-obelix
run-command-recipe-kustomize
run-command-recipe-sops
run-command-recipe-pytest
run-command-recipe-web-ext)))
#+end_src
* Wallabag
[[https://github.com/jdormit/emacs-wallabag-client][My Wallabag client]] is still a WIP, but it is useful enough to pull in right now:
#+BEGIN_SRC emacs-lisp
(use-package wallabag
:defer t
:straight (:host github :repo "jdormit/emacs-wallabag-client")
:custom
(wallabag-base-url "https://wallabag.jeremydormitzer.com")
(wallabag-client-id (password-store-get "wallabag-client-id"))
(wallabag-client-secret (password-store-get "wallabag-client-secret"))
(wallabag-username "jdormit")
(wallabag-password (password-store-get "wallabag.jeremydormitzer.com")))
#+END_SRC
* Clipmon
Syncs up the X Windows clipboard with the Emacs kill ring. Only
necessary on Linux.
#+BEGIN_SRC emacs-lisp
(use-package clipmon
:if (eq window-system 'x)
:hook (after-init . clipmon-mode-start))
#+END_SRC
* GUD
Emacs ships with a built-in debugger interface that I primarily use
for PDB. The default keybindings are RSI-inducing, so here's a hydra
to use instead:
#+BEGIN_SRC emacs-lisp
(defhydra hydra-gud (:hint nil)
"
_n_: step over _b_: set breakpoint _e_: execute statement _l_: refresh buffer
_s_: step into _d_: remove breakpoint _p_: print statement _q_: quit hyra
_>_: up stack frame _f_: finish function _w_: watch expression
_<_: down stack frame _c_: continue
"
(">" gud-up)
("<" gud-down)
("b" gud-break)
("d" gud-remove)
("e" gud-statement)
("f" gud-finish)
("l" gud-refresh)
("n" gud-next)
("p" gud-print)
("c" gud-cont :color blue)
("s" gud-step)
("w" gud-watch)
("q" nil))
(leader-def-key "cg" #'hydra-gud/body)
#+END_SRC
* realgud
Like [[info:emacs#Debuggers][GUD]], but better!
#+BEGIN_SRC emacs-lisp
(use-package realgud
:commands (realgud:pdb)
:init
(defun projectile-pdb ()
(interactive)
(with-projectile-root
(funcall-interactively #'realgud:pdb))))
#+END_SRC
* EDiff
#+BEGIN_SRC emacs-lisp
(setq ediff-window-setup-function #'ediff-setup-windows-plain)
#+END_SRC
* Apache Drill
#+BEGIN_SRC emacs-lisp
(use-package sql-drill
:straight (:host github :repo "jdormit/sql-drill.el")
:commands (sql-drill)
:init
(when (file-exists-p "~/drill/apache-drill-1.17.0/bin/drill-embedded")
(setq sql-drill-program
(expand-file-name "~/drill/apache-drill-1.17.0/bin/drill-embedded"))))
#+END_SRC
* Helpful
A much-improved help buffer:
#+BEGIN_SRC emacs-lisp
(use-package helpful
:defer t
:init
(setq counsel-describe-function-function #'helpful-callable
counsel-describe-variable-function #'helpful-variable)
(add-hook 'emacs-lisp-mode-hook
(lambda ()
(set (make-local-variable 'evil-lookup-func)
#'helpful-at-point)))
:general
(help-map "k" #'helpful-key))
#+END_SRC
* Inform
Provides links to the help documentation for ELisp symbols from Info
buffers:
#+BEGIN_SRC emacs-lisp
(use-package inform
:straight (:host github :repo "dieter-wilhelm/inform")
:defer 0
:config
;; If Helpful is installed, link to Helpful buffers instead of vanilla help
(with-eval-after-load 'helpful
(define-button-type 'inform-function
:supertype 'inform-xref
'inform-function 'helpful-function
'inform-echo (purecopy "mouse-2, RET: describe this function"))
(define-button-type 'inform-variable
:supertype 'inform-xref
'inform-function 'helpful-variable
'inform-echo (purecopy "mouse-2, RET: describe this variable"))
(define-button-type 'inform-symbol
:supertype 'inform-xref
'inform-function #'helpful-symbol
'inform-echo (purecopy "mouse-2, RET: describe this symbol"))))
#+END_SRC
* Vuiet
A music browser and player:
#+BEGIN_SRC emacs-lisp
(use-package versuri
:straight (versuri :host github :repo "mihaiolteanu/versuri")
:defer t)
(use-package vuiet
:straight (vuiet :host github :repo "mihaiolteanu/vuiet")
:commands (vuiet-track-artist
vuiet-track-name
vuiet-track-duration
vuiet--playing-track)
:defer t
:config
(setq vuiet-update-mode-line-automatically t
vuiet-update-mode-line-interval 1)
(defun vuiet-mode-setup ()
(set (make-local-variable 'org-link-elisp-confirm-function)
nil))
(add-hook 'vuiet-mode-hook #'vuiet-mode-setup)
:general
((normal motion visual) vuiet-mode-map "q" #'quit-window)
((normal motion visual) vuiet-mode-map "C-m" #'org-open-at-point))
#+END_SRC
And a handy hydra for it:
#+BEGIN_SRC emacs-lisp
(defun now-playing (length)
(let* ((str (s-truncate length
(if-let ((track (vuiet--playing-track)))
(format "%s - %s"
(vuiet-track-artist track)
(vuiet-track-name track))
"----")))
(remainder (- length (length str))))
(if (> remainder 0)
(let* ((left-pad (/ remainder 2))
(right-pad (- remainder left-pad)))
(s-prepend (s-repeat left-pad " ")
(s-append (s-repeat right-pad " ") str)))
str)))
(defun track-pos ()
(if-let ((track (vuiet--playing-track)))
(format "[%s/%s]"
(format-time-string "%M:%S" (condition-case nil
(mpv-get-playback-position)
(error 0)))
(s-pad-left 5 "0" (vuiet-track-duration track)))
"[00:00/00:00]"))
(defvar hydra-vuiet-body-timer)
(defun refresh-hydra-vuiet ()
(interactive)
(hydra-show-hint hydra-vuiet/hint 'refresh-hydra-vuiet))
(defhydra hydra-vuiet (:hint nil
:color blue
:body-pre (setq hydra-vuiet-body-timer
(run-at-time 1 1 #'refresh-hydra-vuiet))
:post (cancel-function-timers 'refresh-hydra-vuiet))
"
--- ^^ ^^ ^^ ---
+------------------/ / ^^ ^^ ^^ +------------------/ /
| - ------- / / ^^ ^^ ^^ | - ------- / /
|(-) .d########b. //)|+------------^^----------------^^------------^^-----+|(-) .d########b. //)| Play [_t_]ag(s)
| .d############// ||+-----------^^----------------^^------------^^----+|| .d############// | Play [_a_]rtist
| .d######''####//b. |||%s(now-playing 43)^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^||| .d######''####//b. | Play a[_l_]bum
| 9######( )#-//##P ||+-----------^^--+-------------^^+-----------^^----+|| 9######( )#-//##P |
| 'b######++#/-/##d' || ^^ |%s(track-pos)^^| ^^ || 'b######++#/-/##d' | [_L_]ove current track
| '9############P' || ^^ +-------------^^+ ^^ || '9############P' | [_U_]nlove current track
| -'9a#######aP' || +---------^^+---------------^^--+---------^^--+ || -'9a#######aP' |
| |-| `'''''' || | _s_top | _p_lay/pause | _n_ext | || |-| `'''''' | [_I_]nfo menu
| ---..----------- || +---------^^+---------------^^--+---------^^--+ || ---..----------- | [_S_]imilar menu
| |---||-----------| |+------------^^----------------^^------------^^-----+| |---||-----------| |
| | ^^ ^^ ^^ | | [_q_]uit
+--------------------+ ^^ ^^ ^^ +--------------------+
"
("s" vuiet-stop)
("p" vuiet-play-pause :color red)
("n" vuiet-next :color red)
("t" vuiet-play-tag-similar)
("a" vuiet-play-artist)
("l" vuiet-play-album)
("L" vuiet-love-track)
("U" vuiet-unlove-track)
("I" hydra-vuiet-info/body)
("S" hydra-vuiet-similar/body)
("q" nil))
(general-def "C-c v" #'hydra-vuiet/body)
(defvar hydra-vuiet-info-body-timer)
(defun refresh-hydra-vuiet-info ()
(interactive)
(hydra-show-hint hydra-vuiet-info/hint 'refresh-hydra-vuiet-info))
(defhydra hydra-vuiet-info (:hint nil
:color blue
:body-pre (setq hydra-vuiet-info-body-timer
(run-at-time 1 1 #'refresh-hydra-vuiet-info))
:post (cancel-function-timers 'refresh-hydra-vuiet-info))
"
--- ^^ ^^ ^^ ---
+------------------/ / ^^ ^^ ^^ +------------------/ /
| - ------- / / ^^ ^^ ^^ | - ------- / /
|(-) .d########b. //)|+------------^^----------------^^------------^^-----+|(-) .d########b. //)| Info for [_t_]ag
| .d############// ||+-----------^^----------------^^------------^^----+|| .d############// | Info for [_a_]rtist
| .d######''####//b. |||%s(now-playing 43)^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^||| .d######''####//b. | Info for a[_l_]bum
| 9######( )#-//##P ||+-----------^^--+-------------^^+-----------^^----+|| 9######( )#-//##P | Info for [_c_]urrent artist
| 'b######++#/-/##d' || ^^ |%s(track-pos)^^| ^^ || 'b######++#/-/##d' | L[_y_]rics for current track
| '9############P' || ^^ +-------------^^+ ^^ || '9############P' | [_L_]oved tracks
| -'9a#######aP' || +---------^^+---------------^^--+---------^^--+ || -'9a#######aP' |
| |-| `'''''' || | _s_top | _p_lay/pause | _n_ext | || |-| `'''''' | [_P_]layer menu
| ---..----------- || +---------^^+---------------^^--+---------^^--+ || ---..----------- | [_S_]imilar menu
| |---||-----------| |+------------^^----------------^^------------^^-----+| |---||-----------| |
| | ^^ ^^ ^^ | | [_q_]uit
+--------------------+ ^^ ^^ ^^ +--------------------+
"
("s" vuiet-stop)
("p" vuiet-play-pause :color red)
("n" vuiet-next :color red)
("t" vuiet-tag-info)
("a" vuiet-artist-info-search)
("l" vuiet-album-info-search)
("c" vuiet-playing-artist-info)
("y" vuiet-playing-track-lyrics)
("L" vuiet-loved-tracks-info)
("P" hydra-vuiet/body)
("S" hydra-vuiet-similar/body)
("q" nil))
(defvar hydra-vuiet-similar-body-timer)
(defun refresh-hydra-vuiet-similar ()
(interactive)
(hydra-show-hint hydra-vuiet-similar/hint 'refresh-hydra-vuiet-similar))
(defhydra hydra-vuiet-similar (:hint nil
:color blue
:body-pre (setq hydra-vuiet-similar-body-timer
(run-at-time 1 1 #'refresh-hydra-vuiet-similar))
:post (cancel-function-timers 'refresh-hydra-vuiet-similar))
"
--- ^^ ^^ ^^ ---
+------------------/ / ^^ ^^ ^^ +------------------/ /
| - ------- / / ^^ ^^ ^^ | - ------- / /
|(-) .d########b. //)|+------------^^----------------^^------------^^-----+|(-) .d########b. //)| Play [_t_]ag similar
| .d############// ||+-----------^^----------------^^------------^^----+|| .d############// | Play [_a_]rtist similar
| .d######''####//b. |||%s(now-playing 43)^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^||| .d######''####//b. | Play [_l_]oved tracks similar
| 9######( )#-//##P ||+-----------^^--+-------------^^+-----------^^----+|| 9######( )#-//##P |
| 'b######++#/-/##d' || ^^ |%s(track-pos)^^| ^^ || 'b######++#/-/##d' | Play [_c_]urrent artist similar
| '9############P' || ^^ +-------------^^+ ^^ || '9############P' | Play current [_T_]ag similar
| -'9a#######aP' || +---------^^+---------------^^--+---------^^--+ || -'9a#######aP' |
| |-| `'''''' || | _s_top | _p_lay/pause | _n_ext | || |-| `'''''' | [_P_]layer menu
| ---..----------- || +---------^^+---------------^^--+---------^^--+ || ---..----------- | [_I_]nfo menu
| |---||-----------| |+------------^^----------------^^------------^^-----+| |---||-----------| |
| | ^^ ^^ ^^ | | [_q_]uit
+--------------------+ ^^ ^^ ^^ +--------------------+
"
("s" vuiet-stop)
("p" vuiet-play-pause :color red)
("n" vuiet-next :color red)
("t" vuiet-play-tag-similar)
("a" vuiet-play-artist-similar)
("l" vuiet-play-loved-tracks-similar)
("c" vuiet-play-playing-artist-similar)
("T" vuiet-play-playing-tag-similar)
("P" hydra-vuiet/body)
("I" hydra-vuiet-info/body)
("q" nil))
#+END_SRC
* Twitter
Socially networking from Emacs:
#+BEGIN_SRC emacs-lisp
(use-package twittering-mode
:commands (twit)
:init
(leader-def-key "at" #'twit)
:config
(setq twittering-use-master-password t)
(defun twittering-browse-uri (uri)
(interactive (list (or (get-text-property (point) 'uri)
(if (get-text-property (point) 'field)
(let* ((id (get-text-property (point) 'id))
(status (twittering-find-status id)))
(twittering-get-status-url-from-alist status))
nil))))
(when uri (browse-url uri)))
:general
(twittering-mode-map "SPC" leader-map)
(twittering-mode-map "W" #'twittering-browse-uri))
#+END_SRC
* Structlog
#+BEGIN_SRC emacs-lisp
(use-package structlog-mode
:straight (structlog-mode :repo "https://git.jeremydormitzer.com/jdormit/structlog-el.git")
:commands (structlog)
:config
(setq structlog-db-username "jdormit"
structlog-db-database "fluentd"))
#+END_SRC
* PlantUML
#+BEGIN_SRC emacs-lisp
(use-package plantuml-mode
:commands (plantuml-mode)
:config
(setq plantuml-default-exec-mode 'jar
plantuml-jar-path "~/bin/plantuml.jar"
org-plantuml-jar-path "~/bin/plantuml.jar"))
#+END_SRC
* Typo mode
An incredibly useful mode for editing typographic text - automatically
makes quotes curly and dashes long, etc.
#+BEGIN_SRC emacs-lisp
(use-package typo
:commands (typo-mode typo-global-mode))
#+END_SRC
* Open Policy Agent
#+BEGIN_SRC emacs-lisp
(use-package rego-mode
:mode (("\\.rego\\'" . rego-mode)))
#+END_SRC
* Homebrew
#+begin_src emacs-lisp
(use-package homebrew
:straight (:host github :repo "jdormit/homebrew.el")
:defer t)
#+end_src