224 KiB
Executable File
- Prelude
- Default directory
- Packages
- Benchmarking
- Load org mode
- Doom themes
- Ewal theme
- Customization File
- Path
- General
- Autocompletion
- Which-key
- Evil Mode
- Multiple cursors
- Transient
- Hydra
- Magit
- git-link
- with-editor
- Password Store
- Emacs Lisp
- Syncthing
- Init File
- Keybindings
- Evil-numbers
- Winner
- Buffer move
- Info
- xref
- IBuffer
- Speedbar
- Trailing whitespace
- Whitespace Visualation
- Line Numbers
- Amx
- Olivetti Mode
- Winum
- Backups and Autosaves
- Smartparens/Parinfer
- jq
- Org Mode
- org-present
- link-hint
- org-cliplink
- org-board
- org-rifle
- Org Noter
- Org Roam
- org-journal
- org-super-agenda
- Projectile
- Mode line
- UI
- Centaur tabs
- Frame parameters
- Transpose Frame
- EShell
- Flycheck
- Tabs
- aggressive-indent-mode
- JSON
- JavaScript
- Java
- Kotlin
- Groovy
- Typescript
- LSP Mode
- Python
- Hy
- Go
- Clojure
- Scheme
- Common Lisp
- Haskell
- PHP
- YAML
- Pharen
- Bash
- Ruby
- Rust
- XML
- CSVs
- Markdown
- IELM
- Ledger Mode
- PDFs
- EPubs
- Dashboard
- w3m
- Wakatime
- Elfeed
- Undo Tree
- Emojify
- Calc
- Deadgrep
- RCIRC
- dumb-jump
- Dictionary
- Gnus
- Dired
- Crontab
- Emacs Server
- YASnippet
- mpc
- wgrep
- Ivy
- graphviz
- HideShow
- Slack
- Matrix
- EMMS
- Direnv
- SQL
- GraphQL
- Docker
- Kubernetes
- AWS
- Prodigy
- Lola
- StumpWM
- Emacs Network Client
- Deft
- Pollen
- Ngrok
- Make
- Redis
- Restclient
- IMenu
- calfw-org
- hackernews
- counsel-spotify
- Flyspell
- VTerm
- Hide mode line
- Dash
- Alfred
- gist.el
- ffap
- Format-all-the-code
- Compiling
- Wallabag
- Clipmon
- GUD
- realgud
- EDiff
- Apache Drill
- Helpful
- Inform
- Vuiet
- Jira
- Structlog
-- eval: (add-hook 'after-save-hook 'org-babel-tangle 0 t) --
This is a literate init file holding my Emacs configuration. It is initially loaded by a bootstrap file that lives at ~/.emacs.d/init.el; after the initial bootstrapping it writes itself to ~/.emacs.el. Since ~/.emacs.el takes priority over ~/.emacs.d/init.el, after the initial bootstrapping process the tangled ~/.emacs.el file will get loaded without needing to load the bootstrap file first.
Prelude
Enables lexical binding for everything in init.el:
;;; -*- lexical-binding: t; -*-
Garbage collection
Some GC tweaks "borrowed" from Doom emacs.
Turn off GC during init and restore it afterwards:
(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)))
Also suppress GC for 1 second after the minibuffer is active to avoid stuttering autocompletion and other GC hangups:
(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)
Unset file-handler-alist during initialization
Another optimization from Doom Emacs.
(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)))
Variables
(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)
Default directory
(cd "~")
Packages
Load straight.el to manage package installation:
(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)))
`use-package` is a macro that simplifies installing and loading packages.
(straight-use-package 'use-package)
(setq straight-use-package-by-default t)
Utility functions
(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)))))
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.
(use-package benchmark-init
:config
(add-hook 'after-init-hook 'benchmark-init/deactivate))
Load org mode
Load org-mode early to avoid a version clash.
(use-package org
:straight org-plus-contrib
:commands (org-element-map)
:mode (("\\.org\\'" . org-mode)))
;; Annoying that this is necessary...
(require 'org)
(require 'org-refile)
(require 'org-protocol)
Doom themes
(use-package doom-themes)
(use-package doom-modeline)
(doom-modeline-mode 1)
Ewal theme
(use-package doom-ewal-themes
:straight (doom-ewal-themes
:host github
:repo "jdormit/doom-ewal-themes"
:files ("themes" :defaults)))
Customization File
I don't want anything to write to my init.el, so save customizations in a separate file:
(setq custom-file (expand-file-name "custom.el" user-emacs-directory))
(load custom-file t)
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.
(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")
exec-path-from-shell-check-startup-files nil)
(exec-path-from-shell-initialize))
General
Better keybinding.
(use-package general
:init
(setq general-override-states '(insert
emacs
hybrid
normal
visual
motion
operator
replace)))
Autocompletion
There seems to be some contention about whether autocomplete or company are better autocomplete packages. I'm going with company for now because the maintainer seems nicer…
(use-package company
:config
(setq company-idle-delay 0.3)
(add-hook 'after-init-hook #'global-company-mode))
(general-def "C-M-i" #'company-complete)
(general-def "M-<tab>" #'company-complete)
Which-key
`which-key` makes keybindings discoverable.
(use-package which-key
:config
(which-key-mode))
This function defines a prefix group for `which-key` so that it doesn't display `prefix`.
(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))
Evil Mode
Because I like modal editing and dislike RSI.
(use-package evil
:init
(setq evil-want-keybinding nil)
:config
(evil-mode 1))
Make undo not undo paragraphs at a time:
(setq evil-want-fine-undo t)
Add a convenience function for making buffer-local ex-commands:
(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))
evil-collection
A collection of evil bindings for various modes
(use-package evil-collection
:after (evil)
:config
(setq evil-collection-company-use-tng nil)
(evil-collection-init))
leader key
Use the spacebar as a leader key in evil-mode's normal state and in various other modes:
(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)
(jdormit/define-prefix "?" "help")
(leader-def-key "?" help-map)
evil-snipe
(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)))
evil-commentary
Adds Evil commands to comment out lines of code:
(use-package evil-commentary
:after (evil)
:hook ((prog-mode . evil-commentary-mode)))
Additional keybindings
(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)
Multiple cursors
(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))
(use-package evil-multiedit
:defer 0
:config
(evil-multiedit-default-keybinds))
Transient
A framework for creating Magit-style popups:
(use-package transient
:commands (define-transient-command))
Hydra
Hydras are convenient keybinding menus.
(use-package hydra
:defer t)
Magit
Magit is objectively the best Git interface.
(use-package magit
:commands (magit-status
magit-blame
magit-find-file
magit-name-local-branch))
(jdormit/define-prefix "g" "git")
(leader-def-key "gs" #'magit-status)
(leader-def-key "gg" #'magit-file-dispatch)
(leader-def-key "gf" #'magit-find-file)
Use ido-mode for completion within Magit:
;; (setq magit-completing-read-function 'magit-ido-completing-read)
Forge
Forge is an extension for Magit that lets it interact with code forges (e.g. GitHub).
(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")))))
evil-magit
Evil keybindings for magit!
(use-package evil-magit
:after (evil magit)
:config
(with-eval-after-load 'magit
(require 'evil-magit))
:general
('normal magit-mode-map "SPC" leader-map))
Transient
(setq transient-default-level 7)
git-link
Open files in Git forges (GitHub, GitLab, etc.):
(use-package 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))))
with-editor
A utility from the author of Magit to run shell commands using the current Emacs instance as $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)
Password Store
Interfacing with Pass, the "standard Unix password manager". This should also be loaded before `exec-path-from-shell`.
(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 motion visual) pass-mode-map "S" #'password-store-synchronize))
(leader-def-key "ap" #'pass)
(setq auth-sources '("~/.authinfo" password-store))
Emacs Lisp
Packages
Some helpful ELisp packages:
- deferred is an async API library
- s.el is a string manipulation library
- dash.el is a list manipulation library
- f.el is a file manipulation library
- request is an HTTP library
(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))))
(use-package request
:commands (request request-deferred))
Editing Elisp
(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)
Load path
For machine or user specific libraries:
(add-to-list 'load-path (expand-file-name "~/site-lisp"))
And for global ones:
(add-to-list 'load-path "/usr/local/share/emacs/site-lisp")
Utilities
Reading a file as a string:
(defun read-file (path)
"Returns the contents of the file as a string"
(with-temp-buffer
(insert-file-contents path)
(buffer-string)))
Opening a file as sudo:
(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)))
Recursive assoc
for nested alists:
(defun assoc-recursive (alist &rest keys)
"Recursively find KEYs in ALIST."
(while keys
(setq alist (cdr (assoc (pop keys) alist))))
alist)
Format a millis timestamp into human-readable form:
(defun format-epoch-millis (millis)
(interactive "nTimestamp: ")
(message (format-time-string "%F %r" (/ millis 1000))))
The same but for seconds:
(defun format-epoch-seconds (seconds)
(interactive "nTimestamp: ")
(message (format-time-string "%F %r" seconds)))
Checking if a buffer contains a string:
(defun buffer-contains-substring (string)
(save-excursion
(save-match-data
(goto-char (point-min))
(search-forward string nil t))))
Pretty-print JSON:
(defun pprint-json (raw-json)
(with-temp-buffer
(insert raw-json)
(json-pretty-print (point-min) (point-max))
(buffer-substring (point-min) (point-max))))
Load environment variables into Emacs from a shell script:
(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))
Convenience macro to run some code in a particular default-directory:
(defmacro with-default-directory (dir &rest body)
(declare (indent 1))
`(let ((default-directory ,dir))
,@body))
(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))))))))
A handy function to colorize a buffer with ANSI escape characters in it:
(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)))
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).
(defvar persisted-vars-file "~/.emacs.d/persisted-vars")
This function retrieves the plist of persisted variables or nil if it doesn't exist:
(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)))))))
This function retrieves a persisted variable:
(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))))
And this function persists a variable:
(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)))))
Process handling
Some utilities for calling out to other processes.
(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)))))
A function to call a process passing some string as stdin and returning the process output:
(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)))))
The same function but for commands that need to run in a shell:
(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))))))
Running a shell command as sudo:
(defun sudo-shell-command (command)
(with-temp-buffer
(cd "/sudo::/")
(shell-command command)))
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):
(defvar buffer-mode-hooks '())
To add a new hook, push the code to run onto the correct list:
(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))))
Whenever the buffer changes, look up the major-mode to see if there is any code to run:
(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)
Aliases
(defalias 'doc 'describe-symbol)
Miscellaneous
(setq warning-suppress-types
'((undo discard-info)))
Syncthing
I put lots of stuff in Dropbox, but the actual folder location differs on my different computers. This function resolves to the Dropbox directory:
(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 ""))))
Load up libraries from Dropbox, if there are any:
(add-to-list 'load-path (syncthing-directory "site-lisp"))
Init File
A function to reload my init file. It reloads the major mode after the init file is loaded to rebind keymappings.
(defun reload-init-file ()
(interactive)
(load-file "~/.emacs.el")
(funcall major-mode))
And another one to edit it:
(defun find-init-file ()
(interactive)
(find-file "~/init.org"))
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:
(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))
Visual line navigation
(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)
M-x
(leader-def-key "SPC" 'execute-extended-command)
Eval-ing
(leader-def-key ":" #'eval-expression)
Init file commands
(jdormit/define-prefix "." "dotfile")
(leader-def-key ".r" 'reload-init-file)
(leader-def-key ".f" 'find-init-file)
Commands about files
(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)
Window commands
(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)
Buffer commands
A function to switch to previous buffer from this blog post:
(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)
A function to kill all buffers except the current one from EmacsWiki:
(defun kill-other-buffers ()
"Kill all other buffers."
(interactive)
(mapc 'kill-buffer (delq (current-buffer) (buffer-list))))
(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)
Frame commands
(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)
Running shell commands
(leader-def-key "!" 'shell-command)
(leader-def-key "|" 'shell-command-on-region)
Toggles
Like in Spacemacs, put all toggle commands behind a prefix:
(jdormit/define-prefix "t" "toggle")
Toggles about line truncation:
(leader-def-key "tt" 'toggle-truncate-lines)
(leader-def-key "tT" 'visual-line-mode)
(leader-def-key "tf" 'auto-fill-mode)
Toggle lisp debugging:
(leader-def-key "td" 'toggle-debug-on-error)
Shells/REPLs
Emacs has a shell for every mood!
(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)
Applications
(jdormit/define-prefix "a" "applications")
Help Buffers
(general-def 'motion help-mode-map "TAB" #'forward-button)
Code commands
(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)
Process list
(general-def '(normal motion visual) process-menu-mode-map
"d" #'process-menu-delete-process)
Registers
(jdormit/define-prefix "r" "registers")
(leader-def-key "r" ctl-x-r-map)
Evil-numbers
Handy functions to increment/decrement numbers at point:
(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))
Winner
Winner is a minor mode that keeps an undo/redo history of the window configuration:
(winner-mode 1)
(leader-def-key "wn" #'winner-redo)
(leader-def-key "wp" #'winner-undo)
Buffer move
A handy package to shift buffers between open windows:
(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))
Info
(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))
xref
After I select an xref reference, I want the xref buffer closed:
(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)
Don't prompt for an identifier for these xref commands:
(setq xref-prompt-for-identifier
'(not xref-find-definitions
xref-find-definitions-other-window
xref-find-definitions-other-frame
xref-find-references))
Some keybindings:
(general-def "M-r" #'xref-find-references)
IBuffer
(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))
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:
(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)
Trailing whitespace
ws-butler deletes trailing whitespace on lines that you touch, but leaves other lines alone:
(use-package ws-butler
:straight (ws-butler :host github :repo "lewang/ws-butler")
:hook ((prod-mode . ws-butler-mode)))
Whitespace Visualation
(setq whitespace-line-column 80
whitespace-style '(face lines-tail))
(leader-def-key "tw" #'whitespace-mode)
Line Numbers
Toggle line numbers:
(setq display-line-numbers-type 'visual)
(leader-def-key "tn" 'display-line-numbers-mode)
Toggle line numbering mode (normal or relative):
(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)
Display line numbers by default in code and org-mode buffers:
(add-hook 'prog-mode-hook #'display-line-numbers-mode)
(add-hook 'org-mode-hook #'display-line-numbers-mode)
Amx
A better M-x.
(use-package amx
:config
(amx-mode))
Olivetti Mode
Olivetti is a minor mode for a nice writing environment.
(use-package olivetti
:config
(setq-default olivetti-body-width 100)
(setq olivetti-body-width 100)
:commands olivetti-mode)
(leader-def-key "to" 'olivetti-mode)
Winum
This package includes functions to switch windows by number.
(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))
I don't want which-key display "lambda" for the descriptions of these, so set a custom display function. This is stolen from Spacemacs.
(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)
Backups and Autosaves
Store backups and autosaves in a centralized place. This should really be the default…
(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")))
Smartparens/Parinfer
Smartparens enables structured editing of s-expressions and other pairs:
(use-package smartparens
:hook ((prog-mode . smartparens-strict-mode)
(eshell-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 "\'"))
: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)
Enable ES6 arrow functions in web-mode ("borrowed" from this GitHub comment):
(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 (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))
Parinfer infers parens from indentation and vice-versa. Currently disabled since it turned out to be more annoying than good…
(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)))
jq
The JSON multitool.
(use-package jq-mode
:commands (jq-mode jq-interactively))
Org Mode
Notes, agenda, calendar, blogging, journaling, etc.
A function to get my org directory:
(defun org-directory (&optional path)
"Returns the directory for my org notes, appends PATH if given"
(f-join (syncthing-directory "org") (s-chop-prefix (f-path-separator) (or path ""))))
(jdormit/define-prefix "o" "org")
(leader-def-key "oa" 'org-agenda)
(leader-def-key "oc" 'org-capture)
(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)
(add-hook 'org-mode-hook #'auto-fill-mode)
Use RET to follow links:
(setq org-return-follows-link t)
Always show inline images:
(add-hook 'org-mode-hook
(lambda ()
(org-display-inline-images nil t)
(org-redisplay-inline-images)))
Include a timestamp when TODO entries are closed:
(setq org-log-done 'time)
Tell Emacs how to open file links of various types:
(with-eval-after-load 'org
(add-to-list 'org-file-apps '(directory . emacs))
(add-to-list 'org-file-apps '("log" . emacs)))
A function to add ids to every heading in an Org file:
(defun org-get-create-ids-all ()
(interactive)
(save-excursion
(goto-char (point-max))
(while (outline-previous-heading)
(org-id-get-create))))
Agenda files
(defun agenda-files (&optional file)
(let ((agenda-dir (org-directory)))
(if file
(concat (file-name-as-directory agenda-dir) file)
agenda-dir)))
(setq org-agenda-files `(,(agenda-files)))
Capture templates
(setq 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>%?")))
Refile targets
(setq 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)))
Todo keywords
(setq org-todo-keywords
'((sequence "TODO(t)" "IN PROGRESS(i)" "BLOCKED(b)" "|" "DONE(d)" "CANCELLED(c)")))
Agenda views
(setq 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)))))
Keybindings
(general-def 'normal org-mode-map "T" #'org-insert-todo-heading)
(general-def 'normal org-mode-map "K" #'org-move-subtree-up)
(general-def 'normal org-mode-map "J" #'org-move-subtree-down)
(general-def 'normal org-mode-map "<return>" #'org-return)
(general-def 'normal org-mode-map "TAB" #'org-cycle)
(general-def 'normal org-mode-map "SPC" leader-map)
(general-def '(normal motion visual) org-mode-map "gn" #'org-next-link)
(general-def '(normal motion visual) org-mode-map "gp" #'org-previous-link)
(general-def org-mode-map "C-c e" #'org-preview-latex-fragment)
(general-def "C-c l" #'org-store-link)
Set up evil keybindings:
(use-package evil-org
:after (evil org)
:hook (org-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))
And a global keybinding to open an org file:
(defun find-org-file (file)
(interactive
(list (completing-read
"Find org file: "
(directory-files (agenda-files) t))))
(find-file file))
(leader-def-key "fo" #'find-org-file)
Finally, add a helpful agenda-mode hydra:
(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))
Exporting
HTML
Export to HTML:
(use-package htmlize
:commands htmlize-buffer)
Don't put section numbers in front of headers:
(setq org-export-with-section-numbers nil)
Disable the preamble and postamble:
(setq org-html-preamble nil
org-html-postamble nil)
Redefine org-html-src-block to wrap code blocks in <pre><code> and language class for use by highlight.js:
(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)))))))
Github-flavored markdown
(use-package ox-gfm
:defer t
:init
(with-eval-after-load 'org
(require 'ox-gfm)))
Jira
(use-package ox-jira
:defer t
:straight (:host github :repo "stig/ox-jira.el")
:init
(with-eval-after-load 'org
(require 'ox-jira)))
org-babel
Literate programming!
(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)))))
Get rid of the confirmation prompt:
(setq org-confirm-babel-evaluate nil)
Display inline images after executing a source block:
(add-hook 'org-babel-after-execute-hook
(lambda ()
(org-display-inline-images nil t)
(org-redisplay-inline-images)))
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.
(use-package ob-async
:straight (ob-async :host github :repo "astahlman/ob-async")
:defer t
:hook (org-mode . (lambda () (require 'ob-async))))
Filter out the "u" from unicode results in org tabels returned from Python source blocks:
(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))))
Images
(setq org-image-actual-width nil)
org-scratch
It's very useful to open a new org buffer for a quick org-babel exploration.
(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)
org-gcal
Integrate Google calendar with org-mode:
(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
:hook ((emacs-startup . org-gcal-fetch))
: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))
Utilities
A function to get the TITLE property of the current org buffer:
(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))))))))
org-present
An ultra-minimalist presentation mode for Org:
(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))
link-hint
A very helpful package that provides jump-to-link functionality:
(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))
org-cliplink
Intelligently inserts an org-mode link from the clipboard.
(use-package org-cliplink
:commands (org-cliplink
org-cliplink-clipboard-content)
:general
(org-mode-map "C-c C-S-l" #'org-cliplink))
org-board
Org-board is a bookmarking/archiving system built on Org mode:
(use-package org-board
:defer t
:after org
:init
;; 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))
(add-to-list 'org-protocol-protocol-alist
'("Bookmark"
:protocol "bookmark"
:function bookmark-via-org-protocol
:kill-client t))
:config
(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))
org-rifle
Quickly find stuff in Org files:
(use-package helm-org-rifle
:defer t
: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"))
Org Noter
Org Noter lets me take org-mode notes on PDFs, epubs, and DocView files:
(use-package org-noter
:commands org-noter
:general
((normal visual motion) org-noter-doc-mode-map "i" #'org-noter-insert-note)
((normal visual motion) org-noter-doc-mode-map "q" #'org-noter-kill-session)
((normal visual motion) org-noter-doc-mode-map "C-M-n" #'org-noter-sync-next-note)
((normal visual motion) org-noter-doc-mode-map "C-M-p" #'org-noter-sync-prev-note)
((normal visual motion) org-noter-doc-mode-map "M-." #'org-noter-sync-current-page-or-chapter)
((normal visual motion) org-noter-doc-mode-map "M-i" #'org-noter-insert-precise-note)
((normal visual motion) org-noter-doc-mode-map "M-n" #'org-noter-sync-next-page-or-chapter)
((normal visual motion) org-noter-doc-mode-map "M-p" #'org-noter-sync-prev-page-or-chapter)
((normal visual motion) org-noter-doc-mode-map "C-M-." #'org-noter-sync-current-note))
Org Roam
Org-roam is another backlink package for org-mode:
(use-package org-roam
:after org
: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)
:hook
(after-init . org-roam-mode)
:custom
(org-roam-directory (org-directory))
:init
(leader-def-key "fo" #'org-roam-find-file)
(leader-def-key "of" #'org-roam-find-file)
(defvar org-roam-map (make-sparse-keymap))
(leader-def-key "on" org-roam-map)
(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")
:config
(add-hook 'org-roam-backlinks-mode-hook #'olivetti-mode)
:general
(org-roam-map "l" #'org-roam)
(org-roam-map "t" #'org-roam-today)
(org-roam-map "f" #'org-roam-find-file)
(org-roam-map "i" #'org-roam-insert)
(org-roam-map "g" #'org-roam-show-graph)
((normal motion visual) org-mode-map "gr" org-roam-map)
((normal motion visual) org-roam-backlinks-mode-map "<return>" #'org-open-at-point)
((normal motion visual emacs) org-roam-backlinks-mode-map "q" #'quit-window)
("C-c n" org-roam-map))
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:
(use-package org-journal
:defer t
: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))
(add-to-list 'org-capture-templates
'("j" "Journal entry" entry (function org-journal-capture-func)
"* %(format-time-string org-journal-time-format)\n%?"))
(jdormit/define-prefix "oj" "org-journal")
(leader-def-key "ojn" #'org-journal-new-entry)
(leader-def-key "ojt" #'org-journal-today)
: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))
org-super-agenda
(use-package org-super-agenda
: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)))
Projectile
(use-package projectile
:commands (projectile-find-file
projectile-grep
projectile-switch-project
projectile-project-root)
:init
(defhydra hydra-projectile (:color teal :hint nil) "
PROJECTILE: %(projectile-project-root)
Find File Search/Tags Buffers Cache
------------------------------------------------------------------------------------------
_s-f_: file _a_: ag _i_: Ibuffer _c_: cache clear _ff_: file dwim
_g_: update gtags _b_: switch to buffer _x_: remove known project
_fd_: file curr dir _o_: multi-occur _s-k_: Kill all buffers _X_:
cleanup non-existing _r_: recent file ^^^^_z_: cache current _d_:
dir
"
("a" projectile-ag)
("b" projectile-switch-to-buffer)
("c" projectile-invalidate-cache)
("d" projectile-find-dir)
("s-f" projectile-find-file)
("ff" projectile-find-file-dwim)
("fd" projectile-find-file-in-directory)
("g" ggtags-update-tags)
("s-g" ggtags-update-tags)
("i" projectile-ibuffer)
("K" projectile-kill-buffers)
("s-k" projectile-kill-buffers)
("m" projectile-multi-occur)
("o" projectile-multi-occur)
("s-p" projectile-switch-project "switch project")
("p" projectile-switch-project)
("s" projectile-switch-project)
("r" projectile-recentf)
("x" projectile-remove-known-project)
("X" projectile-cleanup-known-projects)
("z" projectile-cache-current-file)
("`" hydra-projectile-other-window/body "other window")
("q" nil "cancel" :color blue))
:config
(projectile-mode)
(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))
Mode line
UI
Get rid of the janky buttons:
(tool-bar-mode -1)
And the menu bar:
(menu-bar-mode -1)
And the ugly scroll bars:
(set-scroll-bar-mode nil)
Use variable-pitch-mode
in text modes:
(add-hook 'text-mode-hook (lambda () (variable-pitch-mode)))
(add-hook 'w3m-mode-hook (lambda () (variable-pitch-mode)))
Always use buffer-face-mode
in code and text buffers:
(add-hook 'prog-mode-hook #'buffer-face-mode)
(add-hook 'text-mode-hook #'buffer-face-mode)
Display the column number in programming modes:
(add-hook 'prog-mode-hook #'column-number-mode)
Disable the bell:
(setq ring-bell-function 'ignore)
Render stuff differently based on whether or not we are graphical:
(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)
Try to make the background normal colored in the terminal:
(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)
UI-related keybindings:
(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)
Centaur tabs
Centaur tabs is a package that gives Emacs buffer tabs similar to those in Atom or VS Code:
(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 (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)))))
Frame parameters
Functions to change the frame size:
(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))
Keybindings:
(leader-def-key "Fw" 'jdormit/set-frame-width)
(leader-def-key "Fh" 'jdormit/set-frame-height)
(leader-def-key "Fs" 'jdormit/set-frame-size)
Transpose Frame
A handy utility that reverse the current frame window split (vertical to horizontal or vice-versa):
(use-package transpose-frame
:defer t
:init
(leader-def-key "wt" 'transpose-frame))
EShell
Easy keybinding to open EShell:
(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)
Make EShell's tab completion work like Bash's:
(setq eshell-cmpl-cycle-completions nil)
Add additional useful modules:
(with-eval-after-load 'esh-module
(add-to-list 'eshell-modules-list 'eshell-tramp))
(with-eval-after-load 'eshell
(require 'esh-module))
Prefer Lisp commands to external programs:
(setq eshell-prefer-lisp-functions t
eshell-prefer-lisp-variables t)
Enable password caching:
(setq password-cache t
password-cache-expiry 120)
Destroy shell buffers created by eshell when the process dies::
(setq eshell-destroy-buffer-when-process-dies t)
Visual programs:
(defun eshell-setup ()
(add-to-list 'eshell-visual-commands "crawl")
(add-to-list 'eshell-visual-commands "ssh")
(add-to-list 'eshell-visual-subcommands '("kubectl" "exec"))
(add-to-list 'eshell-visual-subcommands '("k" "exec")))
(add-hook 'eshell-mode-hook #'eshell-setup)
And a function to run any program visually:
(defun eshell/v (&rest args)
(apply #'eshell-exec-visual args))
Load .dir-locals.el when switching directories:
(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)
A function to properly clear the eshell:
(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)))
Some aliases:
(setq eshell-aliases
'(("k" . "kubectl $*")
("kctx" . "kubectx $*")
("root" . "cd (projectile-project-root)")))
(add-hook
'eshell-mode-hook
(lambda ()
(dolist (alias eshell-aliases)
(eshell/alias (car alias) (cdr alias)))))
Prompt
(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))
(if (= (user-uid) 0) " # " " $ ")))))
(setq jdormit-eshell-prompt-regex "^[^#$\n]* [#$] ")
(setq eshell-prompt-function 'jdormit-eshell-prompt)
(setq eshell-prompt-regexp jdormit-eshell-prompt-regex)
Flycheck
Syntax checking etc.:
(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))
Tabs
(defun disable-tab-insert ()
(interactive)
(setq indent-tabs-mode nil))
aggressive-indent-mode
Like electric-indent-mode but reindents after every change:
(use-package aggressive-indent
:hook ((clojure-mode . aggressive-indent-mode)
(emacs-lisp-mode . aggressive-indent-mode)))
JSON
(use-package json-mode
:mode (("\\.json\\'" . json-mode)))
(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)
JavaScript
Some formatting stuff:
(setq js-indent-level 4)
(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))
Use nvm to manage node versions:
(use-package nvm
:straight ((nvm :host github :repo "rejeep/nvm.el"))
:commands (nvm-use nvm-use-for nvm-use-for-buffer))
A command to format JS via prettier:
(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)))
NVM
Manage node version via NVM within Emacs:
(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)))
Java
(use-package lsp-java
:hook ((java-mode . lsp-deferred)))
Kotlin
(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))))
Groovy
Used for Jenkins configuration scripts and probably other things.
(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))
Typescript
(use-package typescript-mode
:mode ("\\.ts\\'")
:config
(with-eval-after-load 'lsp
(add-hook 'typescript-mode-hook 'lsp-deferred)))
LSP Mode
Emacs support for the Language Server Protocol
(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))
(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))))))
Python
General
(leader-def-key "sp" #'run-python)
(add-hook 'python-mode-hook #'disable-tab-insert)
ISort is a Python utility to sort imports:
(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))
Run black on the current buffer:
(general-def 'normal python-mode-map "C-M-\\" #'format-all-buffer)
sphinx-doc.el automatically generates doc strings for Python functions (and updates existing ones!):
(use-package sphinx-doc
:hook ((python-mode . sphinx-doc-mode)))
Dev environment/IDE stuff
Support pyvenv within Emacs:
(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))
And support pyenv (NOT pyvenv) to change Python versions:
(use-package pyenv-mode
:defer t)
Use the LSP python client:
(use-package lsp-python-ms
:init
(setq lsp-python-ms-auto-install-server t)
(defun python-lsp ()
(require 'lsp-python-ms)
(lsp-deferred))
:hook
(python-mode . python-lsp)
:general
(python-mode-map "C-c C-d" #'lsp-describe-thing-at-point))
(general-def 'normal python-mode-map "C-c C-d" #'lsp-describe-thing-at-point)
Override the flycheck python-mypy checker to run in the right directory:
(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)))
Autoflake
Autoflake is a tool that removes unused imports and variables from Python code:
(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)))))
Testing
python-pytest.el integrates Pytest with Emacs:
(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))
And borrowing some functions from python-pytest, we can get some nosetests support as well:
(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")
Hy
Python but Lispy!
(defun run-hy ()
(interactive)
(run-lisp (expand-file-name "~/.virtualenvs/hy/bin/hy")))
Go
Basic support:
(use-package go-mode
:mode (("\\.go\\'" . go-mode)))
LSP support - requires go-langserver.
(add-hook 'go-mode-hook #'lsp-deferred)
Clojure
Start with clojure-mode:
(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)))
Add flycheck support:
(use-package flycheck-clj-kondo
:after (clojure-mode)
:if (executable-find "clj-kondo")
:ensure t)
Sprinkle in some CIDER:
(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))
(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)
Add some handy hydras:
(use-package cider-hydra
:after (cider)
:hook ((cider-mode . cider-hydra-mode)))
Clj-refactor adds magical refactoring abilities:
(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)))
Add support for running Org-mode Clojure source blocks with Babashka:
(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."
(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)))
Scheme
Tell emacs about file extensions which should activate scheme-mode:
(add-to-list 'auto-mode-alist '("\\.guile\\'" . scheme-mode))
(add-to-list 'auto-mode-alist '("\\.rkt\\'" . scheme-mode))
Geiser is a Scheme IDE for Emacs that supports a bunch of common Scheme implementations.
(use-package geiser
:commands (run-geiser)
:general
(geiser-debug-mode-map "SPC" leader-map))
And a handy shortcut to hop into a Geiser REPL:
(leader-def-key "sg" 'run-geiser)
Common Lisp
SLIME is a set of modes and utilities for writing Common Lisp in Emacs. Quicklisp is the de-facto Common Lisp package manager. It comes with some Emacs bindings.
(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)
:general
((normal motion visual insert emacs) slime-mode-map "M-." #'slime-edit-definition))
(add-to-list 'auto-mode-alist '("\\.cl\\'" . lisp-mode))
Keyboard shortcut to start a SLIME REPL:
(leader-def-key "sc" 'slime)
Haskell
(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)
PHP
(use-package php-mode
:mode "\\.php\\'")
(use-package mmm-mode
:after php-mode)
Geben is an interface to XDebug allowing debugging PHP in Emacs:
(use-package geben
:commands (geben))
Some keybindings to start and end Geben:
(general-def php-mode-map "C-c C-d" #'geben)
(general-def php-mode-map "C-c C-q" #'geben-end)
An Eshell alias to start PHP using XDebug:
(add-hook 'eshell-mode-hook
(lambda ()
(eshell/alias "php-debug"
"php -d xdebug.remote_enable=on -d xdebug.remote_host=127.0.0.1 -d xdebug.remote_port=9000 -d xdebug.remote_handler=dbgp -d xdebug.idekey=geben -d xdebug.remote_autostart=On $*")))
LSP for PHP requires php-language-server to be installed in ~/.composer:
(add-hook 'php-mode-hook #'lsp-deferred)
YAML
(use-package yaml-mode
:mode ("//.yml//'"))
Pharen
Pharen is a Lisp that compiles to PHP. It looks a lot like Clojure.
(add-to-list 'auto-mode-alist '("\\.phn\\'" . clojure-mode))
Bash
Use LSP if bash-language-server is installed.
(when (executable-find "bash-language-server")
(add-hook 'sh-mode-hook #'lsp-deferred))
Ruby
(add-hook 'ruby-mode-hook #'lsp-deferred)
Rust
(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))
XML
Set up hideshow for nXML mode:
(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))
A function to format XML using tidy or xmllint if available, falling back to sgml-pretty-print:
(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)
CSVs
(use-package csv-mode
:mode "\\.csv\\'")
Markdown
(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"))
Edit-indirect allows markdown-mode to edit source blocks in separate buffers:
(use-package edit-indirect
:defer t)
IELM
Enable lexical scoping in IELM:
(add-hook 'ielm-mode-hook
#'(lambda ()
(interactive)
(setq lexical-binding t)))
Ledger Mode
This mode requires that ledger be installed on the system.
(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))))
Importing
(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")))
PDFs
(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 motion visual) pdf-view-mode-map "." #'hydra-pdftools/body)
((normal motion visual) pdf-view-mode-map "F" #'pdf-links-action-perform))
EPubs
(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))
Org mode links
First, keep a reference to filename of the .epub before nov.el blows it away:
(defvar nov-epub-file)
(add-hook 'nov-mode-hook
#'(lambda ()
(message "epub file: " buffer-file-name)
(setq nov-epub-file buffer-file-name)))
That reference lets us construct a link back to the .epub:
(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)
Dashboard
Instead of the GNU Emacs buffer on startup, display a cool dashboard:
(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)
For some reason Emacs is starting up with the dashboard and the scratch buffer open in a split configuration. Not sure why, but let's put a stop to that…
(add-hook
'after-init-hook
(lambda ()
(switch-to-buffer "*dashboard*")
(delete-other-windows)))
I use mu/mu4e as my email client.
First, add it to the load path:
(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)
Then set up autoloads on the entry functions:
(autoload 'mu4e (concat mu4e-load-path "/mu4e.el") nil t)
(autoload 'mu4e-update-index (concat mu4e-load-path "/mu4e.el") nil t)
Then configure it:
(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))))
Support sending attachments from Dired:
(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))
Support sending rich-text emails via Markdown:
(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)
Global keybindings:
(leader-def-key "am" #'mu4e)
Keybindings within mu4e:
(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))
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:
(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))))))
w3m
Browsing the web from Emacs. Relies on having w3m installed.
(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-line nil
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)
I mostly want `browse-url-at-point` to open stuff in Firefox, but in some cases I want it within Emacs:
(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)
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:
(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))))))
And here are the websites where I want custom referers and/or no cookies:
(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"))
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
:
(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)
Add a w3m filter to handle the code block delimiters:
;; (with-eval-after-load 'w3m-filter
;; (add-to-list 'w3m-filter-configuration
;; '(t "Render code blocks with a different face" ".*" w3m-filter-code-blocks)))
Wakatime
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.
(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))))))
Elfeed
Elfeed is a feed reader for Emacs.
(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")
(general-def 'normal elfeed-search-mode-map "q" 'elfeed-search-quit-window)
(general-def 'normal elfeed-search-mode-map "C-r" 'elfeed-search-update--force)
(general-def 'normal elfeed-search-mode-map "R" 'elfeed-search-fetch)
(general-def 'normal elfeed-search-mode-map "RET" 'elfeed-search-show-entry)
(general-def 'normal elfeed-search-mode-map "s" 'elfeed-search-live-filter)
(general-def 'normal elfeed-search-mode-map "S" 'elfeed-search-set-filter)
(general-def 'normal elfeed-search-mode-map "B" 'elfeed-search-browse-url)
(general-def 'normal elfeed-search-mode-map "y" 'elfeed-search-yank)
(general-def 'normal elfeed-search-mode-map "u" 'elfeed-search-tag-all-unread)
(general-def 'normal elfeed-search-mode-map "r" 'elfeed-search-untag-all-unread)
(general-def 'normal elfeed-search-mode-map "n" 'next-line)
(general-def 'normal elfeed-search-mode-map "p" 'previous-line)
(general-def 'normal elfeed-search-mode-map "+" 'elfeed-search-tag-all)
(general-def 'normal elfeed-search-mode-map "-" 'elfeed-search-untag-all)
(general-def 'normal elfeed-show-mode-map "d" 'elfeed-show-save-enclosure)
(general-def 'normal elfeed-show-mode-map "q" 'elfeed-kill-buffer)
(general-def 'normal elfeed-show-mode-map "r" 'elfeed-show-refresh)
(general-def 'normal elfeed-show-mode-map "n" 'elfeed-show-next)
(general-def 'normal elfeed-show-mode-map "p" 'elfeed-show-prev)
(general-def 'normal elfeed-show-mode-map "s" 'elfeed-show-new-live-search)
(general-def 'normal elfeed-show-mode-map "B" 'elfeed-show-visit)
(general-def 'normal elfeed-show-mode-map "y" 'elfeed-show-yank)
(general-def 'normal elfeed-show-mode-map "u" (elfeed-expose #'elfeed-show-tag 'unread))
(general-def 'normal elfeed-show-mode-map "+" 'elfeed-show-tag)
(general-def 'normal elfeed-show-mode-map "-" 'elfeed-show-untag)
(general-def 'normal elfeed-show-mode-map "SPC" 'scroll-up-command)
(general-def 'normal elfeed-show-mode-map "DEL" 'scroll-down-command)
(general-def 'normal elfeed-show-mode-map "\t" 'shr-next-link)
(general-def 'normal elfeed-show-mode-map [tab] 'shr-next-link)
(general-def 'normal elfeed-show-mode-map "\e\t" 'shr-previous-link)
(general-def 'normal elfeed-show-mode-map [backtab] 'shr-previous-link)
(general-def 'normal elfeed-show-mode-map [mouse-2] 'shr-browse-url)
(general-def 'normal elfeed-show-mode-map "A" 'elfeed-show-add-enclosure-to-playlist)
(general-def 'normal elfeed-show-mode-map "P" 'elfeed-show-play-enclosure))
(use-package elfeed
:commands elfeed
:config
(add-hook 'elfeed-show-mode-hook 'elfeed-setup-hook)
(setq-default elfeed-search-filter "@6-months-ago +unread")
(add-to-list 'evil-normal-state-modes 'elfeed-search-mode)
(add-to-list 'evil-normal-state-modes 'elfeed-show-mode)
(setq elfeed-feeds
'(;; ("http://www.wsj.com/xml/rss/3_7085.xml" news)
;; ("https://www.wsj.com/xml/rss/3_7014.xml" news)
;; ("http://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml" news)
;; ("https://www.newyorker.com/feed/everything" news)
;; ("https://www.economist.com/sections/business-finance/rss.xml" news)
;; ("https://www.economist.com/sections/economics/rss.xml" news)
("https://taibbi.substack.com/feed" news)
;; NYTimes - The Morning newsletter
("https://www.kill-the-newsletter.com/feeds/lb5od7xsvr3nwmg9qp2a.xml" news)
("https://metaredux.com/feed.xml" clojure)
("https://lambdaisland.com/feeds/blog.atom" clojure)
("https://emacsredux.com/atom.xml" emacs)
("https://sachachua.com/blog/category/emacs-news/feed" emacs)
("https://mxb.dev/feed.xml" web)
"https://feed.tedium.co/"
"https://joy.recurse.com/feed.atom"
;; Changelog Weekly
"https://www.kill-the-newsletter.com/feeds/cfiasax3ct12r7b9svjq.xml"
"https://wiki.xxiivv.com/links/rss.xml"
("https://jeremydormitzer.com/blog/feed.xml" my-website))))
Keybinding for opening Elfeed:
(leader-def-key "al" 'elfeed)
Undo Tree
(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))
Emojify
Because emojis make everything better.
(use-package emojify
:commands (emojify-mode
emojify-apropos-emoji
emojify-insert-emoji)
:hook
((emacs-startup . emojify-mode))
:init
(leader-def-key "te" 'emojify-mode))
Calc
(leader-def-key "ac" 'calc)
Deadgrep
A nice Emacs UI over ripgrep.
(use-package deadgrep
:commands deadgrep
:general
(deadgrep-mode-map "SPC" leader-map))
(leader-def-key "fg" 'deadgrep)
RCIRC
IRC in Emacs, just in case anyone actually still uses it…
Channels:
(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")))))
Key bindings:
(leader-def-key "ai" 'irc)
Use evil keybindings by default:
(add-to-list 'evil-normal-state-modes 'rcirc-mode)
dumb-jump
Dumb-jump uses ripgrep and some algorithms based on the major-mode to implement jump-to-definition.
(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)
Dictionary
This package looks up word definitions online.
(use-package define-word
:commands (define-word define-word-at-point)
:general
(normal "gl" #'define-word-at-point)
(normal "gL" #'define-word))
Gnus
An ancient newsreader.
(leader-def-key "ag" 'gnus)
(general-def gnus-mode-map "SPC" leader-map)
It can read email:
(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)
Or Gnus can read RSS feeds directly:
;; (add-to-list 'gnus-secondary-select-methods
;; '(nnrss "http://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml"))
Dired
Keybindings
(general-def 'normal dired-mode-map "f" #'dired-create-empty-file)
Dired subtree
(use-package dired-subtree
:general
(normal dired-mode-map "TAB" 'dired-subtree-toggle)
:config
(advice-add 'dired-subtree-toggle :after (lambda () (revert-buffer))))
Dired narrow
(use-package dired-narrow
:general
(normal dired-mode-map "/" #'dired-narrow))
Dired sidebar
(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))
All-the-icons
(use-package all-the-icons)
(use-package all-the-icons-dired
:hook ((dired-mode . (lambda ()
(unless (eq major-mode 'dired-sidebar-mode)
(all-the-icons-dired-mode))))))
Variables
(setq dired-dwim-target t)
Dired hydra
(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))
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:
(defun edit-crontab ()
(interactive)
(with-editor-async-shell-command "crontab -e"))
Emacs Server
In case I need an emacsclient
for some reason.
(add-hook 'after-init-hook #'server-start)
YASnippet
YASnippet is Yet Another Snippet template system.
(use-package yasnippet
: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")))
(yas-global-mode))
(use-package yasnippet-snippets
:after (yasnippet)
:config (yasnippet-snippets-initialize))
mpc
An Emacs interface to MPD, the Music Player Daemon
(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)
wgrep
(use-package wgrep
:defer t)
Ivy
An alternative minibuffer completion framework:
(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" #'counsel-ibuffer)
(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))
(leader-def-key "cs" #'lsp-ivy-workspace-symbol)
(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))
graphviz
(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))))
Functions
A function that converts a lisp form into Graphviz format, e.g.:
'(a ((label . "Node A")) (b ((label . "Node B")) (d ((label . "Node D")))) (c ((label . "Node C")) (e ((label . "Node E")))))
becomes:
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; }
(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 "))))
HideShow
hs-minor-mode enables comment and code-folding. It's useful almost everywhere, so just enable it:
(add-hook 'prog-mode-hook (lambda () (hs-minor-mode 1)))
Slack
(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
;; :client-id "2400384563.846406584294"
;; :client-secret "31d7bb557aebc773ef26fb53b0f3caf5"
:token "xoxs-2400384563-531653313251-753670261511-dd9fd44b4755430caf78045af40361d9a2bf46d8a5392f09ea1b6f3c6db0b545"
;; :token "xoxp-2400384563-531653313251-844227895840-6bd114a9d3d8642116f0a1d50675fa43"
: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))
(use-package alert
:commands (alert)
:init
(setq alert-default-style (if (eq system-type 'darwin)
'osx-notifier
'libnotify)))
Matrix
(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"))))
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:
git clone git://git.sv.gnu.org/emms.git
cd emms
make emms-print-metadata
make
make install
(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))))
Direnv
Direnv automatically runs bash scripts when you enter certain directories. This sets it up to work with Emacs:
(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)))))
SQL
Emacs has excellent built-in SQL support.
(leader-def-key "sP" #'sql-postgres)
GraphQL
GraphQL mode for editing GraphQL queries:
(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))
GraphQL environments:
(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")))))
And ob-graphql for evaluating GraphQL source blocks in org-mode:
(use-package ob-graphql
:defer t)
Docker
Syntax highlighting for Dockerfiles:
(use-package dockerfile-mode
:mode ("\\Dockerfile\\'"))
Kubernetes
(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))
AWS
S3
(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))
Prodigy
Prodigy gives Emacs a nice way to run services (web servers, etc.).
(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)
Add the ability to associate a file with a service instead of a buffer:
(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))
And add the ability to inhibit all service output processing:
(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)))))
Lola
Some functions to make my day job easier.
Services (Prodigy)
(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 "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 "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 "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 "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 "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"
:tags '(lola frontend)
: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 "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"
:tags '(lola backend)
: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"))
Services (eShell)
(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)
Python stuff
Run pip install in the current project:
(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))))
AWS-MFA
The aws-mfa command:
(defun aws-mfa (mfa-token)
(interactive "MMFA code: ")
(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))
1Password
(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)
Resetting DNSResponder
(defun reset-dnsresponsder ()
(interactive)
(sudo-shell-command "killall -HUP mDNSResponder"))
Devpi
(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))))
Release notes
(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*"))))
StumpWM
A handy keybinding to connect to the StumpWM SBCL process via SLIME:
(defun connect-stumpwm ()
(interactive)
(slime-connect "127.0.0.1" 4004))
Emacs Network Client
Emacs frontend for networkmanager.
(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))
Deft
A fuzzy-finder for notes.
(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
(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))
Tell Deft to use the TITLE property for entry titles, if it exists:
(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))))))
Pollen
(use-package pollen-mode
:mode ("\\.p\\'" "\\.pp\\'" "\\.pm\\'"))
Ngrok
(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)))
Make
(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"))))
Redis
(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"))))
Restclient
Explore APIs from within Emacs!
(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))))
IMenu
Get a nice IMenu sidebar:
(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)
calfw-org
A fancy calendar view:
(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)
hackernews
(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)
counsel-spotify
Spotify in Emacs!
(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)
Flyspell
(when (executable-find "hunspell")
(setq ispell-program-name "hunspell"
ispell-really-hunspell t))
(add-hook 'text-mode-hook #'flyspell-mode)
(add-hook 'prog-mode-hook #'flyspell-prog-mode)
VTerm
A better terminal emulator for Emacs. Replaces ansi-term, not EShell.
(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)))
(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)))
:commands (vterm vterm-other-window vterm-mode))
(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)
Hide mode line
Does what it says on the box.
(use-package hide-mode-line
:hook ((dired-sidebar-mode imenu-list-major-mode) . hide-mode-line-mode))
Dash
Dash is a code browser app for MacOS. dash-at-point is an Emacs interface to it.
(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))
Alfred
Add a function to power the alfred-org-capture Alfred workflow:
(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))
gist.el
Integrate with GitHub gists from Emacs!
(use-package gist
:defer t
:config
(add-hook 'gist-mode-hook
(lambda ()
(evil-ex-define-local-cmd "w[rite]" 'gist-mode-save-buffer))))
ffap
Add the ability to jump to a particular line number in find-file-at-point. "Borrowed" from the EmacsWiki.
(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)))
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):
(use-package format-all
:commands (format-all-buffer)
:init
(leader-def-key "cf" 'format-all-buffer))
Compiling
Enable ANSI colors in compile buffers:
(autoload 'ansi-color-apply-on-region "ansi-color")
(defun colorize-compilation-buffer ()
(ansi-color-apply-on-region compilation-filter-start (point-max)))
(add-hook 'compilation-filter-hook #'colorize-compilation-buffer)
Set up some keybindings for Comint-mode compilation buffers:
(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)
Wallabag
My Wallabag client is still a WIP, but it is useful enough to pull in right now:
(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")))
Clipmon
Syncs up the X Windows clipboard with the Emacs kill ring. Only necessary on Linux.
(use-package clipmon
:if (eq window-system 'x)
:hook (after-init . clipmon-mode-start))
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:
(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)
realgud
Like GUD, but better!
(use-package realgud
:init
(defun projectile-pdb ()
(interactive)
(with-projectile-root
(funcall-interactively #'realgud:pdb))))
EDiff
(setq ediff-window-setup-function #'ediff-setup-windows-plain)
Apache Drill
(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"))))
Helpful
A much-improved help buffer:
(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))
Inform
Provides links to the help documentation for ELisp symbols from Info buffers:
(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"))))
Vuiet
A music browser and player:
(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))
And a handy hydra for it:
(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))
Jira
Jira in Emacs:
(use-package org-jira
: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))
Socially networking from Emacs:
(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))
Structlog
(use-package structlog-mode
:straight (structlog-mode :repo "https://git.jeremydormitzer.com/jdormit/structlog-el.git")
:config
(setq structlog-db-username "jdormit"
structlog-db-database "fluentd"))