This repository contains my Emacs configuration. It is written and documented in literate programming style.
Play Emacs like an instrumentIf you’re new to Emacs and just want to have a look around: Lean back and relax while enjoying a deep dive into the wonderful world of the Emacs editor. I have a talk “Play Emacs like an instrument” which is a small teaser of what Emacs can do - and what kinds of features you’ll find in this repository: https://www.youtube.com/watch?v=gfZDwYeBlO4
Initial
Emacs configuration is usually done in the home directory in the .emacs.d
folder. This holds true for Unix and Linux systems. For Windows, look it up here.
git clone https://github.com/munen/emacs.d.git ~/.emacs.d
Emacs dependencies/libraries are managed via the internal package management system. To initially install packages, open ~/.emacs.d/init.el
, refresh your package list with M-x package-refresh-contents
and install everything using M-x eval-buffer
.
I’m running Debian and for some things I use GNU Guix for package management. This Emacs config started out using just package.el, but it’s moving towards using Guix whenever possible. The benefits are plenty:
Spread throughout this config, there are source blocks with :noweb-ref packages
config. They define Guix packages which are tangled into one Emacs manifest, so that all things Emacs are well defined in just one config file.
(specifications->manifest '("emacs" <<packages>> ))Add guix packages to load-path
This adds the installed packages to the standard Emacs load path, so that require
just works.
;; Single directory ;; (add-to-list 'load-path "~/.guix-profile/share/emacs/site-lisp/mu4e") ;; This was enough pre Emacs 30.1: All sub directories ;; (let ((default-directory "~/.guix-profile/share/emacs/site-lisp/")) ;; (normal-top-level-add-subdirs-to-load-path)) ;; This was required from Emacs 30.1 to ensure that packages installed ;; via Guix have precedence over the versions that come with Emacs ;; pre-installed: (let ((guix-site-lisp "~/.guix-profile/share/emacs/site-lisp/")) (when (file-directory-p guix-site-lisp) ;; Get a list of all subdirectories, but ignore "." and ".." (let ((package-dirs (directory-files guix-site-lisp nil "^[^.]"))) ;; Prepend each package directory to the load-path. ;; We reverse the list to maintain a predictable order. (dolist (dir (reverse package-dirs)) (let ((path (expand-file-name dir guix-site-lisp))) (when (file-directory-p path) (add-to-list 'load-path path)))))))Automatically tangle Guix manifest
https://github.com/yilkalargaw/org-auto-tangle
Whenever a change to the Guix package blocks is made, it would be possible to manually call M-x org-babel-tangle (C-c C-v t)
.
With auto-tangle
, it’s a bit more convenient, though.
(require 'org-auto-tangle) (add-hook 'org-mode-hook 'org-auto-tangle-mode)
Guix package
"emacs-org-auto-tangle"Define package repositories(archives)
(require 'package) (setq package-archives '(("gnu" . "https://elpa.gnu.org/packages/") ;; ("nongnu" . "https://elpa.nongnu.org/nongnu/") ("melpa" . "https://melpa.org/packages/") ))Define packages that are to be installed
List all used third-party packages. Most will be configured further down in this file, some are used with the default configuration.
(defvar my-packages '(ace-window ac-js2 ag aidermacs atomic-chrome auto-complete beacon bicycle browse-kill-ring cider clj-refactor clojure-mode coffee-mode counsel-jq comment-tags darktooth-theme dired-narrow diminish dumb-jump edit-indirect editorconfig elfeed elfeed-goodies emacs-everywhere enh-ruby-mode erc-image evil evil-escape evil-leader evil-mc evil-numbers evil-surround exec-path-from-shell flycheck flycheck-flow flycheck-clj-kondo flycheck-package forge gnuplot ;; gptel hcl-mode hide-mode-line impatient-mode sops ivy counsel swiper json-mode js2-mode js2-refactor js-comint ledger-mode magit-delta markdown-mode org-ai org-mime package-lint pdf-tools projectile rainbow-mode rjsx-mode ob-restclient restclient robe sass-mode smex synosaurus tide visual-fill-column web-mode which-key writegood-mode writeroom-mode quelpa yaml-mode zenburn-theme))
(dolist (p my-packages) (unless (package-installed-p p) (package-refresh-contents) (package-install p)) (add-to-list 'package-selected-packages p))
https://github.com/quelpa/quelpa
Build and install your Emacs Lisp packages on-the-fly directly from source.
This section contains settings for built-in Emacs features.
Emacs 26.1 (for example in Debian Buster) requests the GNU Elpa repo with the wrong TLS version - which makes the request fail. This is a manual patch for older versions of Emacs. It’s fixed from 26.3 and above upstream.
(if (string< emacs-version "26.3") (setq gnutls-algorithm-priority "NORMAL:-VERS-TLS1.3"))
Allow 20MB of memory (instead of 0.76MB) before calling garbage collection. This means GC runs less often, which speeds up some operations.
(setq gc-cons-threshold 20000000)Do not create backup files
(setq make-backup-files nil)Warn when opening big files
The default warning comes with a 10MB file size which my machine handles with no noticeable delay at all. Hence, only warn when opening files bigger than 200MB.
(setq large-file-warning-threshold 200000000)
Store backups and auto-saved files in TEMPORARY-FILE-DIRECTORY
(which defaults to /tmp on Unix), instead of in the same directory as the file.
(setq backup-directory-alist `((".*" . ,temporary-file-directory))) (setq auto-save-file-name-transforms `((".*" ,temporary-file-directory t)))
When opening a file, always follow symlinks.
(setq vc-follow-symlinks t)Sentences have one space after a period
Don’t assume that sentences should have two spaces after periods.
(setq sentence-end-double-space nil)Confirm before closing Emacs
(setq confirm-kill-emacs 'y-or-n-p)
Ability to use a
to visit a new directory or file in dired
instead of using RET
. RET
works just fine, but it will create a new buffer for every interaction whereas a
reuses the current buffer.
(put 'dired-find-alternate-file 'disabled nil)
Human readable units
(setq-default dired-listing-switches "-alh")
On C
, recursively copy by default
(setq dired-recursive-copies 'always)
dired-narrow
of the dired-hacks repository allows to dynamically narrow a dired buffer down to contents of interest. A demo can be seen on this blog post.
(require 'dired) (define-key dired-mode-map (kbd "/") 'dired-narrow-fuzzy)
Commands:
/
starts fuzzy matchingg
to go back to the complete file listingy/n
instead of yes/no
This is a favorable shorthand.
(fset 'yes-or-no-p 'y-or-n-p)Auto revert files on change
When something changes a file, automatically refresh the buffer containing that file so they can’t get out of sync.
(global-auto-revert-mode t)Shortcut for changing font-size
(defun zoom-in () (interactive) (let ((x (+ (face-attribute 'default :height) 10))) (set-face-attribute 'default nil :height x))) (defun zoom-out () (interactive) (let ((x (- (face-attribute 'default :height) 10))) (set-face-attribute 'default nil :height x))) (define-key global-map (kbd "C-1") 'zoom-in) (define-key global-map (kbd "C-0") 'zoom-out)
(setq inhibit-splash-screen t) (setq inhibit-startup-message t)Do not display GUI Toolbar
Do not enable automatic line breaks for all text-mode based hooks, because several text-modes (markdown, mails) enjoy the pain of long lines. So here, I only add whitelisted modes sparingly. The other modes have a visual-line-mode=
configuration which makes the text look nice locally, at least.
(add-hook 'org-mode-hook 'auto-fill-mode)
https://github.com/joostkremers/visual-fill-column
visual-fill-column-mode
is a small Emacs minor mode that mimics the effect of fill-column
in visual-line-mode
. Instead of wrapping lines at the window edge, which is the standard behaviour of visual-line-mode
, it wraps lines at fill-column
. If fill-column
is too large for the window, the text is wrapped at the window edge.
Enable whenever upstream visual-line-mode
is activated.
(add-hook 'visual-line-mode-hook #'visual-fill-column-mode)
Enable visual-fill-mode
for all text based modes:
;; Don't do it at this time, it's only enabled for some modes explicitly. ;; (add-hook 'text-mode-hook 'visual-line-mode)
Enable adative-wrap-prefix-mode
:
https://elpa.gnu.org/packages/adaptive-wrap.html
This package provides the `adaptive-wrap-prefix-mode’ minor mode which sets the wrap-prefix property on the fly so that single-long-line paragraphs get word-wrapped in a way similar to what you’d get with M-q using adaptive-fill-mode, but without actually changing the buffer’s text.
(add-hook 'visual-line-mode-hook #'adaptive-wrap-prefix-mode)
Enable narrow-to-region (C-x n n
/ C-x n w
). This is disabled by default to not confuse beginners.
(put 'narrow-to-region 'disabled nil)Remember the cursor position of files when reopening them
(setq save-place-file "~/.emacs.d/saveplace") (if (version<= emacs-version "25.1") (progn (setq-default save-place t) (require 'saveplace)) (save-place-mode 1))Set $MANPATH, $PATH and exec-path from shell even when started from GUI helpers like
dmenu
or Spotlight
;; Safeguard, so this only runs on Linux (or MacOS) (when (memq window-system '(mac ns x)) (exec-path-from-shell-initialize))
https://github.com/abo-abo/ace-window
Quickly switch windows in Emacs
(global-set-key (kbd "M-o") 'ace-window)
Allows to ‘undo’ (and ‘redo’) changes in the window configuration with the key commands ‘C-c left’ and ‘C-c right’.
(when (fboundp 'winner-mode) (winner-mode 1))
Getting from many windows to one window is easy: ‘C-x 1’ will do it. But getting back to a delicate WindowConfiguration is difficult. This is where Winner Mode comes in: With it, going back to a previous session is easy.
Do not ring the system bell, but show a visible feedback.
Try to use passive mode for FTP.
Note: Some firewalls might not allow standard active mode. However: Some FTP Servers might not allow passive mode. So if there’s problems when connecting to an FTP, try to revert to active mode.
(setq ange-ftp-try-passive-mode t)
When entering eww, use cursors to scroll without changing point.
(add-hook 'eww-mode-hook 'scroll-lock-mode)
(setq custom-file "~/.emacs.d/custom-settings.el") (load custom-file t)
https://www.gnu.org/software/emacs/manual/html_node/emacs/Bidirectional-Editing.html
Emacs supports editing text written in scripts, such as Arabic, Farsi, and Hebrew, whose natural ordering of horizontal text for display is from right to left. However, digits and Latin text embedded in these scripts are still displayed left to right.
Whilst this is a great feature, it adds to the amount of line scans that Emacs has to do to render a line. Too many line scans will cause Emacs to hang. Since I personally do not work with right-to-left languages, I’m defaulting to displaying all paragraphs in a left-to-right manner.
(setq-default bidi-paragraph-direction 'left-to-right) (if (version<= "27.1" emacs-version) (setq bidi-inhibit-bpa t))
When the lines in a file are so long that performance could suffer to an unacceptable degree, we say “so long” to the slow modes and options enabled in that buffer, and invoke something much more basic in their place.
(if (version<= "27.1" emacs-version) (global-so-long-mode 1))
Do not report warnings and errors from asynchronous native compilation.
(setq native-comp-async-report-warnings-errors nil)
(when (>= emacs-major-version 29) (pixel-scroll-precision-mode 1))
(require 'undo-tree) (global-undo-tree-mode) (setq undo-tree-auto-save-history nil) (with-eval-after-load 'evil (define-key evil-normal-state-map (kbd "u") 'undo-tree-undo) (define-key evil-normal-state-map (kbd "C-r") 'undo-tree-redo))
Guix package
"emacs-undo-tree"
Things that should not be public.
(when (file-exists-p "~/.emacs.d/private_config.el") (load "~/.emacs.d/private_config.el"))
Some helper functions and packages I wrote that are only accessible within this Git repository and not published to a package repository.
Elisp wrapper around the dict.cc translation service. Translations are exposed in an org-mode table.
Helper functions to clean up the gazillion buffersWhen switching projects in Emacs, it can be prudent to clean up every once in a while. Deleting all buffers except the current one is one of the things I often do (especially in the long-running emacsclient
).
(defun kill-other-buffers () "Kill all other buffers." (interactive) (mapc 'kill-buffer (delq (current-buffer) (buffer-list))))
dired
will create buffers for every visited folder. This is a helper to clear them out once you’re done working with those folders.
(defun kill-dired-buffers () "Kill all open dired buffers." (interactive) (mapc (lambda (buffer) (when (eq 'dired-mode (buffer-local-value 'major-mode buffer)) (kill-buffer buffer))) (buffer-list)))Encode HTML to HTML entities
Rudimentary function converting certain HTML syntax to HTML entities.
(defun encode-html (start end) "Encodes HTML entities; works great in Visual Mode (START END)." (interactive "r") (save-excursion (save-restriction (narrow-to-region start end) (goto-char (point-min)) (replace-string "&" "&") (goto-char (point-min)) (replace-string "<" "<") (goto-char (point-min)) (replace-string ">" ">"))))Convenience functions when working with PDF exports
When working on markdown or org-mode files that will be converted to PDF, I use pdf-tools
to preview the PDF and shortcuts to automatically save, compile and reload on demand.
Here is a screencast showing how I edit Markdown or org-mode files in Emacs whilst having a PDF preview.
In a screenshot, it looks like this:
(defun md-compile () "Compiles the currently loaded markdown file using pandoc into a PDF" (interactive) (save-buffer) (shell-command (concat "pandoc " (buffer-file-name) " -o " (replace-regexp-in-string "md" "pdf" (buffer-file-name))))) (defun update-other-buffer () (interactive) (other-window 1) (revert-buffer nil t) (other-window -1)) (defun md-compile-and-update-other-buffer () "Has as a premise that it's run from a markdown-mode buffer and the other buffer already has the PDF open" (interactive) (md-compile) (update-other-buffer)) (defun latex-compile-and-update-other-buffer () "Has as a premise that it's run from a latex-mode buffer and the other buffer already has the PDF open" (interactive) (save-buffer) (shell-command (concat "pdflatex " (buffer-file-name))) (switch-to-buffer (other-buffer)) (kill-buffer) (update-other-buffer)) (defun org-compile-beamer-and-update-other-buffer () "Has as a premise that it's run from an org-mode buffer and the other buffer already has the PDF open" (interactive) (org-beamer-export-to-pdf) (update-other-buffer)) (defun org-compile-latex-and-update-other-buffer () "Has as a premise that it's run from an org-mode buffer and the other buffer already has the PDF open" (interactive) (org-latex-export-to-pdf) (update-other-buffer)) (eval-after-load 'latex-mode '(define-key latex-mode-map (kbd "C-c r") 'latex-compile-and-update-other-buffer)) (define-key org-mode-map (kbd "C-c lr") 'org-compile-latex-and-update-other-buffer) (define-key org-mode-map (kbd "C-c br") 'org-compile-beamer-and-update-other-buffer) (eval-after-load 'markdown-mode '(define-key markdown-mode-map (kbd "C-c r") 'md-compile-and-update-other-buffer))Use left Cmd to create Umlauts
Unrelated to Emacs, in macOS, you can write Umlauts by using the combo M-u [KEY]
. For example M-u u
will create the letter ü
.
This is actually faster than the default way of Emacs or that of VIM. The following code ports that functionality to Emacs.
Thx @jcfischer for the function!
(define-key key-translation-map [dead-diaeresis] (lookup-key key-translation-map "\C-x8\"")) (define-key isearch-mode-map [dead-diaeresis] nil) (global-set-key (kbd "M-u") (lookup-key key-translation-map "\C-x8\""))
Through pwgen
.
Thanks to @branch14 of 200ok fame for the function!
(defun generate-password-non-interactive () (string-trim (shell-command-to-string "pwgen -A 24"))) (defun generate-password () "Generates and inserts a new password" (interactive) (insert (shell-command-to-string (concat "pwgen -A " (read-string "Length: " "24") " 1"))))
Open the GPG encrypted password file.
Within this file, I’ll search for passwords with counsel-imenu
which has nice auto-completion and means that the headers will always be folded, so that no other person can see the passwords.
When the right header is found, I’ll copy the password under the current header to the clipboard from where I can use it where I need it (for example a browser):
Copy password to clipboard(fset 'copy-password-to-clipboard [?\C-s ?P ?a ?s ?s ?w ?o ?r ?d ?: return ?w ?v ?$ ?y C-up C-up C-up tab])
(defun passwords () "Open main 'passwords' file." (interactive) (find-file (concat org-directory "vault/primary.org.gpg")))Running
M-x shell
with zsh
If you’re a zsh
user, you might have configured a custom prompt and such. Also, you might be using a powerful $TERM
for that. When running zsh
within M-x shell
, you will have to set the $TERM
to dumb
, though. Otherwise you’ll get all kinds of escape sequences instead of colored text.
I’m using this within my ~/.zshrc
# This allows running `shell` properly within Emacs if [ -n "$INSIDE_EMACS" ]; then export TERM=dumb else export TERM=xterm-256color fi
This is the converse function to the built-in server-start
.
(defun server-shutdown () "Save buffers, Quit, and Shutdown (kill) server" (interactive) (save-some-buffers) (kill-emacs))Helper function to measure the running time of a function
(defmacro measure-time (&rest body) "Measure the time it takes to evaluate BODY." `(let ((time (current-time))) ,@body (message "%.06f" (float-time (time-since time)))))
For example (measure-time (prettier-eslint)
.
If the current buffer is not writable, ask if it should be saved with sudo
.
Happily taken from Pascals configuration: https://github.com/SirPscl/emacs.d#sudo-save
(defun ph/sudo-file-name (filename) "Prepend '/sudo:root@`system-name`:' to FILENAME if appropriate. This is, when it doesn't already have a sudo-prefix." (if (not (or (string-prefix-p "/sudo:root@localhost:" filename) (string-prefix-p (format "/sudo:root@%s:" system-name) filename))) (format "/sudo:root@%s:%s" system-name filename) filename)) (defun ph/sudo-save-buffer () "Save FILENAME with sudo if the user approves." (interactive) (when buffer-file-name (let ((file (ph/sudo-file-name buffer-file-name))) (if (yes-or-no-p (format "Save file as %s ? " file)) (write-file file))))) (advice-add 'save-buffer :around '(lambda (fn &rest args) (when (or (not (buffer-file-name)) (not (buffer-modified-p)) (file-writable-p (buffer-file-name)) (not (ph/sudo-save-buffer))) (call-interactively fn args))))Open file with emacsclient using
filename:line
path
This configuration is originally from the great bbatsov’s prelude.
“`bash emacsclient somefile:1234 “`
This will open file ‘somefile’ and set cursor on line 1234.
(defadvice server-visit-files (before parse-numbers-in-lines (files proc &optional nowait) activate) "Open file with emacsclient with cursors positioned on requested line. Most of console-based utilities prints filename in format 'filename:linenumber'. So you may wish to open filename in that format. Just call: emacsclient filename:linenumber and file 'filename' will be opened and cursor set on line 'linenumber'" (ad-set-arg 0 (mapcar (lambda (fn) (let ((name (car fn))) (if (string-match "^\\(.*?\\):\\([0-9]+\\)\\(?::\\([0-9]+\\)\\)?$" name) (cons (match-string 1 name) (cons (string-to-number (match-string 2 name)) (string-to-number (or (match-string 3 name) "")))) fn))) files)))Emacs takes SVG screenshot of itself
;; https://www.reddit.com/r/emacs/comments/idz35e/emacs_27_can_take_svg_screenshots_of_itself/ (defun screenshot-svg () "Save a screenshot of the current frame as an SVG image. Saves to a temp file and puts the filename in the kill ring." (interactive) (let* ((filename (make-temp-file "Emacs" nil ".svg")) (data (x-export-frames nil 'svg))) (with-temp-file filename (insert data)) (kill-new filename) (message filename)))Search non-ASCII characters
isearch can find a wide range of Unicode characters (like á, ⓐ, or 𝒶) when you search for ASCII characters (a in this example).
(setq search-default-mode #'char-fold-to-regexp)Move current line up or down
https://emacsredux.com/blog/2013/04/02/move-current-line-up-or-down/
(defun move-line-up () "Move up the current line." (interactive) (transpose-lines 1) (forward-line -2) (indent-according-to-mode)) (defun move-line-down () "Move down the current line." (interactive) (forward-line 1) (transpose-lines 1) (forward-line -1) (indent-according-to-mode)) (global-set-key (kbd "M-<down>") 'move-line-down) (global-set-key (kbd "M-<up>") 'move-line-up)How productive was I today?
The productivity-of-the-day
returns the total number of TODO statements that have either been added or removed from all agenda files. This is a pretty good proxy for productivity - or at least to see that there’s a bit of progress throughout the day.
The codes does the following:
git log --since=midnight -p things.org | grep TODO | grep -E "^\+|^\-" | wc -l
(defun count-lines-with-expression (s exp) "Count the number of lines in the string S that contain the regular expression EXP." (let ((count 0)) (mapc (lambda (line) (when (string-match-p exp line) (setq count (+ 1 count)))) (split-string s "\n")) count)) (defun productivity-of-the-day () (seq-reduce (lambda (acc it) (let* ((folder (file-name-directory it)) (file (file-name-nondirectory it)) (base-cmd (concat "cd " folder "; git log --since=midnight -p " file "| egrep 'TODO|WAITING'")) (changed (shell-command-to-string base-cmd)) (added (count-lines-with-expression changed "^\\+")) (removed (count-lines-with-expression changed "^\\-"))) (cons (+ (car acc) added) (- (cdr acc) removed)))) org-agenda-files '(0 . 0)))
The grep -E
part ensures that the function counts all occurences of TODO which have either been added or removed by discounting the ones that are just in the vicinity and also shown in the diff.
I add the result of productivity-of-the-day
to my i3 status bar (polybar), so it’s always visible. Here’s the config for it.
This section contains settings for non-built-in Emacs features that are generally applicable to different kinds of modes.
https://github.com/Malabarba/beacon
Whenever the window scrolls a light will shine on top of your cursor so you know where it is.
Ever wish you could just look through everything you’ve killed recently to find out if you killed that piece of text that you think you killed (or yanked), but you’re not quite sure? If so, then browse-kill-ring is the Emacs extension for you.
(require 'browse-kill-ring) (setq browse-kill-ring-highlight-inserted-item t browse-kill-ring-highlight-current-entry nil browse-kill-ring-show-preview t) (define-key browse-kill-ring-mode-map (kbd "j") 'browse-kill-ring-forward) (define-key browse-kill-ring-mode-map (kbd "k") 'browse-kill-ring-previous)
https://github.com/tecosaur/emacs-everywhere/
"xclip" "xdotool" "xprop" "xwininfo"
https://github.com/emacs-gnuplot/gnuplot
"emacs-gnuplot"
https://www.emacswiki.org/emacs/PrintingFromEmacs
(setq ps-lpr-command "print_preview")
Evil is an extensible Vim layer for Emacs.
This combines the best of both worlds: VIM being a great text-editor with modal editing through semantic commands and Emacs being a LISP REPL.
(evil-mode t) ;; Enable "M-x" in evil mode (global-set-key (kbd "M-x") 'execute-extended-command)
(global-evil-leader-mode) (evil-leader/set-leader ",") (evil-leader/set-key "w" 'basic-save-buffer "s" 'flyspell-buffer "b" 'evil-buffer "q" 'evil-quit)Evil Surround, emulating tpope’s
surround.vim
(require 'evil-surround) (global-evil-surround-mode 1)
https://github.com/gabesoft/evil-mc
evil-mc
provides multiple cursors functionality for Emacs when used with evil-mode
.
C-n / C-p
are used for creating cursors, and M-n / M-p
are used for cycling through cursors. The commands that create cursors wrap around; but, the ones that cycle them do not. To skip creating a cursor forward use C-t
or grn
and backward grp
. Finally use gru
to remove all cursors.
evil-mc
for all buffers Fast switching between buffers
(define-key evil-normal-state-map (kbd "{") 'evil-next-buffer) (define-key evil-normal-state-map (kbd "}") 'evil-prev-buffer)Increment / Decrement numbers
(global-set-key (kbd "C-=") 'evil-numbers/inc-at-pt) (global-set-key (kbd "C--") 'evil-numbers/dec-at-pt) (define-key evil-normal-state-map (kbd "C-=") 'evil-numbers/inc-at-pt) (define-key evil-normal-state-map (kbd "C--") 'evil-numbers/dec-at-pt)Use
j/k
for browsing wrapped lines
(define-key evil-normal-state-map (kbd "j") 'evil-next-visual-line) (define-key evil-normal-state-map (kbd "k") 'evil-previous-visual-line)
(define-key evil-insert-state-map (kbd "C-v") 'evil-visual-paste)Disable
evil-mode
for some modes
Since Emacs is a multi-purpose LISP REPL, there are many modes that are not primarily (or not at all) centered about text-manipulation. For those, it is reasonable to disable evil-mode
, because it will bring nothing to the table, but might just shadow some keyboard shortcuts.
(mapc (lambda (mode) (evil-set-initial-state mode 'emacs)) '(elfeed-show-mode elfeed-search-mode forge-pullreq-list-mode forge-topic-list-mode dired-mode tide-references-mode image-dired-mode image-dired-thumbnail-mode eww-mode))
Turning off evil when working in cider--debug
minor mode:
(defadvice cider--debug-mode (after toggle-evil activate) "Turn off `evil-local-mode' when enabling `cider--debug-mode', and turn it back on when disabling `cider--debug-mode'." (evil-local-mode (if cider--debug-mode -1 1)))Unbind certain Emacs keybindings in
evil-mode
M-.
and M-,
are popular keybindings for “jump to definition” and “back”. evil-mode
by default binds those to rather rarely used functions evil-repeat-pop-next
and xref-pop-marker-stack
, for some reason.
(define-key evil-normal-state-map (kbd "M-.") nil) (define-key evil-normal-state-map (kbd "M-,") nil)
M-l
and M-l M-l
is downcase-word
. This happens a lot by accident for me. And undoing it often undoes a lot more - like deleting whole paragraphs of text. Also, I don’t need it, because I’d use evil bindings for that.
(define-key global-map (kbd "M-l") nil) (define-key evil-insert-state-map (kbd "M-l M-l") nil)
M-k
is kill-sentence
. That happens by accident, as well. And sometimes, when in insert-mode, it even erases the history. I don’t need it, I’d use evil for that.
(define-key global-map (kbd "M-k") nil) (define-key evil-insert-state-map (kbd "M-k M-k") nil)
TAB
is evil-jump-forward
: Go to newer position in jump list.
(define-key global-map (kbd "<tab>") nil) (define-key evil-insert-state-map (kbd "<tab>") nil) (evil-define-key 'normal org-mode-map (kbd "<tab>") #'org-cycle)Call
ex
by default on visual selection
(setq evil-ex-visual-char-range t)
Example:
When visually selecting “foo” out of the string “foo foobar”, and then calling :s/o/i/g
, the result would be “fii fiibar” without this setting. With this setting, it will be “fii foobar”.
https://github.com/syl20bnr/evil-escape
Escape from insert state and everything else.
(setq-default evil-escape-delay 0.2) (setq-default evil-escape-key-sequence "jk") (evil-escape-mode)
This results in the same feature-set like this vim keybinding:
"Remap ESC to jk :imap jk <esc>Change some Emacs keybindings
With backward-kill-sentence
, I sometimes shoot myself in the foot. I trigger this shortcut by accident and then all kinds of stuff happens. And undo-tree-undo
does not always undo the deed for reasons. Anyway, I do not need the Emacs style backward-kill-sentence
:
(global-unset-key (kbd "C-x <backspace>")) (global-unset-key (kbd "C-x DEL"))
which-key
displays available keybindings in a popup.
(add-hook 'org-mode-hook 'which-key-mode) (add-hook 'cider-mode-hook 'which-key-mode)
Use which-key
to show VIM shortcuts, too.
(setq which-key-allow-evil-operators t) (setq which-key-show-operator-state-maps t)
https://github.com/auto-complete/auto-complete
Basic Configuration
Set tab width to 2 for all buffers
(setq-default tab-width 2)
Use 2 spaces instead of a tab.
(setq-default tab-width 2 indent-tabs-mode nil)
Indentation cannot insert tabs.
(setq-default indent-tabs-mode nil)
Use 2 spaces instead of tabs for programming languages.
(setq js-indent-level 2) (setq coffee-tab-width 2) (setq python-indent 2) (setq css-indent-offset 2) (add-hook 'sh-mode-hook (lambda () (setq sh-basic-offset 2 sh-indentation 2))) (setq web-mode-markup-indent-offset 2)Syntax Checking (flycheck)
Enable global on the fly syntax checking through flycheck
.
(add-hook 'after-init-hook #'global-flycheck-mode)
https://github.com/purcell/flycheck-package
This library provides a flycheck checker for the metadata in Emacs Lisp files which are intended to be packages. That metadata includes the package description, its dependencies and more.
(eval-after-load 'flycheck '(flycheck-package-setup))Auto-indent with the Return key
(define-key global-map (kbd "RET") 'newline-and-indent)Highlight matching parenthesis Delete trailing whitespace
Delete trailing whitespace in all modes. Except when editing Markdown, because it uses two trailing blanks as a signal to create a line break.
(add-hook 'before-save-hook '(lambda() (when (not (or (derived-mode-p 'markdown-mode) (derived-mode-p 'org-mode))) (delete-trailing-whitespace))))
Enable code folding for programming modes with two strategies:
zc
: Close fold (one)za
: Toggle fold (one)zr
: Open folds (all)zm
: Close folds (all)(add-hook 'prog-mode-hook #'hs-minor-mode)2. Org mode style folds with
outline-minor-mode
outline-minor-mode
is built-in to Emacs. It enables structural editing of hierarchical structures - just as Org mode does, but in any major mode.
Change the shortcuts to be the same as in Org mode:
(add-hook 'prog-mode-hook #'outline-minor-mode) ;; Org mode style keybindings (define-key outline-minor-mode-map (kbd "C-<return>") 'outline-insert-heading) (define-key outline-minor-mode-map (kbd "M-S-<right>") 'outline-demote) (define-key outline-minor-mode-map (kbd "M-S-<left>") 'outline-promote) (define-key outline-minor-mode-map (kbd "C-c C-n") 'outline-next-visible-heading) (define-key outline-minor-mode-map (kbd "C-c C-p") 'outline-previous-visible-heading)
Leverage the bicycle library from tarsius for the ability to cycle visibility of local and global sections:
(define-key outline-minor-mode-map (kbd "C-<tab>") 'bicycle-cycle) (define-key outline-minor-mode-map (kbd "<backtab>") 'bicycle-cycle-global)
Use the built-in foldout.el to narrow and widen the current subtree:
(require 'foldout) (define-key outline-minor-mode-map (kbd "C-x n s") 'foldout-zoom-subtree) (define-key outline-minor-mode-map (kbd "C-x n w") 'foldout-exit-fold)
Enable linum-mode
for programming modes. For newer versions of Emacs, use display-line-numbers-mode
, because it’s much faster.
(add-hook 'prog-mode-hook '(lambda () (if (version<= emacs-version "26.0.50") (linum-mode) (display-line-numbers-mode))))
(defun indent-buffer () (interactive) (save-excursion (indent-region (point-min) (point-max) nil)))
For syntax checking to work, installing the command-line linter tools ruby-lint and eslint are a premise:
gem install rubocop ruby-lint npm install -g eslint
(setq ruby-indent-level 2) ;; scss-mode blocks Emacs when opening bigger files, so open them with css-mode (add-to-list 'auto-mode-alist '("\\.scss?\\'" . css-mode)) (add-to-list 'auto-mode-alist '("\\.rb?\\'" . enh-ruby-mode)) (add-to-list 'auto-mode-alist '("\\.rake?\\'" . enh-ruby-mode))
https://github.com/dgutov/robe
Code navigation, documentation lookup and completion for Ruby
(add-hook 'enh-ruby-mode-hook 'robe-mode) (add-hook 'robe-mode-hook 'ac-robe-setup) (add-to-list 'auto-mode-alist '("\\.erb?\\'" . robe-mode))
Start robe-mode
with M-x robe-start
.
Shortcuts:
C-c C-d
Lookup documentationM-.
Jump to definitionTAB
Auto-completion through auto-complete-mode
auto-complete
for robe-mode
(add-hook 'enh-ruby-mode-hook 'auto-complete-mode)
(add-hook 'enh-ruby-mode-hook (lambda () (local-set-key (kbd "C-x C-e") 'ruby-send-line)))
https://github.com/clojure-emacs/cider
Cider is short for The “Clojure Interactive Development Environment that Rocks for Emacs”. For good reasons, it is the most popular IDE for developing Clojure.
M-x cider-jack-in
To start REPLC-c C-k
Evaluate current bufferC-c M-n
Change ns in cider-nrepl to current nsC-c C-d C-d
Display documentation for the symbol under pointC-c C-d C-a
Apropos search for arbitrary text across function names and documentationC-↑, C-↓
Cycle through REPL history.Remove C-c C-p
(cider-pprint-eval-last-sexp
) from mode map in favor of using Org mode style folding.
(add-hook 'cider-mode-hook (lambda () (define-key cider-mode-map (kbd "C-c C-p") nil)))
Create a ~/.lein/profiles.clj
file with:
{:user {:plugins [[cider/cider-nrepl "0.13.0-SNAPSHOT"] [refactor-nrepl "2.2.0"]] :dependencies [[org.clojure/tools.nrepl "0.2.12"]]}}
When connecting to a repl, don’t pop to the new repl buffer.
(setq cider-repl-pop-to-buffer-on-connect nil)
https://github.com/clojure-emacs/clj-refactor.el/
A collection of Clojure refactoring functions for Emacs.
(require 'clj-refactor) (defun my-clojure-mode-hook () (clj-refactor-mode 1) (yas-minor-mode 1) ; for adding require/use/import statements ;; This choice of keybinding leaves cider-macroexpand-1 unbound (cljr-add-keybindings-with-prefix "C-c C-m")) (add-hook 'clojure-mode-hook #'my-clojure-mode-hook)
clj-refactor
enables refactorings like extracting functions (C-c C-m ef
). Find the list of available refactorings here.
Integrant configures, starts and manages a system
and exposes a lifecycle for it.
For REPL-driven development this adds one layer of indirection: When starting a service through lein run
(or bundled in a Docker container), the system
will already be started by Integrant. Without having a ref to this system
, we cannot stop it, we can only start new systems. This means that reloading the code will only start new systems, but not be able to halt the old one. The internal code from Integrant relies on spawning a thread after initializing a system through lein run
and will not return until the process is done. Therefore we cannot retrieve the system when running lein run
.
When Emacs has a connection to a REPL for an Integrant based application, this snippet actually enables reloading of front and back-ends. The code doesn’t use cider internal functions for interacting with the REPL, because not all buffers might be connected (for example the CLJS buffers might not have a dedicated REPL themselves). Instead, it uses common Elisp.
(defun ok-cider-reload-integrant () (interactive) (require 'seq) (save-buffer) (let ((cider-buffer (first (seq-filter '(lambda (buf) (string-match "cider-repl" buf)) (mapcar 'buffer-name (buffer-list)))))) (if cider-buffer (progn (switch-to-buffer cider-buffer) (insert "(in-ns 'dev)(integrant.repl/reset)") (cider-repl-return) (switch-to-buffer (other-buffer))) (message "No Cider buffer!")))) (add-hook 'clojure-mode-hook '(lambda () (define-key clojure-mode-map (kbd "C-c r") 'ok-cider-reload-integrant)))
Usage
When you want to reload the system
, use C-c r
. It will save your current buffer and reload the system
.
https://github.com/borkdude/flycheck-clj-kondo
clj-kondo is installed via stow.
(add-hook 'clojure-mode-hook (lambda () (require 'flycheck-clj-kondo)))
https://github.com/ananthakumaran/tide
Claim: TypeScript Interactive Development Environment for Emacs. However, also JavaScript development gets big improvements with tide-mode
.
Tide is an alternative to Tern which also has great Emacs integration and which I have happily been using for years. However, tide works even better (in my experience).
For completion to work in a Node.js project, a jsconfig.json
file like this is required:
{ "compilerOptions": { "target": "es6" }, "exclude": [ "node_modules" ] }
If no project file is found, it’ll fall back to an inferred configuration.
Tide default shortcuts:
M-.
Jump to the definition of the thing under the cursor.M-,
Brings you back to last place you were when you pressed M-..(require 'rjsx-mode) (define-key rjsx-mode-map (kbd "C-c C-r") 'tide-rename-symbol) (define-key rjsx-mode-map (kbd "C-c C-d") 'tide-documentation-at-point)
(defun setup-tide-mode () (interactive) ;; For bigger JS projects and intense tasks like =tide=references= ;; the default of 2s will time out (setq tide-sync-request-timeout 10) (tide-setup) ;; Increase sync request timeout for bigger projects (flycheck-mode +1) (setq flycheck-check-syntax-automatically '(save mode-enabled)) (eldoc-mode +1) (tide-hl-identifier-mode +1)) (add-hook 'rjsx-mode-hook #'setup-tide-mode)
https://github.com/redguardtoo/js-comint
Run a JavaScript interpreter in an inferior process window.
(add-hook 'rjsx-mode-hook (lambda () (local-set-key (kbd "C-x C-e") 'js-send-last-sexp) (local-set-key (kbd "C-M-x") 'js-send-last-sexp-and-go) (local-set-key (kbd "C-c b") 'js-send-buffer) (local-set-key (kbd "C-c C-b") 'js-send-buffer-and-go) (local-set-key (kbd "C-c l") 'js-load-file-and-go)))
Flow is a static type checker for JavaScript.
Flow uses type inference to find bugs even without type annotations. It precisely tracks the types of variables as they flow through your program.
Flow is designed for JavaScript programmers. It understands common JavaScript idioms and very dynamic code.
Flow incrementally rechecks your changes as you work, preserving the fast feedback cycle of developing plain JavaScript.
(require 'flycheck-flow) (add-hook 'javascript-mode-hook 'flycheck-mode)
https://github.com/felipeochoa/rjsx-mode
This mode derives from js2-mode, extending its parser to support JSX syntax according to the official spec. This means you get all of the js2 features plus proper syntax checking and highlighting of JSX code blocks.
(add-to-list 'auto-mode-alist '("components\\/.*\\.js\\'" . rjsx-mode))General JavaScript configuration
(add-to-list 'auto-mode-alist '("\\.js\\'" . rjsx-mode)) (add-hook 'js-mode-hook 'js2-minor-mode) (setq js2-highlight-level 3) (setq js-indent-level 2) ;; Semicolons are optional in JS, do not warn about them missing (setq js2-strict-missing-semi-warning nil)
rainbow-mode
is a minor mode for Emacs which displays strings representing colors with the color they represent as background.
(add-hook 'prog-mode-hook 'rainbow-mode)
https://github.com/netguy204/imp.el
Live JavaScript Coding Emacs/Browser: See your changes in the browser as you type
Enable the web server provided by simple-httpd: M-x httpd-start
Publish buffers by enabling the minor mode impatient-mode: M-x impatient-mode
And then point your browser to http://localhost:8080/imp/, select a buffer, and watch your changes appear as you type!
https://github.com/200ok-ch/counsel-jq
jq is a lightweight and flexible command-line JSON processor. This loads a counsel wrapper to quickly test queries and traverse a complex JSON structure whilst having live feedback.
Thanks to @branch14 of 200ok fame for starting with the initial function!
web-mode.el is an autonomous major-mode for editing web templates.
(add-to-list 'auto-mode-alist '("\\.html?\\'" . web-mode)) ;; Ruby Templates (add-to-list 'auto-mode-alist '("\\.erb?\\'" . web-mode)) ;; Handlebars (add-to-list 'auto-mode-alist '("\\.hbs?\\'" . web-mode)) ;; JSON (add-to-list 'auto-mode-alist '("\\.json?\\'" . web-mode)) (setq web-mode-enable-current-element-highlight t) (setq web-mode-ac-sources-alist '(("html" . (ac-source-words-in-buffer ac-source-abbrev))))
p_slides is a static files only, dead simple way, to create semantic slides. The slide content is markdown, embedded in a HTML file. When opening a presentation.html
file, enable markdown-mode
.
(add-to-list 'auto-mode-alist '("presentation.html" . markdown-mode))
Introducing a custom browser-reloading-mode
. It’s a quick implementation and not a real derived mode.
When enabling browser-reloading-mode
for a specific buffer, whenever this buffer is saved, a command-line utility reload_chromium.sh
is called. This in turn is a wrapper around xdotool
with which a reloading of the Chromium browser is triggered.
This is handy when working in a web environment that doesn’t natively support hot-reloading (static web pages, for instance) and the page has too much (dynamic) content to be displayed properly in impatient-mode
. I’m using it for example when working on a p_slides slide deck.
(defun reload-chromium () (when enable-browser-reloading (shell-command-to-string "reload_chromium.sh"))) (defun browser-reloading-mode () "Finds the open chromium session and reloads the tab" (interactive) ;; When set, disable the local binding and therefore disable the mode (if enable-browser-reloading (setq enable-browser-reloading nil) ;; Otherwise create a local var and set it to True (progn (make-local-variable 'enable-browser-reloading) (setq enable-browser-reloading t)))) ;; By default, disable the guard against using `reload-chromium` (setq enable-browser-reloading nil) (add-hook 'after-save-hook #'reload-chromium)
(require 'yaml-mode) (add-to-list 'auto-mode-alist '("\\.yml$" . yaml-mode)) (add-hook 'yaml-mode-hook 'visual-line-mode)
(add-hook 'markdown-mode-hook 'flyspell-mode) (add-hook 'markdown-mode-hook 'outline-minor-mode)
Unfortunately line breaks are semantic in some versions of markdown (for example Github). So doing automatic line breaks would be harmful. However, this leads to super long lines in many documents which is unreadable. Therefore, always use visual-line-mode
.
(add-hook 'markdown-mode-hook 'visual-line-mode)
https://github.com/magit/magit
Magit is an interface to the version control system Git.
"emacs-magit"
Create shortcut for Magit
.
(global-set-key (kbd "C-x g") 'magit-status)
Always sign commits with GPG
(setq magit-commit-arguments (quote ("--gpg-sign=137099B38E1FC0E9")))Start the commit buffer in evil normal mode
(add-hook 'with-editor-mode-hook 'evil-normal-state)
https://magit.vc/manual/magit/Performance.html
(setq magit-refresh-status-buffer nil) ;; (setq magit-refresh-verbose t) (with-eval-after-load 'magit (remove-hook 'magit-refs-sections-hook 'magit-insert-tags) (remove-hook 'server-switch-hook 'magit-commit-diffq) (remove-hook 'with-editor-filter-visit-hook 'magit-commit-diff))
https://github.com/magit/forge/
Work with Git forges from the comfort of Magit.
Temporarily use the version from melpa, because there’s no official release tag, but there’s lots of new commits and releases on melpa.
(with-eval-after-load 'magit (require 'forge))
Add 200ok gitlab instance to list of known forges
(with-eval-after-load 'forge (add-to-list 'forge-alist '("gitlab.200ok.ch" "gitlab.200ok.ch/api/v4" "gitlab.200ok.ch" forge-gitlab-repository)) (add-to-list 'forge-alist '("gitlab.switch.ch" "gitlab.switch.ch/api/v4" "gitlab.switch.ch" forge-gitlab-repository)))
Show assigned issues and PRs as well as requested reviews directly in the status buffer:
;; TODO: This is not possible in newer versions of magit/forge, ;; anymore, but there are alternatives: ;; https://github.com/magit/forge/issues/676 ;; (with-eval-after-load 'magit ;; (magit-add-section-hook 'magit-status-sections-hook 'forge-insert-assigned-issues nil t) ;; (magit-add-section-hook 'magit-status-sections-hook 'forge-insert-assigned-pullreqs nil t) ;; (magit-add-section-hook 'magit-status-sections-hook 'forge-insert-requested-reviews nil t))
https://github.com/dandavison/magit-delta
Provides a minor mode which configures Magit to use delta when displaying diffs.
Enable magit-delta
when running magit
.
(with-eval-after-load 'magit (add-hook 'magit-mode-hook (lambda () (magit-delta-mode +1))))
Enable magit-delta
only for ‘regular’ diffs, but not when merging huge amounts of stuff, because that’ll make Emacs hang. Taken from dandavison/magit-delta#9 (comment).
(defun magit-delta-toggle () "Toggle magit-delta-mode and refresh magit." (interactive) (progn (call-interactively 'magit-delta-mode) (magit-refresh))) (defvar nth/magit-delta-point-max 50000) ;; Disable mode if there are too many characters (advice-add 'magit-delta-call-delta-and-convert-ansi-escape-sequences :around (defun nth/magit-delta-colorize-maybe-a (fn &rest args) (if (<= (point-max) nth/magit-delta-point-max) (apply fn args) (magit-delta-mode -1)))) ;; Re-enable mode after `magit-refresh' if there aren't too many characters (add-hook 'magit-post-refresh-hook (defun nth/magit-enable-magit-delta-maybe-h (&rest _args) (when (and (not magit-delta-mode) (<= (point-max) nth/magit-delta-point-max)) (magit-delta-mode +1))))
Override the settings (~/.gitconfig
) for delta
, because the line-numbers
feature won’t work well with magit-delta
(see dandavison/magit-delta#13).
(setq magit-delta-delta-args '("--24-bit-color" "always" "--features" "magit-delta" "--color-only"))
https://github.com/bbatsov/projectile
Projectile is a project interaction library. For instance - finding project files (C-c p f
) or jumping to a new project (C-c p p
).
Enable Projectile globally
(projectile-mode +1) (define-key projectile-mode-map (kbd "C-c p") 'projectile-command-map)
Disable projectile when using TRAMP. Otherwise Tramp will crawl to a halt.
(defadvice projectile-project-root (around ignore-remote first activate) (unless (file-remote-p default-directory) ad-do-it))
https://github.com/jacktasia/dumb-jump
“Jump to definition” with support for multiple programming languages that favors “just working”. This means minimal – and ideally zero – configuration with absolutely no stored indexes (TAGS) or persistent background processes.
Dumb Jump uses The Silver Searcher ag, ripgrep rg, or grep to find potential definitions of a function or variable under point. It uses a set of regular expressions based on the file extension, or major-mode, of the current buffer.
(dumb-jump-mode) (setq dumb-jump-selector 'ivy)
The one important shortcut is C-M-g
which attempts to jump to the definition of the thing under point.
Automatically format code for different languages and frameworks.
This implements the interactive function autoformat
which is a thin wrapper around command-line based code autoformatters which it utilizes through a strategy pattern.
To add a new language/framework, the only required change is to add the respective command-line tool configuration into a separate strategy function. It is trivial to do if the new language/framework has a command-line tool which takes code into stdin
and formats it to stdout
.
It’s possible to install the dependencies locally, so that the setup doesn’t impose dependencies on team members - or they can be installed through the respective packages managers (npm/yarn) to enforce code guidelines.
This requires prettier
, @prettier/plugin-ruby
and prettier-eslint-cli
to be installed:
npm install -g prettier-eslint-cli prettier @prettier/plugin-ruby
Linting JavaScript with eslint happens automatically through flycheck. eslint just needs to be installed.
(defun autoformat () "Automatically format current buffer." (interactive) (if (derived-mode-p 'clojure-mode) (autoformat-clojure-function) (let ((eslint-path (concat (projectile-project-root) ".eslintrc.yml"))) ; could be .json or .yml (autoformat-with (cond ((derived-mode-p 'web-mode) 'autoformat-html-command) ((derived-mode-p 'css-mode) 'autoformat-css-command) ((derived-mode-p 'nxml-mode) 'autoformat-xml-command) ((derived-mode-p 'json-mode) 'autoformat-json-command) ((derived-mode-p 'sass-mode) 'autoformat-sass-command) ((derived-mode-p 'yaml-mode) 'autoformat-yaml-command) ((derived-mode-p 'enh-ruby-mode) 'autoformat-ruby-command) ;; JS projects with eslint config ((and (file-exists-p eslint-path) (derived-mode-p 'js2-mode)) 'autoformat-prettier-eslint-command) ((derived-mode-p 'js2-mode) 'autoformat-javascript-command)))))) (defun autoformat-with (strategy) "Automatically format current buffer using STRATEGY." (let ((p (point)) (s (window-start))) ;; Remember the current position (save-mark-and-excursion ;; Call prettier-eslint binary with the contents of the current ;; buffer (shell-command-on-region (point-min) (point-max) (funcall strategy) ;; Write into a temporary buffer (get-buffer-create "*Temp autoformat buffer*") ;; Replace the current buffer with the output of ;; the =autoformat strategy= output t ;; If the =autoformat strategy= returns an error, show it in a ;; separate error buffer (get-buffer-create "*replace-errors*") ;; Automatically show error buffer t)) ;; Return to the previous point and scrolling position (the point ;; was lost, because the whole buffer got replaced. (set-window-start (selected-window) s) (goto-char p))) (defun autoformat-clojure-function () "Cider function to format Clojure buffer." (indent-buffer) ;; (cider-format-buffer) ) (defun autoformat-ruby-command () "CLI tool to format Ruby." "prettier --parser ruby") (defun autoformat-javascript-command () "CLI tool to format Javascript." "prettier --parser babel") (defun autoformat-html-command () "CLI tool to format HTML." "prettier --parser html") (defun autoformat-css-command () "CLI tool to format CSS." "prettier --parser css") (defun autoformat-xml-command () "CLI tool to format XML." "xmllint -format -") (defun autoformat-sass-command () "CLI tool to format SASS." "prettier --parser sass") (defun autoformat-json-command () "CLI tool to format JSON." "prettier --parser json") (defun autoformat-yaml-command () "CLI tool to format YAML." "prettier --parser yaml") (defun autoformat-prettier-eslint-command () "CLI tool to format Javascript with .eslintrc.json configuration." (concat "npx prettier-eslint --stdin --eslint-config-path=" ;; Hand over the path of the current projec (concat (projectile-project-root) ".eslintrc.yml") " --stdin-filepath=" (buffer-file-name) " --parser babel"))
Shortcut
(setq ok-autoformat-modes (list 'web-mode 'css-mode 'json-mode 'clojure-mode 'sass-mode 'enh-ruby-mode 'yaml-mode 'js2-mode 'rjsx-mode)) (dolist (mode ok-autoformat-modes) (evil-leader/set-key-for-mode mode "f" 'autoformat))
Demo
Call autoformat on every save - for certain projectsI don’t want to autoformat
for every project, because I might not be the primary owner of the code (that accounts for consulting projects). However, there are projects where I actually do want to run autoformat
every time. That is on projects with strict formatting requirements.
NB: The overhead of prettier + eslint is about 1.3s on a maxed out X1 Carbon 6th gen.
;; Define list of projects to autoformat (setq ok-autoformat-projects (list "src/200ok/organice")) (add-hook 'before-save-hook '(lambda() ;; Check if the current directory matches the list of ;; projects that are to be autoformatted. (if (seq-some '(lambda (e) (numberp e)) (mapcar '(lambda (dir) (string-match dir (projectile-project-root))) ok-autoformat-projects) ) (when (or (derived-mode-p 'js2-mode) (derived-mode-p 'css-mode) (derived-mode-p 'sass-mode) (derived-mode-p 'yaml-mode)) (autoformat)))))Alternative implementation
NB: This could be a good alternative solution. However, scoping to the local directory doesn’t work like this. Maybe I’m doing it wrong, maybe dir-locals just shouldn’t be used outside of setting variables.
Call autoformat on every save for specific projects
those projects, you can enable autoformat
by creating a .dir-locals.el
file in your home directory.
(("src" (nil . ((eval add-hook 'before-save-hook '(lambda() (autoformat)))))))
The first node “src/” is the directory, while the second node is the mode-name, or “nil” to apply to every mode.
EditorConfig helps maintain consistent coding styles for multiple developers working on the same project across various editors and IDEs. I’m an Emacs guy, however, when in an heterogeneous team, it does make sense to adhere to some commonly shared definitions.
With this plugin, if there is an .editorconfig
in a project, the settings in this file will trump my personal config.
https://github.com/purcell/emacs-hcl-mode
Major mode for Hashicorp Configuration Language. I use it for Terraform.
(add-to-list 'auto-mode-alist '("\\.tf" . hcl-mode))
(add-to-list 'auto-mode-alist '("\\.drl\\'" . java-mode))
https://github.com/djgoku/sops
When opening an encrypted file, it will still show the encrypted values. To edit in the clear, use sops-edit-file
. After modifications have been made you can save with sops-save-file
or discard modifications with sops-cancel
.
https://github.com/natrys/whisper.el
I installed whisper.ccp manually on the commandline, though whisper.el supports automatic installation.
I benchmarked different models against each other. The “base” model is quite fast and has good results. “tiny” has worse results and starting from “small”, the computation time goes way up.
Also, I’ve benchmarked whisper.cpp streaming vs whisper.el and the latter is far superior.
Note that setting the whisper-language
to ‘auto’ implies that one recording can only have one language. Giving it multiple sentences in different languages will not return a happy result.
(quelpa '(whisper :fetcher git :url "https://github.com/natrys/whisper.el.git")) (setq whisper-install-directory "~/src" whisper-model "base-q8_0" whisper-language "auto" whisper-use-threads 10 whisper-translate nil) ;; Don't use this locally anymore, because it's too slow for me to run ;; a large model on a machine without appropriate GPU. I'm using ;; ok-whisper, instead. ;; (define-key global-map (kbd "C-x R") 'whisper-run)Access to whisper recordings in Emacs from anywhere in the OS
With an OS keybinding of Shift + Alt + r
, I run emacsclient -c --eval '(start-whisper-recording)
(I use xbindkeys for that).
(defun start-whisper-recording () "Interactively start a new whisper recording." (interactive) (switch-to-buffer "*whisper-recording*") (self-insert-command 1) (whisper-run))
(add-to-list 'load-path "~/src/200ok/ok-audio-transcription") (require 'ok-audio-transcription) (define-key global-map (kbd "C-x R") 'ok-audio-transcription--record-dwim)
Outline-based notes management and organizer. It is an outline-mode for keeping track of everything.
Next to Emacs Org mode, I use organice (https://github.com/200ok-ch/organice/) to manage my Org files on the go and to collaborate with non-Emacs users.
(setq org-directory "~/Dropbox/org/")
Configure org-display-inline-images
width so that they always fit. My screenshots would otherwise overflow, because I’m on a HiDPI display.
(setq org-image-actual-width 720)
Allow ‘a.’, ‘A.’, ‘a)’ and ‘A) as list elements:
(setq org-list-allow-alphabetical t)Warn about an approaching deadline
The default is 14 days ahead. That’s way too much for me. If a task needs a lot of work ahead of the deadline, I’ll set a custom reminder date or an additional schedule.
(setq org-deadline-warning-days 3)
(require 'org) ; languages for org-babel support (org-babel-do-load-languages 'org-babel-load-languages '( (shell . t) (dot . t) (js . t) (ruby . t) )) (add-hook 'org-mode-hook 'auto-fill-mode) (add-hook 'org-mode-hook 'flyspell-mode) (evil-leader/set-key "a" 'org-archive-subtree-default) ;; Allow =pdflatex= to use shell-commands. This will allow it to use ;; =pygments= as syntax highlighter for exports to PDF. (setq org-latex-pdf-process '("pdflatex -shell-escape -interaction nonstopmode -output-directory %o %f" "pdflatex -shell-escape -interaction nonstopmode -output-directory %o %f" "pdflatex -shell-escape -interaction nonstopmode -output-directory %o %f")) ;; Alternatively use =xelatex=. Required for documents where I want to use ttf fonts. ;; (setq org-latex-pdf-process ;; '("xelatex -shell-escape -interaction nonstopmode -output-directory %o %f" ;; "xelatex -shell-escape -interaction nonstopmode -output-directory %o %f" ;; "xelatex -shell-escape -interaction nonstopmode -output-directory %o %f")) ;; Include =minted= package for LaTeX exports (add-to-list 'org-latex-packages-alist '("" "minted")) (setq org-latex-listings 'minted) ;; Don’t ask every time when executing a code block. (setq org-confirm-babel-evaluate nil)
imenu
would normally only index two levels - since I run deeply nested documents, go up to six levels.
When a document is folded and the user searches and finds with imenu
, the body of the folded header is revealed, so that the search result can actually be seen.
(defun ok-imenu-show-entry () "Reveal content of header." (cond ((and (eq major-mode 'org-mode) (org-at-heading-p)) (org-show-entry) (org-reveal t)) ((bound-and-true-p outline-minor-mode) (outline-show-entry)))) (add-hook 'imenu-after-jump-hook 'ok-imenu-show-entry)
(require 'ox-latex) (add-to-list 'org-latex-classes '("scrartcl" "\\documentclass{scrartcl}" ("\\section{%s}" . "\\section*{%s}")))
(require 'ox-latex) (add-to-list 'org-latex-classes '("tuftehandout" "\\documentclass{tufte-handout} \\usepackage{color} \\usepackage{amssymb} \\usepackage{amsmath} \\usepackage{gensymb} \\usepackage{nicefrac} \\usepackage{units}" ("\\section{%s}" . "\\section*{%s}") ("\\subsection{%s}" . "\\subsection*{%s}") ("\\paragraph{%s}" . "\\paragraph*{%s}") ("\\subparagraph{%s}" . "\\subparagraph*{%s}")))
(with-eval-after-load 'ox-latex (add-to-list 'org-latex-classes '("memoir" "\\documentclass[12pt]{memoir}" ("\\chapter{%s}" . "\\chapter*{%s}") ("\\section{%s}" . "\\section*{%s}") ("\\subsection{%s}" . "\\subsection*{%s}") ("\\subsubsection{%s}" . "\\subsubsection*{%s}") ("\\paragraph{%s}" . "\\paragraph*{%s}") ("\\subparagraph{%s}" . "\\subparagraph*{%s}"))))
Align tags to the far right of the screen. -77
would be good for a smaller 80 character terminal.
(setq org-tags-column -100)Set up capture templates for:
Inbox
Inbox
snippets.org
things.org
media.org
Org Capture Templates are explained here, Org Template expansion here.
;; Set org-capture inbox (setq org-default-notes-file (concat org-directory "inbox.org")) (define-key global-map "\C-cc" 'org-capture) (setq things-file (expand-file-name "things.org" org-directory)) (setq reference-file (expand-file-name "reference.org" org-directory)) (setq media-file (expand-file-name "media.org" org-directory)) (defun get-domainname (address) "Extract TLD (without country) from ADDRESS. Example: Return '200ok' from 'alain@200ok.ch'." (replace-regexp-in-string "\-" "_" (nth 0 (split-string (nth 1 (split-string address "@")) "\\.")))) (defun from-name (fromname fromaddress from) "Return the first non-empty match for FROMNAME FROMADDRESS and FROM." (nth 0 (seq-filter '(lambda (s) (not (string-empty-p s))) (list fromname fromaddress from)))) (setq org-capture-templates `(("t" "Todo" entry (file+olp things-file "Inbox" "Tasks") "* TODO %?\n %U\n %i\n %a" :prepend t) ("w" "Waiting" entry (file+olp things-file "Waiting") "* WAITING %?\n %U\n %i\n %a") ;; Creates an expense line for the date of the mail, prompts ;; for the amount and currency ("e" "Expense" table-line (file+olp things-file "Inbox" "Expenses") "|%(org-insert-time-stamp (org-read-date nil t \"%:date\") nil t) | %(from-name \"%:fromname\" \"%:fromaddress\" \"%:from\")| [[%:link][Mail]] | %^{amount} | %^{currency|usd|chf|eur} | | | %^{scope|200ok-alain|200ok-joint|lambda} |") ("m" "Mail" entry (file+olp things-file "Inbox" "Mails") ;; Creates "* TODO <2019-05-01 Wed> FromName [[mu4e:msgid:uuid][MessageSubject]] :200ok: ;; Therefore Emails can be properly: ;; - Used as tasks ;; - Attributed tags ;; - Ordered by priority ;; - Scheduled ;; - etc "* TODO %(org-insert-time-stamp (org-read-date nil t \"%:date\") nil t) %(from-name \"%:fromname\" \"%:fromaddress\" \"%:from\") %a \t :%(get-domainname \"%:toaddress\"):") ("d" "Daily focus" plain (file+olp things-file "Inbox" "Daily") (file "~/.emacs.d/org-templates/daily_focus.org")) ("M" "Meeting minutes" plain (file+olp things-file "Inbox" "Tasks") (file "~/.emacs.d/org-templates/minutes.org")) ("s" "Code Snippet" entry (file+headline "~/src/200ok/knowledge/README.org" "Snippets") ;; Prompt for tag and language "* %?\t%^g\n#+BEGIN_SRC %^{language}\n%i\n#+END_SRC") ("S" "Shopping" entry (file+olp "~/Dropbox/org/shared_with_monika/shared_alain_and_monika.org" "@Shopping") "* TODO %?\n %U\n %i\n %a" :prepend t) ("l" "Logbook entry" entry (file+olp reference-file "Logbook" ,(format-time-string "%Y")) "* %?\n %U\n %i\n %a" :prepend t) ("L" "200ok Logbook entry" entry (file+olp "~/src/200ok/admin/THINGS.org" "Reference" "Logbook" ,(format-time-string "%Y")) "* %?\n %U\n %i\n %a" :prepend t) ;; NOTE: This would result in a cleanlier logbook, but ;; organice does not support it, yet. ;; ("l" "Logbook entry" entry (file+olp+datetree reference-file "Logbook") ;; "* %?\n %U\n %i\n %a") ("p" "password" entry (file+headline "~/Dropbox/org/vault/primary.org.gpg" "Passwords") ;; Prompt for name "* %^{name} :PROPERTIES: :username: %^{username} :password: %(generate-password-non-interactive) :url: %^{url} :END:") ("u" "URL / Media Inbox" entry (file+olp media-file "Inbox") "* %?\n%U\nURL: \n" :prepend t) ;; Legacy style ;; ("u" "URL" entry ;; (file+datetree media-file) ;; "* %?\nURL: \nEntered on %U\n") ))Ensure text from capture-templates end with a newline
If they don’t, then the result will look like:
* Tasks ** TODO Foo from capture-template* This should be on the next line
This obviously breaks the structure of the Org file. Here’s a fix:
(defun add-newline-at-end-if-none () "Add a newline at the end of the buffer if there isn't any, but skip this for table-type captures." (save-excursion (save-restriction ;; First check if this is a table capture (let* ((template (org-capture-get :template)) (type (org-capture-get :type))) ;; Only add newline if not a table-type capture (unless (member type '(table table-line)) (goto-char (1- (point-max))) (if (not (looking-at "\n\n")) (progn (goto-char (point-max)) (insert "\n")))))))) ;; NOTE: I think this might have been fixed upstream at some point. ;; For the time being, I'll leave the code, but remove the hook. ;; (add-hook 'org-capture-before-finalize-hook 'add-newline-at-end-if-none)
Enable the <s TAB
syntax for structure templates.
(if (version<= "27.1" emacs-version) (require 'org-tempo))
A lightweight implementation of the Pomodoro Technique is implemented through customizing Org mode. These are the commands:
ok-pomodoro-start
ok-pomodoro-cancel
ok-pomodoro-break
ok-pomodoro-reset
: Reset the completed and cancelled counters.It sets the following stats:
ok-pomodoro-completed
ok-pomodoro-cancelled
Alternatively, if you do not want to manually start a pomodoro, you can hook into the Org mode clocking mechanism. When ok-pomodoro-auto-clock-in
is set, for every Clock that is started (C-c C-x C-i
) an automatic Timer is scheduled to 25min. After these 25min are up, a “Time to take a break!” message is played and a pop-up notification is shown.
The timer is not automatically stopped on clocking out, because clocking in should still work on new tasks without resetting the Pomodoro.
The timer can manually be stopped with M-x org-timer-stop
.
A break can be started with M-x pomodoro-break
. A pomodoro can also manually be started without clocking in via M-x pomodoro-start
.
;; Configure primary org pomodoro buffer to which the timers will get ;; attached to. (setq ok-pomodoro-buffer "things.org") (load "~/.emacs.d/org-pomodoro")
I use two workflow sets:
Additionally I sometimes use the keywords PROJECT and AGENDA to denote special bullets that I might tag (schedule/deadline) in the agenda. These keywords give semantics to those bullets.
Note that “|” denotes a semantic state change that is reflected in a different color. Putting the pipe at the end means that all states prior should be shown in the same color.
(setq org-todo-keywords '((sequence "TODO" "|" "DONE") (sequence "PROJECT" "AGENDA" "|" "MINUTES") (sequence "WAITING" "|" "PROGRESS")))
When using a clock table, org will by default sum up the time in perfectly human readable terms like this:
Headline Time Total time 1d 1:03For easy calculations (I don’t want to parse our hours, weeks and what not), I do prefer that the summation is done only in fractional hours. org-duration-format
is very powerful, the help is helpful to understand the syntax and options.
(setq org-duration-format '(("h" . t) (special . 2)))
This will render the same time as above as:
Headline Time Total time 25.05(load-library "find-lisp") (defun set-org-agenda-files () "Set different org-files to be used in `org-agenda`." (setq org-agenda-files (flatten-list (list (concat org-directory "things.org") (concat org-directory "reference.org") (concat org-directory "media.org") (concat org-directory "shared_with_monika/shared_alain_and_monika.org") "~/src/200ok/admin/ACTIVE_THINGS.org" ;; "~/src/200ok/admin/THINGS.org" (when (file-directory-p "~/src/200ok/admin/pm/") (find-lisp-find-files "~/src/200ok/admin/pm/" "^pm.*\.org$")))))) (set-org-agenda-files) (global-set-key "\C-cl" 'org-store-link) (defun things () "Open main 'org-mode' file and start 'org-agenda' for today." (interactive) (find-file (concat org-directory "things.org")) (set-org-agenda-files) (org-agenda-list) (org-agenda-day-view) (shrink-window-if-larger-than-buffer) (other-window 1))
For a proficient GTD workflow, it is important to be able to refile one item from one list easily to another (for example when processing an inbox). Orgmode makes this easy with the refile command C-c C-w
.
Define where the refiling can happen (the default is to the local buffer):
(setq org-refile-targets (quote ((nil :maxlevel . 9) ;; local buffer (org-agenda-files :maxlevel . 4))))Show “calendar week” in calendar
(setq calendar-week-start-day 1) (setq calendar-intermonth-text '(propertize (format "%2d" (car (calendar-iso-from-absolute (calendar-absolute-from-gregorian (list month day year))))) 'font-lock-face 'font-lock-warning-face)) (setq calendar-intermonth-header (propertize "CW" 'font-lock-face 'font-lock-keyword-face))Hide empty lines between sub-headers in collapsed view
(setq org-cycle-separator-lines 0)
https://github.com/pashky/restclient.el
HTTP REST client tool for emacs
Integration into Org modehttps://github.com/alf/ob-restclient.el
An extension to restclient.el for emacs that provides org-babel support.
(org-babel-do-load-languages 'org-babel-load-languages '((restclient . t)))
https://orgmode.org/manual/Tracking-your-habits.html
Show habits outside of today’s agenda view:
(setq org-habit-show-habits-only-for-today nil)
Bring back ‘clear cell’ shortcut which used to be the default binding, but was removed in the 9.5 release.
(define-key org-mode-map (kbd "C-c SPC") #'org-table-blank-field)
Store a link to a file when attaching it. So after adding an attachment you can just use C-c C-l
to insert the link.
(setq org-attach-store-link-p 'attached)
(eval-after-load "org" '(require 'ox-md nil t))Better unfold performance
https://www.reddit.com/r/orgmode/comments/1drz332/anyone_else_occasionally_experience_being_unable/
Sometimes/quite often, hitting TAB just won’t unfold a heading.
(setq org-fold-core-style 'overlays)Show only the next ‘n’ subheaders
Something similar to `org-narrow-to-block`, but for a dynamic number of elements. This is useful when looking at a ‘Next’ entry in a GTD file and not wanting to be overwhelmed by the whole list, for example.
(defun org-narrow-to-next-n-subheaders (n) "Narrow the buffer to the next N subheaders." (interactive "nNumber of subheaders: ") (save-excursion (org-back-to-heading t) (let ((start (point)) end) (forward-line) (while (and (> n 0) (not (eobp))) (when (org-at-heading-p) (setq n (1- n))) (forward-line)) (setq end (point)) (narrow-to-region start end))))WIP Notifications / appointment reminders
This is a work-in-progress, but already working.
Important commands are:
M-x appt-check
: At any point, re-display current appointment remindersM-x apt-delete
: Delete obsolete appointment reminders
https://orgmode.org/worg/org-faq.html#automatic-reminders
https://orgmode.org/list/13222.1201471868@gamaville.dokosmarshall.org/
;; Show first notification 2h before event (setq appt-message-warning-time (* 60 2)) ;; Then, have a reminder every 30min (setq appt-display-interval 30) ;; Don't display the 'time to appointment in minutes' in the modeline (setq appt-display-mode-line nil)
Option 1
;; ; Use appointment data from org-mode ;; (defun my-org-agenda-to-appt () ;; (interactive) ;; (setq appt-time-msg-list nil) ;; (org-agenda-to-appt)) ;; ; Update alarms when... ;; ; (1) ... Starting Emacs ;; (my-org-agenda-to-appt) ;; ; (2) ... Everyday at 12:05am (useful in case you keep Emacs always on) ;; (run-at-time "12:05am" (* 24 3600) 'my-org-agenda-to-appt) ;; ; (3) ... When TODO.txt is saved ;; (add-hook 'after-save-hook ;; '(lambda () ;; (if (string= (buffer-file-name) (concat (getenv "HOME") "/Dropbox/org/things.org")) ;; (my-org-agenda-to-appt)))) ;; ; Display appointments as a window manager notification ;; (setq appt-disp-window-function 'my-appt-display) ;; (setq appt-delete-window-function (lambda () t)) ;; (setq my-appt-notification-app (concat (getenv "HOME") "/bin/appt-notification")) ;; (defun my-appt-display (min-to-app new-time msg) ;; (if (atom min-to-app) ;; (start-process "my-appt-notification-app" nil my-appt-notification-app min-to-app msg) ;; (dolist (i (number-sequence 0 (1- (length min-to-app)))) ;; (start-process "my-appt-notification-app" nil my-appt-notification-app (nth i min-to-app) (nth i msg)))))
Alternative:
(defadvice org-agenda-redo (after org-agenda-redo-add-appts) "Pressing `r' on the agenda will also add appointments." (progn (defvar appt-time-msg-list nil) (org-agenda-to-appt))) (ad-activate 'org-agenda-redo) (progn (appt-activate 1) (setq appt-display-format 'window) (setq appt-disp-window-function (function my-appt-disp-window)) (defun my-appt-disp-window (min-to-app new-time msg) (call-process (concat (getenv "HOME") "/bin/appt-notification") nil 0 nil min-to-app msg new-time))) (add-hook 'after-save-hook '(lambda () (when (seq-contains org-agenda-files (s-replace "/home/munen" "~" (buffer-file-name))) (org-agenda-to-appt))))
https://github.com/karthink/gptel
Temporarily use my patch:
(add-to-list 'load-path "~/src/gptel-clone") (require 'gptel) (setq gptel-log-level 'info)
(setq gptel-default-mode 'org-mode) (setq gptel-track-media t) (with-eval-after-load 'gptel ;; The chats can have long lines. (add-hook 'gptel-mode-hook 'visual-line-mode) ;; And can be pages long. (add-hook 'gptel-post-stream-hook 'gptel-auto-scroll) (add-hook 'gptel-post-response-functions 'gptel-end-of-response)) (gptel-make-ollama "Ollama" :host "localhost:11434" :stream t :models '("codellama")) (global-set-key (kbd "C-c g m") 'gptel-menu) (global-set-key (kbd "C-c g c") 'gptel) (global-set-key (kbd "C-c g s") 'gptel-send) (global-set-key (kbd "C-c g a") 'gptel-add) (global-set-key (kbd "C-c g A") 'gptel-abort) (setq gptel-cache '(message system tool)) (setq gptel-use-tools t)
(gptel-make-tool :function (lambda (buffer) (with-temp-message (format "Reading buffer: %s" buffer) (condition-case err (if (buffer-live-p (get-buffer buffer)) (with-current-buffer buffer (buffer-substring-no-properties (point-min) (point-max))) (format "Error: buffer %s is not live." buffer)) (error (format "Error reading buffer %s: %s" buffer (error-message-string err)))))) :name "read_buffer" :description "Return the contents of an Emacs buffer" :args (list '(:name "buffer" :type string :description "The name of the buffer whose contents are to be retrieved")) :category "emacs" :include t) (gptel-make-tool :function (lambda (buffer text) (with-temp-message (format "Appending to buffer: %s" buffer) (condition-case err (if (buffer-live-p (get-buffer buffer)) (with-current-buffer buffer (goto-char (point-max)) (insert text) (format "Successfully appended text to buffer %s." buffer)) (format "Error: buffer %s is not live or does not exist." buffer)) (error (format "Error appending to buffer %s: %s" buffer (error-message-string err)))))) :name "append_to_buffer" :description "Append the given text to the end of an Emacs buffer. Returns a success or error message." :args (list '(:name "buffer" :type string :description "The name of the buffer to append to.") '(:name "text" :type string :description "The text to append to the buffer.")) :category "emacs" :include t) (defun gptel-read-documentation (symbol) "Read the documentation for SYMBOL, which can be a function or variable." (with-temp-message (format "Reading documentation for: %s" symbol) (condition-case err (let ((sym (intern symbol))) (cond ((fboundp sym) (documentation sym)) ((boundp sym) (documentation-property sym 'variable-documentation)) (t (format "No documentation found for %s" symbol)))) (error (format "Error reading documentation for %s: %s" symbol (error-message-string err)))))) (gptel-make-tool :name "read_documentation" :function #'gptel-read-documentation :description "Read the documentation for a given function or variable" :args (list '(:name "name" :type string :description "The name of the function or variable whose documentation is to be retrieved")) :category "emacs" :include t) (gptel-make-tool :function (lambda (text) (with-temp-message (format "Sending message: %s" text) (message "%s" text) (format "Message sent: %s" text))) :name "echo_message" :description "Send a message to the *Messages* buffer" :args (list '(:name "text" :type string :description "The text to send to the messages buffer")) :category "emacs" :include t) (gptel-make-tool :function (lambda (buffer_name content) (with-temp-message (format "Replacing buffer contents: `%s`" buffer_name) (if (get-buffer buffer_name) (with-current-buffer buffer_name (erase-buffer) (insert content) (format "Buffer contents replaced: %s" buffer_name)) (format "Error: Buffer '%s' not found" buffer_name)))) :name "replace_buffer" :description "Completely overwrites buffer contents with the provided content." :args (list '(:name "buffer_name" :type string :description "The name of the buffer whose contents will be replaced.") '(:name "content" :type string :description "The new content to write to the buffer, replacing all existing content.")) :category "emacs" :include t) (gptel-make-tool :function (lambda (path filename content) (with-temp-message (format "Creating file: %s in %s" filename path) (condition-case err (let ((full-path (expand-file-name filename path))) (with-temp-buffer (insert content) (write-file full-path)) (format "Created file %s in %s" filename path)) (error (format "Error creating file %s in %s: %s" filename path (error-message-string err)))))) :name "create_file" :description "Create a new file with the specified content" :args (list '(:name "path" :type string :description "The directory where to create the file") '(:name "filename" :type string :description "The name of the file to create") '(:name "content" :type string :description "The content to write to the file")) :category "filesystem" :include t) (gptel-make-tool :function (lambda (parent name) (with-temp-message (format "Creating directory: %s in %s" name parent) (condition-case err (progn (make-directory (expand-file-name name parent) t) (format "Directory %s created/verified in %s" name parent)) (error (format "Error creating directory %s in %s: %s" name parent (error-message-string err)))))) :name "make_directory" :description "Create a new directory with the given name in the specified parent directory" :args (list '(:name "parent" :type string :description "The parent directory where the new directory should be created, e.g. /tmp") '(:name "name" :type string :description "The name of the new directory to create, e.g. testdir")) :category "filesystem" :include t) (gptel-make-tool :function (lambda (file_path new_content) (with-temp-message (format "Replacing content in file: `%s`" file_path) (let ((full-path (expand-file-name file_path))) (with-temp-file full-path (insert new_content)) (format "Successfully replaced content in %s" full-path)))) :name "replace_file_contents" :description "Replaces the entire content of a file. Use this tool ONLY as a last resort if both 'edit_file' and 'apply_diff' fail. It is highly token-inefficient as it requires sending the full file content. WARNING: This operation completely overwrites the target file." :args (list '(:name "file_path" :type string :description "The path to the file that needs to be replaced.") '(:name "new_content" :type string :description "The new content for the file.")) :category "filesystem" :include t) (gptel-make-tool :function (lambda (command &optional working_dir) (with-temp-message (format "Executing command: `%s`" command) (let ((default-directory (if (and working_dir (not (string= working_dir ""))) (expand-file-name working_dir) default-directory))) (shell-command-to-string command)))) :name "run_command" :description (concat "Executes a shell command and returns the output as a string. IMPORTANT: This tool allows execution of arbitrary code." "Installed commandline tools: coreutils, git, patch, findutils, the-silver-searcher curl" "NOTE: You can use a combination of `find` and `the-silver-searcher` to find your way around a codebase") :args (list '(:name "command" :type string :description "The complete shell command to execute.") '(:name "working_dir" :type string :description "Optional: The directory in which to run the command. Defaults to the current directory if not specified.")) :category "command" :include t) (gptel-make-tool :function (lambda (query) (with-temp-message (format "Searching for: `%s`" query) (let ((url (format "https://%s:%s@search.twohundredok.com/search?q=%s&format=json" munen-gptel--twohundredok-user munen-gptel--twohundredok-password (url-hexify-string query)))) (with-temp-buffer (url-insert-file-contents url) (let ((json-response (json-read))) (mapconcat (lambda (result) (format "%s - %s\n%s" (cdr (assoc 'title result)) (cdr (assoc 'url result)) (cdr (assoc 'content result)))) (cdr (assoc 'results json-response)) "\n\n")))))) :name "search_web" :description "Searches the web using SearXNG metasearch engine and returns formatted results including titles, URLs, and content excerpts." :args (list '(:name "query" :type string :description "The search query to execute against the search engine.")) :category "web" :include t) (defun munen-gptel--trafilatura-fetch-url (url) "Fetch content from URL using trafilatura and return it as a string." (with-temp-message (format "Fetching content from: %s" url) (with-temp-buffer (call-process "trafilatura" nil t nil "--output-format=markdown" "--with-metadata" "-u" url) (buffer-string)))) (gptel-make-tool :name "TrafilaturaFetch" :function #'munen-gptel--trafilatura-fetch-url :description "Fetch content from a URL using trafilatura, which extracts main content and metadata while removing boilerplate, navigation and ads." :args '((:name "url" :type string :description "URL to fetch content from")) :category "web" :include t)
(defun munen-gptel--edit-file (file-path &optional file-edits) "Edit FILE-PATH by applying FILE-EDITS using fuzzy string matching (non-interactive). This function directly modifies the file on disk without user confirmation. Each edit in FILE-EDITS should specify: - :old_string - The string to find and replace (fuzzy matched) - :new_string - The replacement string EDITING RULES: - The old_string is matched using fuzzy search throughout the file - If multiple matches exist, only the first occurrence is replaced - Include enough context in old_string to uniquely identify the location - Whitespace differences are normalized during matching - Keep edits focused on the specific change requested Returns a success/failure message indicating whether edits were applied." (if (and file-path (not (string= file-path "")) file-edits) (with-current-buffer (get-buffer-create "*edit-file*") (erase-buffer) (insert-file-contents (expand-file-name file-path)) (let ((inhibit-read-only t) (target-file-name (expand-file-name file-path)) (edit-success nil) (applied-edits 0) (total-edits (length file-edits))) ;; Apply changes (dolist (file-edit (seq-into file-edits 'list)) (when-let ((old-string (plist-get file-edit :old_string)) (new-string (plist-get file-edit :new_string)) (is-valid-old-string (not (string= old-string "")))) (goto-char (point-min)) ;; Try exact match first (if (search-forward old-string nil t) (progn (replace-match new-string t t) (setq edit-success t) (setq applied-edits (1+ applied-edits))) ;; If exact match fails, try fuzzy match (goto-char (point-min)) (when (munen-gptel--fuzzy-search old-string) (replace-match new-string t t) (setq edit-success t) (setq applied-edits (1+ applied-edits)))))) ;; Return result (if edit-success (progn (write-file target-file-name nil) (kill-buffer (current-buffer)) (format "Successfully edited and saved %s (%d/%d edits applied)" target-file-name applied-edits total-edits)) (progn (kill-buffer (current-buffer)) (format "Failed to apply edits to %s. No matching strings found." target-file-name))))) (format "Failed to edit %s (invalid path or no edits provided)." file-path))) (defun munen-gptel--normalize-whitespace (string) "Normalize whitespace in STRING for fuzzy matching. Converts multiple whitespace characters to single spaces and trims." (string-trim (replace-regexp-in-string "[ \t\n\r]+" " " string))) (defun munen-gptel--fuzzy-search (target-string) "Search for TARGET-STRING using fuzzy matching. Returns t if found and positions point after the match, nil otherwise. Tries multiple matching strategies in order of preference." (let ((normalized-target (munen-gptel--normalize-whitespace target-string)) (case-fold-search nil)) (or ;; Strategy 1: Normalized whitespace matching (progn (goto-char (point-min)) (let ((found nil)) (while (and (not found) (not (eobp))) (let* ((line-start (line-beginning-position)) (line-end (line-end-position)) (line-text (buffer-substring-no-properties line-start line-end)) (normalized-line (munen-gptel--normalize-whitespace line-text))) (when (string-match-p (regexp-quote normalized-target) normalized-line) ;; Found a match, now find the actual position in the original text (goto-char line-start) (when (re-search-forward (munen-gptel--create-fuzzy-regex target-string) line-end t) (setq found t))) (unless found (forward-line 1)))) found)) ;; Strategy 2: Case-insensitive search (progn (goto-char (point-min)) (let ((case-fold-search t)) (search-forward target-string nil t))) ;; Strategy 3: Regex-based flexible matching (progn (goto-char (point-min)) (re-search-forward (munen-gptel--create-flexible-regex target-string) nil t))))) (defun munen-gptel--create-fuzzy-regex (string) "Create a regex pattern for fuzzy matching STRING. Allows flexible whitespace matching between words." (let ((escaped (regexp-quote string))) ;; Replace escaped whitespace with flexible whitespace pattern (replace-regexp-in-string "\\\\[ \t\n\r]+" "[ \t\n\r]+" escaped))) (defun munen-gptel--create-flexible-regex (string) "Create a very flexible regex pattern for STRING. Allows optional whitespace between characters and words." (let* ((chars (string-to-list string)) (pattern-parts '())) (dolist (char chars) (cond ((memq char '(?\s ?\t ?\n ?\r)) ;; For whitespace, allow flexible matching (push "[ \t\n\r]*" pattern-parts)) (t ;; For regular characters, escape and add optional whitespace after (push (concat (regexp-quote (char-to-string char)) "[ \t\n\r]*") pattern-parts)))) (concat "\\(" (string-join (reverse pattern-parts) "") "\\)"))) (gptel-make-tool :function #'munen-gptel--edit-file :name "edit_file" :description "Edits a file by applying fuzzy string matching changes. This is a primary method for file modification. If this tool fails, try 'apply_diff'. As a last resort, use 'replace_file_contents'. This tool modifies the file directly on disk without user confirmation. Each edit requires an old string to find (fuzzy matched) and a new string to replace it with." :args (list '(:name "file-path" :type string :description "The full path of the file to edit.") '(:name "file-edits" :type array :items (:type object :properties (:old_string (:type string :description "The exact string to be replaced (will be fuzzy matched if exact match fails).") :new_string (:type string :description "The new string to replace old_string."))) :description "A list of edits to apply to the file. Each edit must contain old_string and new_string.")) :category "filesystem" :include t)
(defun munen-gptel--edit-file-interactive (file-path file-edits) "Edit FILE-PATH by applying FILE-EDITS with interactive review using ediff. This function applies the specified edits to the file and then opens an ediff session to review changes before saving. Each edit in FILE-EDITS should specify: - :line_number - The 1-based line number where the edit occurs - :old_string - The exact string to find and replace - :new_string - The replacement string EDITING RULES: - The old_string must EXACTLY MATCH the existing file content at the specified line - Include enough context in old_string to uniquely identify the location - Keep edits concise and focused on the specific change requested - Do not include long runs of unchanged lines After applying edits, opens ediff to compare original vs modified versions, allowing user to review and selectively apply changes before saving. Returns a success/failure message indicating whether edits were applied." (if (and file-path (not (string= file-path "")) file-edits) (with-current-buffer (get-buffer-create "*edit-file*") (erase-buffer) (insert-file-contents (expand-file-name file-path)) (let ((inhibit-read-only t) (case-fold-search nil) (file-name (expand-file-name file-path)) (edit-success nil)) ;; apply changes (dolist (file-edit (seq-into file-edits 'list)) (when-let ((line-number (plist-get file-edit :line_number)) (old-string (plist-get file-edit :old_string)) (new-string (plist-get file-edit :new_string)) (is-valid-old-string (not (string= old-string "")))) (goto-char (point-min)) (forward-line (1- line-number)) (when (search-forward old-string nil t) (replace-match new-string t t) (setq edit-success t)))) ;; return result to gptel (if edit-success (progn ;; show diffs (ediff-buffers (find-file-noselect file-name) (current-buffer)) (format "Successfully edited %s" file-name)) (format "Failed to edited %s" file-name)))) (format "Failed to edited %s" file-path))) (gptel-make-tool :function #'munen-gptel--edit-file-interactive :name "edit_file_interactive" :description "Edit a file interactively by applying a list of edits with review via ediff. This tool applies the specified edits and opens an ediff session for review. Each edit specifies a line number, old string to find, and new string replacement. After applying edits, ediff opens to compare original vs modified versions, allowing interactive review and selective application of changes before saving. This provides a safe way to review changes before committing them to disk." :args (list '(:name "file-path" :type string :description "The full path of the file to edit") '(:name "file-edits" :type array :items (:type object :properties (:line_number (:type integer :description "The line number of the file where edit starts.") :old_string (:type string :description "The old-string to be replaced.") :new_string (:type string :description "The new-string to replace old-string."))) :description "The list of edits to apply on the file")) :category "emacs")
(gptel-make-tool :name "apply_diff" :description (concat "Applies a diff (patch) to a file. This is a primary method for file modification. " "If this tool fails, try 'edit_file'. As a last resort, use 'replace_file_contents'. " "The diff must be in the unified format ('diff -u'). " "Ensure file paths in the diff (e.g., '--- a/file', '+++ b/file') match the 'file_path' argument and 'patch_options'. " "Common 'patch_options' include: '' (for exact/relative paths), " "'-p0' (if diff paths are full), '-p1' (to strip one leading directory). " "Default options are '-N' (ignore already applied patches).") :args (list '(:name "file_path" :type string :description "The path to the file that needs to be patched.") '(:name "diff_content" :type string :description "The diff content in unified format (e.g., from 'diff -u').") '(:name "patch_options" :type string :optional t :description "Optional: Additional options for the 'patch' command (e.g., '-p1', '-p0', '-R'). Defaults to '-N'. Prepend other options if needed, e.g., '-p1 -N'.") '(:name "working_dir" :type string :optional t :description "Optional: The directory in which to interpret file_path and run patch. Defaults to the current buffer's directory if not specified.")) :category "filesystem" :function (lambda (file_path diff_content &optional patch_options working_dir) (let ((original-default-directory default-directory) (user-patch-options (if (and patch_options (not (string-empty-p patch_options))) (split-string patch_options " " t) nil)) ;; Combine user options with -N, ensuring -N is there. ;; If user provides -N or --forward, use their version. Otherwise, add -N. (base-options '("-N")) (effective-patch-options '())) (if user-patch-options (if (or (member "-N" user-patch-options) (member "--forward" user-patch-options)) (setq effective-patch-options user-patch-options) (setq effective-patch-options (append user-patch-options base-options))) (setq effective-patch-options base-options)) (let* ((out-buf-name (generate-new-buffer-name "*patch-stdout*")) (err-buf-name (generate-new-buffer-name "*patch-stderr*")) (target-file nil) (exit-status -1) ; Initialize to a known non-zero value (result-output "") (result-error "")) (unwind-protect (progn (when (and working_dir (not (string-empty-p working_dir))) (setq default-directory (expand-file-name working_dir))) (setq target-file (expand-file-name file_path)) (unless (file-exists-p target-file) ;; Use error to signal failure, which gptel should catch. (error "File to patch does not exist: %s" target-file)) (with-temp-message (format "Applying diff to: `%s` with options: %s" target-file effective-patch-options) (with-temp-buffer (insert diff_content) (unless (eq (char-before (point-max)) ?\n) (goto-char (point-max)) (insert "\n")) ;; Pass buffer *names* to call-process-region (setq exit-status (apply #'call-process-region (point-min) (point-max) "patch" ; Command nil ; delete region (no) (list out-buf-name err-buf-name) ; stdout/stderr buffer names nil ; display (no) (append effective-patch-options (list target-file)))))) ;; Retrieve content from buffers using their names (let ((stdout-buf (get-buffer out-buf-name)) (stderr-buf (get-buffer err-buf-name))) (when stdout-buf (with-current-buffer stdout-buf (setq result-output (buffer-string)))) (when stderr-buf (with-current-buffer stderr-buf (setq result-error (buffer-string))))) (if (= exit-status 0) (format "Diff successfully applied to %s.\nPatch command options: %s\nPatch STDOUT:\n%s\nPatch STDERR:\n%s" target-file effective-patch-options result-output result-error) ;; Signal an Elisp error, which gptel will catch and display. ;; The arguments to 'error' become the error message. (error "Failed to apply diff to %s (exit status %s).\nPatch command options: %s\nPatch STDOUT:\n%s\nPatch STDERR:\n%s" target-file exit-status effective-patch-options result-output result-error))) ;; Cleanup clause of unwind-protect (setq default-directory original-default-directory) (let ((stdout-buf-obj (get-buffer out-buf-name)) (stderr-buf-obj (get-buffer err-buf-name))) (when (buffer-live-p stdout-buf-obj) (kill-buffer stdout-buf-obj)) (when (buffer-live-p stderr-buf-obj) (kill-buffer stderr-buf-obj))))))) :include t)
This tool emulates tree
behavior, but it adheres to .gitignore
and it’s written entirely in elisp, so it is platform independent.
(defun gptel--normalize-max-depth (max-depth) "Convert MAX-DEPTH to a number, handling strings, numbers, or nil. Returns 3 as default if MAX-DEPTH is nil or invalid." (cond ;; Already a number ((numberp max-depth) max-depth) ;; String that can be converted to number ((and (stringp max-depth) (not (string-empty-p max-depth)) (string-match-p "^[0-9]+$" max-depth)) (string-to-number max-depth)) ;; Default case (nil, empty string, or invalid input) (t 3))) (defun gptel--parse-gitignore (gitignore-file) "Parse a .gitignore file and return a list of patterns." (when (file-exists-p gitignore-file) (with-temp-buffer (insert-file-contents gitignore-file) (let ((patterns '())) (goto-char (point-min)) (while (not (eobp)) (let ((line (string-trim (buffer-substring-no-properties (line-beginning-position) (line-end-position))))) (unless (or (string-empty-p line) (string-prefix-p "#" line)) (push line patterns))) (forward-line 1)) (nreverse patterns))))) (defun gptel--should-ignore-p (file-path gitignore-patterns) "Check if FILE-PATH should be ignored based on GITIGNORE-PATTERNS." (let ((relative-path (file-name-nondirectory file-path))) (cl-some (lambda (pattern) (cond ;; Directory pattern (ends with /) ((string-suffix-p "/" pattern) (and (file-directory-p file-path) (string-match-p (concat "^" (regexp-quote (string-remove-suffix "/" pattern)) "$") relative-path))) ;; Exact match ((not (string-match-p "[*?]" pattern)) (string= relative-path pattern)) ;; Wildcard pattern (t (string-match-p (concat "^" (replace-regexp-in-string "\\*" ".*" (regexp-quote pattern)) "$") relative-path)))) gitignore-patterns))) (defun gptel--collect-gitignore-patterns (directory) "Collect all .gitignore patterns from DIRECTORY and parent directories." (let ((patterns '()) (current-dir (expand-file-name directory))) (while (and current-dir (not (string= current-dir "/"))) (let ((gitignore-file (expand-file-name ".gitignore" current-dir))) (when (file-exists-p gitignore-file) (setq patterns (append (gptel--parse-gitignore gitignore-file) patterns)))) (let ((parent (file-name-directory (directory-file-name current-dir)))) (setq current-dir (if (string= parent current-dir) nil parent)))) ;; Add common ignore patterns (append patterns '(".git" ".DS_Store" "node_modules" "__pycache__" "*.pyc")))) (defun gptel--directory-tree (directory max-depth show-hidden) "Generate a tree representation of DIRECTORY." (let ((expanded-dir (expand-file-name directory))) (concat (abbreviate-file-name expanded-dir) "\n" (gptel--directory-tree-recursive expanded-dir max-depth 0 show-hidden "")))) (defun gptel--directory-tree-recursive (directory max-depth current-depth show-hidden prefix) "Internal recursive function for generating directory tree." (if (>= current-depth max-depth) "" (let* ((expanded-dir (expand-file-name directory)) (gitignore-patterns (gptel--collect-gitignore-patterns expanded-dir)) (entries (condition-case nil (directory-files expanded-dir t "^[^.]" t) (error nil))) (filtered-entries '()) (result "")) ;; Add hidden files if requested (when show-hidden (setq entries (append entries (directory-files expanded-dir t "^\\.[^.]" t)))) ;; Filter out ignored files (dolist (entry entries) (unless (gptel--should-ignore-p entry gitignore-patterns) (push entry filtered-entries))) (setq filtered-entries (sort filtered-entries #'string<)) ;; Generate tree output (let ((total (length filtered-entries))) (dotimes (i total) (let* ((entry (nth i filtered-entries)) (basename (file-name-nondirectory entry)) (is-last (= i (1- total))) (is-dir (file-directory-p entry)) (connector (if is-last "└── " "├── ")) (new-prefix (concat prefix (if is-last " " "│ ")))) (setq result (concat result prefix connector basename (if is-dir "/" "") "\n")) ;; Recurse into directories (when (and is-dir (< (1+ current-depth) max-depth)) (setq result (concat result (gptel--directory-tree-recursive entry max-depth (1+ current-depth) show-hidden new-prefix))))))) result))) (gptel-make-tool :function (lambda (directory &optional max-depth show-hidden) (with-temp-message (format "Listing directory tree: %s" directory) (condition-case err (let ((max-depth (gptel--normalize-max-depth max-depth)) (show-hidden (and show-hidden (not (string= show-hidden ""))))) (gptel--directory-tree directory max-depth show-hidden)) (error (format "Error listing directory: %s - %s" directory (error-message-string err)))))) :name "list_directory" :description "List the contents of a directory in a tree format, respecting .gitignore files" :args (list '(:name "directory" :type string :description "The path to the directory to list") '(:name "max-depth" :type string :description "Optional: Maximum depth to traverse (default: 3)") '(:name "show-hidden" :type string :description "Optional: Show hidden files/directories (default: false)")) :category "filesystem" :include t)
(gptel-make-tool :function (lambda (filepath) (let ((ignore-patterns (when (boundp 'gptel-read-file-ignore-patterns) gptel-read-file-ignore-patterns)) (expanded-path (expand-file-name filepath))) (if (and ignore-patterns (cl-some (lambda (pattern) (string-match-p pattern expanded-path)) ignore-patterns)) (format "Access denied: File %s matches ignore patterns" filepath) (with-temp-message (format "Reading file: %s" filepath) (condition-case err (with-temp-buffer (insert-file-contents expanded-path) (buffer-string)) (error (format "Error reading file: %s - %s" filepath (error-message-string err)))))))) :name "read_file" :description "Read and display the contents of a file. Note: If a file is already included in the current gptel context (conversation), there is no need to read it again as the context is always current." :args (list '(:name "filepath" :type string :description "Path to the file to read. Supports relative paths and ~. Only use this tool for files not already in the conversation context.")) :category "filesystem" :include t)gptel-read-file-ignore-patterns
A list of patterns that prevent the read_file
tool from accessing certain files.
(setq gptel-read-file-ignore-patterns '("*.secret" "config/*"))
read_file
returns “Access denied” instead of file contents.dir-locals.el
for project-specific configuration(gptel-make-tool :name "file_lint_with_flycheck" :description (concat "Lints the specified file using Flycheck in Emacs, returning any errors or warnings " "(or a 'no errors found' message).\n\n" "**LLM Workflow for Code Modification:**\n" "1. **Baseline Check:** Run this tool on the file *before* generating a patch or " "other code modifications. This helps understand the existing lint status and " "avoid re-introducing pre-existing issues or being blamed for them.\n" "2. **Verification Check:** After your changes have been applied to the file " "(e.g., via a patch), run this tool again.\n" "3. **Self-Correction:** Compare the lint output from step 2 (after your changes) " "with the baseline from step 1 (before your changes). If your modifications " "introduced *new* lint errors, you are expected to refactor your code to fix " "these new errors. Re-run this lint tool to confirm your fixes before " "considering the task complete. Focus on fixing errors introduced by your changes.") :args (list '(:name "filename" :type "string" :description "The path (relative or absolute) to the file to be checked.")) :category "emacs" :include t :function (lambda (filename) (unless (require 'flycheck nil t) (error "Flycheck package is not available.")) (unless (stringp filename) (error "Filename argument must be a string.")) (let ((original-filename filename)) (condition-case err (let* ((absolute-filename (expand-file-name filename)) (buffer-object (get-file-buffer absolute-filename)) (temp-buffer-created (not buffer-object)) (buffer (or buffer-object (progn (unless (file-exists-p absolute-filename) (error "File not found: %s (expanded from %s)" absolute-filename original-filename)) (find-file-noselect absolute-filename)))) (flycheck-was-on-in-buffer nil) (errors-string "Error: Failed to collect Flycheck results.")) ; Default error (unless (buffer-live-p buffer) (error "Could not open or find buffer for file: %s" absolute-filename)) (with-temp-message (format "Linting %s with Flycheck..." absolute-filename) (unwind-protect (progn ;; Main work block (with-current-buffer buffer (setq flycheck-was-on-in-buffer flycheck-mode) (unless flycheck-mode (flycheck-mode 1) (unless flycheck-mode (error "Failed to enable Flycheck mode in buffer %s." (buffer-name)))) (flycheck-buffer) ;; Request a syntax check (let ((timeout 15.0) (start-time (float-time))) (while (and (flycheck-running-p) (< (- (float-time) start-time) timeout)) (sit-for 0.1 t))) ;; Wait, process events ;; Check reason for loop termination & collect errors (if (flycheck-running-p) (error "Flycheck timed out after %.0f seconds for %s" timeout absolute-filename) ;; Flycheck is no longer running, collect errors (progn ;; flycheck-current-errors is a VARIABLE (let ((current-errors flycheck-current-errors)) (if current-errors (setq errors-string (format "Flycheck results for %s:\n%s" absolute-filename (mapconcat (lambda (err-obj) ;; err-obj is a flycheck-error struct (format "- %S at L%s%s (%s): %s" (flycheck-error-level err-obj) (flycheck-error-line err-obj) (if-let ((col (flycheck-error-column err-obj))) ; CORRECTED (format ":C%s" col) "") (flycheck-error-checker err-obj) (flycheck-error-message err-obj))) current-errors "\n"))) (setq errors-string (format "No Flycheck errors found in %s." absolute-filename))))))) ;; errors-string is now set based on Flycheck output ) ;; End of with-current-buffer ;; Cleanup block for unwind-protect (progn (when (buffer-live-p buffer) (with-current-buffer buffer (when (and flycheck-mode (not flycheck-was-on-in-buffer)) (flycheck-mode 0)))) (when (and temp-buffer-created (buffer-live-p buffer)) (kill-buffer buffer))))) errors-string) (error (format "Error linting file %s: %s" original-filename (error-message-string err)))))))
(gptel-make-tool :function (lambda (code host port working_dir) (let* ((eff-host (if (or (null host) (string= host "")) "localhost" host)) (eff-port (if (or (null port) (and (integerp port) (= port 0))) 7888 port)) (eff-working-dir (let ((base-dir (or default-directory "."))) (if (or (null working_dir) (string= working_dir "")) base-dir (expand-file-name working_dir base-dir)))) (shell-command (format "echo \"%s\" | lein repl :connect %s:%s" code eff-host eff-port))) (with-temp-message (format "Executing: %s" shell-command) (let ((default-directory eff-working-dir)) ; Temporarily set default-directory (shell-command-to-string shell-command))))) :name "run_clojure_in_repl" :description (concat "Executes arbitrary Clojure code in a connected Leiningen nREPL session and returns the output. " "This tool constructs and executes a shell command like 'echo \"<your_code_arg>\" | lein repl :connect host:port'.\n\n" "IMPORTANT LLM USAGE INSTRUCTIONS FOR THE 'code' ARGUMENT:\n" "1. The 'code' argument string you provide will be wrapped *directly* in double quotes by the 'echo' command " "(e.g., the tool effectively does: echo \"<your_code_arg>\").\n" "2. Because of this, if your Clojure code contains string literals that use double quotes, YOU MUST ESCAPE THOSE " "INTERNAL DOUBLE QUOTES as '\\\\\"' within the 'code' argument string you provide. " " Example: To run the Clojure `(println \"Hello \"World\"!\")`, you must provide the 'code' argument as " " \'\'\'(println \\\\\"Hello \\\\\\\\\\\\\\\"World\\\\\\\\\\\\\\\"!\")\'\'\' (note the escaped internal quotes for the 'code' arg string).\n" "3. Single quotes (e.g., `(quote foo)` or `'foo`) and reader macros (like `#'foo/bar`) within your Clojure code " " generally do NOT need special escaping for this tool *beyond what's needed by Clojure itself*, as they will be " " protected by the outer double quotes of the 'echo' command.\n" "4. As per standard tool usage, if you wrap your 'code' argument in \'\'\'...\'\'\' for the tool call and the Clojure " " code *itself* contains a literal \'\'\' sequence, you must escape that as '\\'\\'\\''.\n\n" "This tool allows execution of arbitrary Clojure code within the REPL. Useful for evaluation, running specific test functions with fixtures, or any other REPL interaction. " "Requires a Leiningen nREPL server to be running and connectable.") :args (list '(:name "code" :type string :description "The Clojure code string to execute. CRITICAL: See main tool description for how to format and escape this string, especially internal double quotes.") '(:name "host" :type string :description "Optional: The hostname of the nREPL server. Defaults to 'localhost' if empty string or not provided.") '(:name "port" :type integer :description "Optional: The port number of the nREPL server. Defaults to 7888 if 0, empty or not provided.") '(:name "working_dir" :type string :description "Optional: The directory in which to run the 'lein repl :connect' command. Defaults to the current Emacs default-directory if empty string or not specified.")) :category "clojure" :include t)
https://github.com/MatthewZMD/aidermacs
(require 'aidermacs) (with-eval-after-load 'aidermacs (global-set-key (kbd "C-c a") 'aidermacs-transient-menu) (setq aidermacs-comint-multiline-newline-key "S-<return>") ;; Configure variables (setq aidermacs-use-architect-mode nil) ;; No need showing the diff after change (setq aidermacs-show-diff-after-change nil) (setq aidermacs-auto-accept-architect t) (add-to-list 'aidermacs-extra-args "--cache-prompts") (add-to-list 'aidermacs-extra-args "--no-gitignore") (add-to-list 'aidermacs-extra-args "--analytics-disable") ;; (setq aidermacs-default-model "sonnet") (setq aidermacs-default-model "gemini/gemini-2.5-pro") ;; (setq aidermacs-default-model "groq/meta-llama/llama-4-maverick-17b-128e-instruct") )
(require 'gptel-integrations) (use-package mcp :after gptel :custom (mcp-hub-servers `(("clj-prj" . (:command "/bin/sh" :args ("-c" "cd /home/munen/ && clojure -X:mcp :port 7890"))))) :config (require 'mcp-hub) :hook (after-init . mcp-hub-start-all-server))
https://github.com/vedang/pdf-tools, forked from https://github.com/politza/pdf-tools
PDF Tools is, among other things, a replacement of DocView for PDF files. The key difference is that pages are not pre-rendered by e.g. ghostscript and stored in the file-system, but rather created on-demand and stored in memory.
PDF Tools for me is - hands down - the best PDF viewer! It’s not an excuse to do even more within Emacs.
When using evil-mode
and pdf-tools
and looking at a zoomed PDF, it will blink, because the cursor blinks. This configuration disables this whilst retaining the blinking cursor in other modes.
(evil-set-initial-state 'pdf-view-mode 'emacs) (add-hook 'pdf-view-mode-hook (lambda () (set (make-local-variable 'evil-emacs-state-cursor) (list nil))))
Elfeed is an extensible web feed reader for Emacs, supporting both Atom and RSS.
;; (require 'elfeed) ;; (require 'elfeed-goodies) ;; (elfeed-goodies/setup)
Automatic word-wrap for elfeed entries:
;; (add-hook 'elfeed-show-mode-hook 'visual-line-mode)
Use VIM style scrolling in elfeed entries:
;; (define-key elfeed-show-mode-map (kbd "C-e") 'evil-scroll-line-down) ;; (define-key elfeed-show-mode-map (kbd "C-y") 'evil-scroll-line-up)
;; (load "~/.emacs.d/elfeed-feeds.el")Integration with browsers
Editing text areas in browsers can be quite tedious for the lack of a good editor. Luckily, there’s good extensions for both Chrome/Chromium and Firefox to have a live binding to an Emacs session.
There is a good Emacs package called Atomic Chrome which is similar to Edit with Emacs, but has some advantages as below with the help of websockets:
The name “Atomic Chrome” is a bit misleading, because it actually supports the “GhostText” protocol which allows it to be used with Firefox, as well.
On Firefox, I’m using the GhostText addon. On Chromium, I’m using the AtomicChrome extension. GhostText is also available for Chrome, but it doesn’t work for me which is a non-issue, because both plugins work just the same way: Enter a textarea, hit a button, Emacs opens up, type the text, end the session with C-c C-c
.
(require 'atomic-chrome) ;; Handle if there is an Emacs instance running which has the server already ;; started (ignore-errors ;; Start the server (atomic-chrome-start-server))
Note: I opened a PR against AtomicChrome which will make the safe-guard obsolete.
Default mode
(setq atomic-chrome-default-major-mode 'markdown-mode)
Copy to clipboard
Some websites have aggressive JS which triggers when text is entered to a textarea which can lead to bugs in combination with AtomicChrome. There’s some websites where I regularly lose the text that’s entered. While I’m editing, the textarea is updating, but on C-c C-c
, Emacs closes and the textarea is empty. For such cases, I’m using this simple workaround: Copy the contents to clipboard just before closing Emacs. So if the contents are lost, I can just paste the text into the textarea. Not a perfect solution, but this happens seldomly enough, that it’s good enough for me.
(advice-add 'atomic-chrome-close-current-buffer :before '(lambda() (clipboard-kill-ring-save (point-min) (point-max))))
Writing and reading mail is inherently a text-based workflow. Yes, there’s HTML mails and attachments, but at the core Email is probably the place where many people write and consume the most text. To utilize the best text-processing program available makes a lot of sense.
When combined with other powerful features of Emacs (such as Org mode for organizing mails into projects and todos), processing mails within Emacs not only makes a lot of sense, but becomes a powerhouse.
Emacs has many options for MTAs. I’m using MU4E which is a little similar to using mutt with notmuch. As SMTP, I’m using the built-in smtpmail
Emacs package.
MU works on a local Maildir folder. For synchronization offlineimap is used. Install:
apt-get install offlineimap
brew install offlineimap
For MU4E to work, install MU and MU4E:
apt-get install mu4e
guix package -i mu
brew install mu --with-emacs
For starttls to work when sending mail, install gnutls:
apt-get install gnutls-bin
brew install gnutls
"mu"
.offlineimaprc
file for IMAP.authinfo
file for SMTPTell Emacs where to find the encrypted .authinfo
file.
(setq auth-sources '((:source "~/.authinfo.gpg")))
To open PDFs within Mu4e with Emacs, then there’s one thing to configure. Mu4e uses xdg-open
to chose the app to open any mime type.
Configure xdg-open
to use Emacs in .local/share/applications/mimeapps.list
:
xdg-mime default emacs.desktop application/pdf
mu
setup (Initializing the message store):
https://www.djcbsoftware.nl/code/mu/mu4e/Initializing-the-message-store.html
mu init --my-address=alain.lafon@dispatched.ch --my-address=alain@200ok.ch --my-address=support@200ok.ch --my-address=lafo@zhaw.ch --my-address=alain@zen-tempel.ch --my-address=preek@dispatched.ch --maildir=~/Maildir
(require 'mu4e) ;; Not required from 1.8.7-2, it's already included ;; (require 'org-mu4e) (setq send-mail-function 'smtpmail-send-it) ;; Default account on startup (setq user-full-name "Alain M. Lafon" mu4e-sent-folder "/200ok/INBOX.Sent" mu4e-drafts-folder "/200ok/INBOX.Drafts" mu4e-trash-folder "/200ok/INBOX.Trash") (setq smtpmail-debug-info t message-kill-buffer-on-exit t ;; Custom script to run offlineimap in parallel for multiple ;; accounts as discussed here: ;; http://www.offlineimap.org/configuration/2016/01/29/why-i-m-not-using-maxconnctions.html ;; This halves the time for checking mails for 4 accounts for me ;; (when nothing has to be synched anyway) mu4e-get-mail-command "offlineimap_parallel.sh" mu4e-attachment-dir "~/Dropbox/org/files/inbox") ;; show full addresses in view message (instead of just names) ;; toggle per name with M-RET (setq mu4e-view-show-addresses t) ;; Do not show related messages by default (toggle with =W= works ;; anyway) (setq mu4e-headers-include-related nil) ;; Alternatives are the following, however in first tests they ;; show inferior results ;; (setq mu4e-html2text-command "textutil -stdin -format html -convert txt -stdout") ;; (setq mu4e-html2text-command "html2text -utf8 -width 72") ;; (setq mu4e-html2text-command "w3m -dump -T text/html") (defvar my-mu4e-account-alist '(("200ok" (user-full-name "Alain M. Lafon") (message-signature "\n200ok GmbH, CEO\n\nalain@200ok.ch, +41 76 405 05 67\n\nhttps://200ok.ch/\n\nBook an appointment with me: https://200ok.ch/calendar/alain.html \n\norganice is the best way to get stuff done: https://organice.200ok.ch") (message-signature-auto-include t) (mu4e-sent-folder "/200ok/INBOX.Sent") (mu4e-drafts-folder "/200ok/INBOX.Drafts") (mu4e-trash-folder "/200ok/INBOX.Trash") (user-mail-address "alain@200ok.ch") (smtpmail-default-smtp-server "mail.your-server.de") (smtpmail-local-domain "200ok.ch") (smtpmail-smtp-user "munen@200ok.ch") (smtpmail-smtp-server "mail.your-server.de") (smtpmail-stream-type starttls) (smtpmail-smtp-service 587)) ("200ok-support" (user-full-name "200ok Support") (message-signature "\n200ok GmbH, CEO\n\nalain@200ok.ch, +41 76 405 05 67\n\nhttps://200ok.ch/\n\nBook an appointment with me: https://200ok.ch/calendar/alain.html \n\norganice is the best way to get stuff done: https://organice.200ok.ch") (message-signature-auto-include t) (mu4e-sent-folder "/200ok-support/INBOX.Sent") (mu4e-drafts-folder "/200ok-support/INBOX.Drafts") (mu4e-trash-folder "/200ok-support/INBOX.Trash") (user-mail-address "support@200ok.ch") (smtpmail-default-smtp-server "mail.your-server.de") (smtpmail-local-domain "200ok.ch") (smtpmail-smtp-user "support@200ok.ch") (smtpmail-smtp-server "mail.your-server.de") (smtpmail-stream-type starttls) (smtpmail-smtp-service 587)) ("zen-tempel" (user-full-name "Munen Alain M. Lafon") (message-signature "\nLambda Zen Tempel\n\nalain@zen-temple.net, +41 76 405 05 67\n\nhttps://zen-temple.net/") (message-signature-auto-include t) (mu4e-sent-folder "/zen-tempel/INBOX.Sent") (mu4e-drafts-folder "/zen-tempel/INBOX.Drafts") (mu4e-trash-folder "/zen-tempel/INBOX.Trash") (user-mail-address "alain@zen-tempel.ch") (smtpmail-default-smtp-server "mail.your-server.de") (smtpmail-local-domain "zen-tempel.ch") (smtpmail-smtp-user "alain@zen-tempel.ch") (smtpmail-smtp-server "mail.your-server.de") (smtpmail-stream-type starttls) (smtpmail-smtp-service 587)) ("dispatched" (user-full-name "Alain M. Lafon") (message-signature-auto-include nil) (mu4e-sent-folder "/dispatched/INBOX.Sent") (mu4e-drafts-folder "/dispatched/INBOX.Drafts") (mu4e-trash-folder "/dispatched/INBOX.Trash") (user-mail-address "alain.lafon@dispatched.ch") (smtpmail-default-smtp-server "mail.your-server.de") (smtpmail-local-domain "dispatched.ch") (smtpmail-smtp-user "munen@dispatched.ch") (smtpmail-smtp-server "mail.your-server.de") (smtpmail-stream-type starttls) (smtpmail-smtp-service 587)))) ;; Whenever a new mail is to be composed, change all relevant ;; configuration variables to the respective account. This method is ;; taken from the MU4E documentation: ;; http://www.djcbsoftware.nl/code/mu/mu4e/Multiple-accounts.html#Multiple-accounts (defun my-mu4e-set-account () "Set the account for composing a message." (let* ((account (if mu4e-compose-parent-message (let ((maildir (mu4e-message-field mu4e-compose-parent-message :maildir))) (string-match "/\\(.*?\\)/" maildir) (match-string 1 maildir)) (completing-read (format "Compose with account: (%s) " (mapconcat #'(lambda (var) (car var)) my-mu4e-account-alist "/")) (mapcar #'(lambda (var) (car var)) my-mu4e-account-alist) nil t nil nil (caar my-mu4e-account-alist)))) (account-vars (cdr (assoc account my-mu4e-account-alist)))) (if account-vars (mapc #'(lambda (var) (set (car var) (cadr var))) account-vars) (error "No email account found")))) (add-hook 'mu4e-compose-pre-hook 'my-mu4e-set-account) (add-hook 'mu4e-compose-mode-hook 'visual-line-mode) (setq mu4e-refile-folder (lambda (msg) (cond ((string-match "^/dispatched.*" (mu4e-message-field msg :maildir)) "/dispatched/INBOX.Archive") ((string-match "^/zen-tempel.*" (mu4e-message-field msg :maildir)) "/zen-tempel/INBOX.Archive") ((string-match "^/200ok.*" (mu4e-message-field msg :maildir)) "/200ok/INBOX.Archive") ((string-match "^/200ok-support.*" (mu4e-message-field msg :maildir)) "/200ok-support/INBOX.Archive") ((string-match "^/zhaw.*" (mu4e-message-field msg :maildir)) "/zhaw/Archive") ;; everything else goes to /archive (t "/archive")))) ;; Empty the initial bookmark list (setq mu4e-bookmarks '()) ;; All archived folders (defvar d-archive "NOT (maildir:/dispatched/INBOX.Archive OR maildir:/zen-tempel/INBOX.Archive OR maildir:/200ok/INBOX.Archive OR maildir:/200ok-support/INBOX.Archive OR maildir:/zhaw/Archive)") (defvar inbox-folders (string-join '("maildir:/dispatched/INBOX" "maildir:/zen-tempel/INBOX" "maildir:/200ok/INBOX" "maildir:/200ok-support/INBOX") " OR ")) (defvar draft-folders (string-join '("maildir:/dispatched/INBOX.Drafts" "maildir:/zen-tempel/INBOX.Drafts" "maildir:/200ok/INBOX.Drafts" "maildir:/200ok-support/INBOX.Drafts") " OR ")) (defvar spam-folders (string-join '("maildir:/dispatched/INBOX.spambucket" "maildir:/zen-tempel/INBOX.spambucket" "maildir:/200ok/INBOX.spambucket" "maildir:/200ok-support/INBOX.spambucket") " OR ")) (defvar blacklist-folders (string-join '("maildir:/dispatched/INBOX.blacklist" "maildir:/zen-tempel/INBOX.blacklist" "maildir:/200ok/INBOX.blacklist" "maildir:/200ok-support/INBOX.blacklist") " OR ")) ;; Re-define all standard bookmarks to not include the spam and ;; blacklist folders for searches (defvar d-spam (format "NOT (%s OR %s)" spam-folders blacklist-folders)) ;; Today (let ((today-query (concat d-spam " AND date:today..now"))) (add-to-list 'mu4e-bookmarks `(:name "Today's messages" :query ,today-query :key ?t))) ;; Last 7 days (let ((seven-days-query (concat d-spam " AND date:7d..now"))) (add-to-list 'mu4e-bookmarks `(:name "Last 7 days" :query ,seven-days-query :key ?w))) ;; Flagged (let ((flagged-query (concat d-spam " AND flag:flagged"))) (add-to-list 'mu4e-bookmarks `(:name "Flagged" :query ,flagged-query :key ?f))) ;; Messages with images (let ((images-query (concat d-spam " AND mime:image/*"))) (add-to-list 'mu4e-bookmarks `(:name "Messages with images" :query ,images-query :key ?p))) ;; Spam (add-to-list 'mu4e-bookmarks `(:name "Spam" :query ,spam-folders :key ?S)) ;; Blacklisted (add-to-list 'mu4e-bookmarks `(:name "Blacklisted" :query ,blacklist-folders :key ?B)) ;; Drafts (add-to-list 'mu4e-bookmarks `(:name "Drafts" :query ,draft-folders :key ?d)) ;; Inbox (add-to-list 'mu4e-bookmarks `(:name "Inbox" :query ,inbox-folders :key ?i)) ;; Unread messages (let ((unread-query (concat d-spam d-archive " AND (flag:unread OR flag:flagged) AND NOT flag:trashed"))) (add-to-list 'mu4e-bookmarks `(:name "Unread messages" :query ,unread-query :key ?u))) ;; Unread messages, current year (let ((current-year-query (concat d-spam d-archive (concat " (flag:unread OR flag:flagged) AND NOT flag:trashed AND date:" (format-time-string "%Y"))))) (add-to-list 'mu4e-bookmarks `(:name "Unread messages, current year" :query ,current-year-query :key ?U))) ;; Monitoring (add-to-list 'mu4e-bookmarks '(:name "Monitoring" :query "(cron OR monit OR logcheck or UptimeRobot or noreply@linode.com) AND flag:unread" :key ?M))Use Emacs completion instead of mu completion
(setq mu4e-read-option-use-builtin nil mu4e-completing-read-function 'completing-read)Check for supposed attachments prior to sending them
(defun ok/message-attachment-present-p () "Return t if a non-gpg attachment is found in the current message." (save-excursion (save-restriction (widen) (goto-char (point-min)) (when (search-forward "<#part type" nil t) t)))) (setq ok/message-attachment-regexp (regexp-opt '("[Ww]e send" "[Ii] send" "attach" "[aA]ngehängt" "[aA]nhang" "[sS]chicke" "angehaengt" "haenge" "hänge"))) (defun ok/message-warn-if-no-attachments () "Check if there is an attachment in the message if I claim it." (when (and (save-excursion (save-restriction (widen) (goto-char (point-min)) (re-search-forward ok/message-attachment-regexp nil t))) (not (ok/message-attachment-present-p))) (unless (y-or-n-p "No attachment. Send the message?") (keyboard-quit)))) (add-hook 'message-send-hook #'ok/message-warn-if-no-attachments)
For mail completion, only consider emails that have been seen in the last 6 months. This gets rid of legacy mail addresses of people.
(setq mu4e-compose-complete-only-after (format-time-string "%Y-%m-%d" (time-subtract (current-time) (days-to-time 150))))Enable temp file for faster communication between mu and mu4e
;; Enable to show debug render times ;; (setq mu4e-headers-report-render-time t) (setq mu4e-mu-allow-temp t)
Mini Benchmark (Searching for ‘phil’): With temp file: [mu4e] Found 500 matching messages; 0 hidden; search: 142.3 ms (0.28 ms/msg); render: 122.1 ms (0.24 ms/msg) Without temp file: [mu4e] Found 500 matching messages; 0 hidden; search: 181.3 ms (0.36 ms/msg); render: 134.6 ms (0.27 ms/msg)
(require 'mu4e-contrib) (setq mu4e-html2text-command 'mu4e-shr2text) ;;(setq mu4e-html2text-command "iconv -c -t utf-8 | pandoc -f html -t plain") (add-to-list 'mu4e-view-actions '("ViewInBrowser" . mu4e-action-view-in-browser) t)
Disable colors for HTML mails. HTML mails, especially transactional ones, can be very convoluted. Converting them to text and taking away colors can make them more readable.
Disable “HTML over plain text” heuristic. This variable officially has this rationale: “Ratio between the length of the html and the plain text part below which mu4e will consider the plain text part to be ‘This messages requires html’ text bodies. You can neutralize it (always show the text version) by using `most-positive-fixnum’.”
This heuristic overwrites the default setting (and configuration) that Plain text should be preferred over HTML!
In my experience, HTML Emails are WAY longer than only 5x the Plain text (Doodle, Airbnb, Meetup, etc), so this will yield me a lot of false positives whereas I have never seen a “This message requires HTML” body.
I wrote an accompanying blog post with further information: https://200ok.ch/posts/2018-10-25_disable_mu4e_html_over_plain_text_heuristic.html
(setq mu4e-view-html-plaintext-ratio-heuristic most-positive-fixnum)
(add-hook 'mu4e-compose-mode-hook 'flyspell-mode)
Note: There’s no notifications, because that’s only distracting.
(setq mu4e-update-interval (* 15 60)) (setq mu4e-index-update-in-background t)
C-c RET s o
to signC-c RET C-c
to encryptC-c C-e v
to verify the signatureC-c C-e d
to decryptAlways sign outgoing emails:
(setq mu4e-compose-crypto-reply-plain-policy 'sign)
When sending encrypted messages, also encrypt to self so that I can read the mail in the sent folder:
(setq mml-secure-openpgp-encrypt-to-self t) (setq mml-secure-openpgp-sign-with-sender t)
With upgrading to Emacs 27, this broke mu4e-compose-crypto-reply-plain-policy
set to 'sign
. It always wanted to sign with s/mime whereas I want to sign with gpg. I think this option is obsolete. I’m leaving it here for a moment until I’m sure it will not be needed anymore.
;; (add-hook 'mu4e-compose-mode-hook 'epa-mail-mode) ;; (add-hook 'mu4e-view-mode-hook 'epa-mail-mode)
When looking at emails, show them nicely wrapped. That’s very helpful when people send mails with very long lines.
Disabling for the moment, because some emails looks worse after it. visual-line-mode
can always be invoked by w
in mu4e-view-mode
.
;; (add-hook 'mu4e-view-mode-hook 'visual-line-mode)Always reply everyone, not just the sender
(eval-after-load 'mu4e '(define-key mu4e-compose-minor-mode-map "R" #'mu4e-compose-wide-reply))
(setq mu4e-compose-dont-reply-to-self t)Store link to message if in header view, not to header query
(setq org-mu4e-link-query-in-headers-mode nil)
This only adds :bcc
.
(setq mu4e-view-fields '(:from :to :cc :bcc :subject :flags :date :maildir :mailing-list :tags :attachments :signature :decryption))Close mu4e without asking
(setq mu4e-confirm-quit nil)Reminder to keep to three sentences
Rationale: E-mail takes too long to respond to, resulting in continuous inbox overflow for those who receive a lot of it.
(add-hook 'mu4e-compose-mode-hook (defun ok-mu4e-keep-to-three-sentences () (shell-command "notify-send -u critical 'Keep to three sentences.'") (message "Keep to three sentences.")))
Set a sane ISO 8601 date format.
(setq mu4e-headers-date-format "%+4Y-%m-%d")
Setting format=flowed
for non-text-based mail clients which don’t respect actual formatting, but let the text “flow” as they please. Relevant RFC: https://tools.ietf.org/html/rfc3676
What is required is a format which is in all significant ways Text/Plain, and therefore is quite suitable for display as Text/Plain, and yet allows the sender to express to the receiver which lines are quoted and which lines are considered a logical paragraph, and thus eligible to be flowed (wrapped and joined) as appropriate.
M-q
) to do the right thing.(setq mu4e-compose-format-flowed t)
Some email clients ignore format=flowed
(i.e. Outlook). Therefore, we send very long lines, so that they auto-flow. 998 chars are the actual maximum from the relevant RFC: https://www.ietf.org/rfc/rfc2822.txt
(setq fill-flowed-encode-column 998)Configure manually installed mu paths
(setq mu4e-msg2pdf "/usr/local/bin/msg2pdf")
mu4e has a feature skipping duplicates designed for Borg.
While I’m not Borg, being thrown in with the lot brings trouble. I like scanning my Spam folder for false positives. When ‘duplicates’ are not shown, it’ll take me many runs to delete those tasty Bitcoin offers.
An even better solution to the problem would actually be to automatically delete duplicates - maybe with procmail.
;; Disabled for the moment. ;; (setq mu4e-headers-skip-duplicates t)
https://www.djcbsoftware.nl/code/mu/mu4e/iCalendar.html
Ability to accept or reject mail calendar invitations as well as the ability to add accepted invitations to Org mode (and therefore to the agenda).
(require 'mu4e-icalendar) (mu4e-icalendar-setup) (setq gnus-icalendar-org-capture-file "~/Dropbox/org/things.org") (setq gnus-icalendar-org-capture-headline '("Calendar")) (gnus-icalendar-org-setup)
https://github.com/org-mime/org-mime
org-mime can be used to send HTML email using Org-mode HTML export.
(require 'org-mime) ;; automatically htmlize all outgoing mail. that's even reasonable if ;; I don't explicitly wanted to use 'org', but just make an itemized ;; list or sth. (defun conditional-org-mime-htmlize () "Apply org-mime-htmlize only if the message is not a forwarded email. Disables inline images during this specific export." (let ((subject (message-field-value "Subject"))) (when (not (and subject (or (string-match-p "^\\(Fwd\\|FW\\|Forward\\):" subject) ;; You might want to add other forwarding patterns here ))) ;; Temporarily set org-html-inline-images to nil for this export ;; using let-binding. The original value is restored after ;; org-mime-htmlize finishes. (let ((org-html-inline-images nil)) (org-mime-htmlize))))) ;; Replace the old hook with our conditional version (add-hook 'message-send-hook #'conditional-org-mime-htmlize)
Usage:
M-x org-mime-htmlize
from within a mail composition buffer to export either the entire buffer or just the active region to html, and embed the results into the buffer as a text/html mime section.org-mime-edit-mail-in-org-mode
edit mail in a special editor with org-mode.org-mime-htmlize
, you can always run org-mime-revert-to-plain-text-mail
restore the original plain text mail.https://mathiasbynens.be/notes/gmail-plain-text https://mothereff.in/quoted-printable https://www.gnu.org/software/emacs/manual/html_node/emacs-mime/qp.html
Add a header action “Block” which add the Senders Name and From Address to a procmail blacklist.
(defun append-line-to-file (line path) "Append a `line` to a file behind `path`" (write-region (concat line "\n") nil path 'append)) (defun mu4e-strategy-from (strategy msg) "If STRATEGY is 'blacklist', then add the `from` of a message to the procmail. If it is 'whitelist', then whitelist'." (let* ((from (mu4e-message-field msg :from)) (from_name (car (cdr (car from)))) (from_address (car (cdr (cdr (cdr (car from)))))) (path (format "~/.procmail/%s_from.txt" strategy))) ;; Whitelist/Blacklist the senders Name (if from_name (append-line-to-file from_name path)) ;; Whitelist/Blacklist the Email-Address (append-line-to-file from_address path) (shell-command (format "sort -u -o %s %s" path path)) (message "%s: %s" strategy from))) (defun mu4e-blacklist-subject (msg) "Add the `subject` of a message to the procmail blacklist" (let* ((subject (mu4e-message-field msg :subject)) (path "~/.procmail/blacklist_subject.txt")) (if subject (append-line-to-file subject path)) (shell-command (format "sort -u -o %s %s" path path)) (message "Blacklist: %s" subject))) (add-to-list 'mu4e-headers-actions '("f White 'From:'" . (lambda (msg) (mu4e-strategy-from "whitelist" msg))) t) (add-to-list 'mu4e-headers-actions '("F Block 'From:'" . (lambda (msg) (mu4e-strategy-from "blacklist" msg))) t) (add-to-list 'mu4e-headers-actions '("S Block 'Subject:'" . mu4e-blacklist-subject) t)Rewrite contact information
(defun munen-contact-processor (contact) (cond ((string-match "phil@200ok.ch" contact) ; Phil sometimes dosn't add his name to the reply-to. "Phil Hofmann <phil@200ok.ch>") ((string-match "d.kuehner@n-pg.de" contact) ; Sanitize NPG encoding "Dominik Kühner <d.kuehner@n-pg.de>") ((string-match "wiffbubbles@icloud.com" contact) ; Monika writes her name in all caps and I don't want to forward it like that. "Monika Bieri <wiffbubbles@icloud.com>") ((string-match "bieri.monika@icloud.com" contact) ; Monika writes her name in all caps and I don't want to forward it like that. "Monika Bieri <bieri.monika@icloud.com>") (t contact))) (setq mu4e-contact-process-function 'munen-contact-processor)
ido
means “Interactively Do Things”. ido
has a completion engine that’s sensible to use everywhere. It is built-in and nice and could change a lot of defaults like find-file
and switching buffers.
It works well while not breaking Emacs defaults.
(ido-mode t) (ido-everywhere t) (setq ido-enable-flex-matching t)
https://github.com/abo-abo/swiper
Ivy, a generic completion mechanism for Emacs.
Counsel, a collection of Ivy-enhanced versions of common Emacs commands.
Swiper, an Ivy-enhanced alternative to isearch.
Ivy
is an interactive interface for completion in Emacs. Therefore it overlaps in functionality with ido
. While Ivy
is more powerful, it breaks certain standard functionality. So ido
is enabled globally by default and for certain tasks, Ivy
overrides ido
.
Emacs uses completion mechanism in a variety of contexts: code, menus, commands, variables, functions, etc. Completion entails listing, sorting, filtering, previewing, and applying actions on selected items. When active, ivy-mode
completes the selection process by narrowing available choices while previewing in the minibuffer. Selecting the final candidate is either through simple keyboard character inputs or through powerful regular expressions.
(ivy-mode) (setq enable-recursive-minibuffers t) (global-set-key (kbd "<f6>") 'ivy-resume) (global-set-key (kbd "C-c SPC") 'complete-symbol)
Show total amount of matches and the index of the current match
(setq ivy-count-format "(%d/%d) ")
Wrap to the first result when on the last result and vice versa.
Enable Swiper
(global-set-key "\C-s" 'swiper)
Configure Counsel
(global-set-key (kbd "C-x b") 'counsel-ibuffer) (global-set-key (kbd "C-h d") 'counsel-describe-function) (global-set-key (kbd "C-h v") 'counsel-describe-variable) ;; Run `counsel-ag` against the current directory and not against the ;; whole project (global-set-key (kbd "C-c k") '(lambda() (interactive) (counsel-ag "" default-directory nil nil))) (global-set-key (kbd "C-x l") 'counsel-locate) (define-key minibuffer-local-map (kbd "C-r") 'counsel-minibuffer-history)
Override C-c C-j
(org-goto) with counsel-org-goto
which brings super fast fuzzy matching and navigation capabilities for headlines.
(define-key org-mode-map (kbd "C-c C-j") 'counsel-org-goto)
Override C-x i
(insert-file) in favor of counsel-imenu
which brings fuzzy matching to the ability to jump to any indexed position to the already great [[https://www.gnu.org/software/emacs/manual/html_node/emacs/Imenu.html][imenu]]
.
(global-set-key (kbd "C-x i") 'counsel-imenu)
Override find-file
and dired
in favor of the counsel counter parts.
(global-set-key (kbd "C-x C-f") 'counsel-find-file) (global-set-key (kbd "C-x d") 'counsel-dired)
Next to counsel, there’s also smex
which is M-x
combined with ido
. smex
has a better sorting algorithm than Counsel
and having both installed means that we get the Counsel
interface with smex
sorting. Best of both worlds.
By default, counsel-M-x
starts with a ^
. More often than not, this will be in the way of me fuzzy matching a function. Therefore I’ll start it with an empty string as argument.
(global-set-key (kbd "M-x") (lambda () (interactive) (counsel-M-x "")))
Override insert-char
.
(global-set-key (kbd "C-x 8 RET") 'counsel-unicode-char)
Make the prompt line selectable (with C-p
).
(setq ivy-use-selectable-prompt t)Where
Ivy
doesn’t work well Overwriting standard Emacs functionality
Some basic features are overwritten when “everything” becomes an Ivy
search buffer. For example:
Tramp
auto-completion doesn’t work for me. I’m using sudo:
, ssh:
and the likes a lot in dired
mode. Auto completion when within Tramp
is broken for me, so I always have to type out the whole connection string when Ivy
is enabled for dired
. Since this includes missing auto-completion on remote systems and such, it’s another valid reason to disable Ivy
globally.Ivy/Swiper cannot search in PDFs. It tries to search in the PDF source code. Therefore I fall back to using isearch within PDFs.
(add-hook 'pdf-view-mode-hook '(lambda() (define-key pdf-view-mode-map "\C-s" 'isearch-forward)))
When two dired
buffers are open and files should be copied from one to the other, one can use the up
and down
keys to toggle the destination. When this is a search buffer, it will auto complete for all local folders, instead. This is something I do often.
(add-hook 'dired-mode-hook '(lambda () (ivy-mode -1)))Improve other packages with ivy
Projectile completion (Default is ido
)
(setq projectile-completion-system 'ivy)
Mu4e “folder” and “from” completion (Default is ido
)
(setq mu4e-completing-read-function 'ivy-completing-read)
Synosaurus completion (Default is ido
)
(setq synosaurus-choose-method 'ivy-read)
I used to use isearch
instead of Swiper
.
Replace i-search-(forward|backward) with their respective regexp capable counterparts
;;(global-set-key (kbd "C-s") 'isearch-forward-regexp) ;;(global-set-key (kbd "C-r") 'isearch-backward-regexp)
For chat-based communication, I like to use IRC. In my ~/.authinfo.gpg
file, I have a line like:
machine irc.libera.chat login "munen" password SECRET_PASSWORD
This file is automatically read when connecting to servers. It’s the same for SMTP servers, for example.
For connecting to IRC, I’m using the built-in package erc
.
Configure automatic join list
(setq erc-autojoin-channels-alist '(("libera.chat" "#organice" "#200ok" "#emacsconf" "#emacsconf-org" "#lobsters") ;; Deprecate Freenode (rationale https://github.com/200ok-ch/org-parser/pull/48) ;; ("freenode.net" "#200ok" "#emacsconf" "#emacsconf-org" "#lobsters") ;; This does not work, yet. The ;; channels cannot be joined on ;; connecting to bitlbee. bitlbee ;; needs to first connect to the ;; configured accounts (i.e. ;; slack). This could happen in a ;; timeout, or better event ;; oriented on a message that ;; bitlbee sends when connected to ;; the account. ;; '(("localhost" "#internal" "#general")) ))
By default, connect to Libera.Chat
(defconst erc-default-server "irc.libera.chat")
Do not show join/quit info for lurkers
(setq erc-lurker-hide-list '("JOIN" "PART" "QUIT"))
Automatically unfold images when links are shared
(require 'erc-image) (add-to-list 'erc-modules 'image) (erc-update-modules)
Logging
(setq erc-log-channels-directory "~/.erc/logs/") (add-hook 'erc-insert-post-hook 'erc-save-buffer-in-logs)
Notify when someone is addressing me
(setq erc-pals '("phi|" "branch14")) ;; The quotes around %s are super important to prevent shell injection (add-hook 'erc-text-matched-hook '(lambda(match-type nickuserhost msg) (shell-command-to-string (format "notify-send erc '%s'" msg))))
https://github.com/TheBB/spaceline
This part of the configuration was kindly provided by SirPscl.
"emacs-spaceline"
Slightly simplified flycheck segments for info
, warning
and error
.
;; This is throwing errors when debugging elisp since [2022-12-06 Tue]. ; (spaceline-define-segment ph/flycheck-warning-segment ; (if (flycheck-has-current-errors-p) ; (let ((c (cdr (assq 'warning (flycheck-count-errors ; flycheck-current-errors))))) ; (powerline-raw ; (if c (format "%s" c)))))) (spaceline-define-segment ph/flycheck-error-segment (if (flycheck-has-current-errors-p) (let ((c (cdr (assq 'error (flycheck-count-errors flycheck-current-errors))))) (powerline-raw (if c (format "%s" c)))))) (spaceline-define-segment ph/flycheck-info-segment (if (flycheck-has-current-errors-p) (let ((c (cdr (assq 'info (flycheck-count-errors flycheck-current-errors))))) (powerline-raw (if c (format "%s" c))))))
Default faces for the flycheck segments.
(defface ph/spaceline-flycheck-error-face '((t :inherit 'mode-line :weight bold :foreground "white" :background "dark red")) "Flycheck Error Face" :group 'spaceline) (defface ph/spaceline-flycheck-warning-face '((t :inherit 'mode-line :weight bold :foreground "white" :background "DarkOrange3")) "Flycheck Warning Face" :group 'spaceline) (defface ph/spaceline-flycheck-info-face '((t :inherit 'mode-line :weight bold :foreground "white" :background "dark green")) "Flycheck Info Face" :group 'spaceline)
Setting the face according to evil-state
.
(defun ph/spaceline-highlight-face-evil-state () "Set the highlight face depending on the evil state." (if (bound-and-true-p evil-local-mode) (let* ((face (assq evil-state spaceline-evil-state-faces))) (if face (cdr face) (spaceline-highlight-face-default))) (spaceline-highlight-face-default))) (setq-default spaceline-highlight-face-func 'ph/spaceline-highlight-face-evil-state)
Set the evil-state segment colors for operator-state
.
(defface ph/spaceline-evil-operator-face '((t (:background "cornflower blue" :inherit 'spaceline-evil-normal))) "Spaceline Evil Operator State" :group 'spaceline) (add-to-list 'spaceline-evil-state-faces '(operator . ph/spaceline-evil-operator-face))
(defun ph/git-branch-name () (replace-regexp-in-string "^ Git[:-]" "" vc-mode)) (spaceline-define-segment ph/version-control "Version control information." (when vc-mode (s-trim (concat (ph/git-branch-name)))))
Tramp offers the following file name syntax to refer to files on other machines.
/method:host:filename /method:user@host:filename /method:user@host#port:filename
The following segemnts display the current buffer’s method
and user@host
.
(spaceline-define-segment ph/remote-method (when (and default-directory (file-remote-p default-directory 'method)) (file-remote-p default-directory 'method))) (spaceline-define-segment ph/remote-user-and-host (when (and default-directory (or (file-remote-p default-directory 'user) (file-remote-p default-directory 'host))) (concat (file-remote-p default-directory 'user) "@" (file-remote-p default-directory 'host))))
Default faces for the tramp segments.
(defface ph/spaceline-tramp-user-host-face '((t :inherit 'mode-line :foreground "black" :background "#fce94f")) "Tramp User@Host Face" :group 'spaceline) (defface ph/spaceline-tramp-method-face '((t :inherit 'mode-line :foreground "black" :background "#ff5d17")) "Tramp Method Face" :group 'spaceline)
I’m not using Mu4e contexts, yet, because my configuration started before they were introduced. I’m leaving the segment configuration for the future.
;; (spaceline-define-segment ph/mu4e-context-segment ;; (let ((context (mu4e-context-current))) ;; (when (and context ;; (string-prefix-p "mu4e" (symbol-name major-mode))) ;; (mu4e-context-name context))))
Face for mu4e
segemnt.
;; (defface ph/spaceline-mu4e-context-face ;; '((t :inherit 'mode-line ;; :weight bold)) ;; "mu4e face" ;; :group 'spaceline)
I like to set timers, for example through org-pomodoro.el
(spaceline-define-segment org-timer-left-time "Show the time left in the current org-timer (i.e. a pomodoro)." (ok-pomodoro-remaining-time))
Setting up the mode-line and order of segements. Compile the modeline with M-x spaceline-compile
.
(require 'spaceline-config) (require 'spaceline) (spaceline-spacemacs-theme) ;; Otherwise spaceline will be huge in Emacs >= 27.1 ;; (setq powerline-height 1) ;; Since Emacs >= 28.2, it needs to be bigger to stay the same size. (setq powerline-height 22) ;; Since Emacs >= 27.1, there's no need for font trickery. Just use ;; UTF-8. ;; (setq powerline-default-separator 'utf-8) (spaceline-install 'main '((evil-state :face highlight-face) (buffer-id) ;; (org-timer-left-time) ;; Currently, I show the remaining time in Polybar, not Emacs. ;; (ph/mu4e-context-segment :face 'ph/spaceline-mu4e-context-face) (ph/remote-method :face 'ph/spaceline-tramp-method-face) (ph/remote-user-and-host :face 'ph/spaceline-tramp-user-host-face) (buffer-modified)) '(;;(minor-modes :when active) (projectile-root) (ph/version-control) ;(line-column :when active) ;(buffer-position :when active) (ph/flycheck-info-segment :face 'ph/spaceline-flycheck-info-face :when active) (ph/flycheck-warning-segment :face 'ph/spaceline-flycheck-warning-face :when active) (ph/flycheck-error-segment :face 'ph/spaceline-flycheck-error-face :when active) (line-column) (major-mode)))
Set mode-line always active (don’t hide segments when focus is on a different window).
(defun powerline-selected-window-active () t)
Diminish implements hiding or abbreviation of the mode line displays (lighters) of minor-modes.
(eval-after-load "auto-revert" '(diminish 'auto-revert-mode)) (eval-after-load "beacon" '(diminish 'beacon-mode)) (eval-after-load "ivy" '(diminish 'ivy-mode)) (eval-after-load "projectile" '(diminish 'projectile-mode)) (eval-after-load "projectile-rails" '(diminish 'projectile-rails-mode)) (eval-after-load "rainbow-mode" '(diminish 'rainbow-mode)) (eval-after-load "undo-tree" '(diminish 'undo-tree-mode)) (eval-after-load "which-key" '(diminish 'which-key-mode))
"emacs-spaceline"
https://github.com/hlissner/emacs-hide-mode-line
A minor mode that hides (or masks) the mode-line in your current buffer. It can be used to toggle an alternative mode-line, toggle its visibility, or simply disable the mode-line in buffers where it isn’t very useful otherwise.
(require 'hide-mode-line) (add-hook 'pdf-view-mode-hook #'hide-mode-line-mode)
https://github.com/bnbeckwith/writegood-mode
This is a minor mode to aid in finding common writing problems.
It highlights text based on a set of weasel-words, passive-voice and duplicate words.
https://github.com/hpdeifel/synosaurus/
Synosaurus is a thesaurus front-end with pluggable back-end.
Use the openthesaurus.de back-end.
(setq synosaurus-backend 'synosaurus-backend-openthesaurus) (defalias 'thesaurus-openthesaurus-de 'synosaurus-lookup)
Emacs has built-in functionality for checking and correcting spelling called ispell.el
. On top of that, there’s a built-in minor mode for for on-the-fly spell checking. called flyspell-mode
.
Flyspell can use multiple back-ends (for example ispell, aspell or hunspell).
Order corrections by likelinessDo not order not by the default of alphabetical ordering.
(setq flyspell-sort-corrections nil)Do not print messages for every word
When checking the entire buffer, don’t print messages for every word. This is a major performance gain.
(setq flyspell-issue-message-flag nil)Use
hunspell
with multiple dictionaries
Here in Switzerland, there are four official languages: Swiss German, French, Italian and Romansh. Also, we converse a lot in German and English. Hence, it’s a regular occurrence to have one file with multiple languages in them. Especially for these situations it’s still to have proper spell checking. Fortunately, Emacs has us covered!
Hunspell is a free spell checker and used by LibreOffice, Firefox and Chromium. It allows to set multiple dictionaries - even different dictionaries per language (aspell
, for example also allows multiple dictionaries, but only for the same language). It also can be used as an ispell.el
backend.
To use hunspell
, install it first:
apt install hunspell hunspell-de-de hunspell-en-gb hunspell-en-us hunspell-de-ch-frami
(with-eval-after-load "ispell" ;; Configure `LANG`, otherwise ispell.el cannot find a 'default ;; dictionary' even though multiple dictionaries will be configured ;; in next line. (setenv "LANG" "en_US") (setq ispell-program-name "hunspell") ;; Configure German, Swiss German, and two variants of English. (setq ispell-dictionary "de_DE,de_CH,en_GB,en_US") ;; ispell-set-spellchecker-params has to be called ;; before ispell-hunspell-add-multi-dic will work (ispell-set-spellchecker-params) (ispell-hunspell-add-multi-dic "de_DE,de_CH,en_GB,en_US")) ;; For saving words to the personal dictionary, don't infer it from ;; the locale, otherwise it would save to ~/.hunspell_de_DE. (setq ispell-personal-dictionary "~/.hunspell_personal") ;; The personal dictionary file has to exist, otherwise hunspell will ;; silently not use it. (unless (file-exists-p ispell-personal-dictionary) (write-region "" nil ispell-personal-dictionary))Do not loose all spellchecking information after adding one word to a personal dictionary
Advice to re-check the buffer after a word has been added to the dictionary. This has the benefit of the word actually being cleared, but the downside that the whole buffer has to be re-checked which an take some time.
;; (defun flyspell-buffer-after-pdict-save (&rest _) ;; (flyspell-buffer)) ;; (advice-add 'ispell-pdict-save :after #'flyspell-buffer-after-pdict-save)
The proper solution (for which I don’t have time now) is to just mark all further occurrences of the word you just saved as correct (without having to recheck the whole buffer).
Alternatively to using hunspell
, here’s an option to switch the dictionary between German and English.
The German dictionary is from here.
;; (defun flyspell-switch-dictionary() ;; "Switch between German and English dictionaries" ;; (interactive) ;; (let* ((dic ispell-current-dictionary) ;; (change (if (string= dic "deutsch") "english" "deutsch"))) ;; (ispell-change-dictionary change) ;; (message "Dictionary switched from %s to %s" dic change)))Implement
ispell-pdict-save
with above requirement
My preferred font is “Fira Code Retina”. In Debian, the package is called fonts-firacode
, in Guix it’s font-fira-code
.
(when (eq system-type 'gnu/linux) (add-to-list 'default-frame-alist '(font . "Fira Code Retina 9")) ;; In the past, this was the name for the same font: ;; (add-to-list 'default-frame-alist ;; '(font . "Fira CodeSource Code Pro Retina 9")) ;; (set-face-attribute 'default t :font "Fira Code Retina 9") ;; This is a great fallback font. It looks very similar and ;; comes preinstalled on many systems. ;; (add-to-list 'default-frame-alist ;; '(font . "DejaVu Sans Mono 9")) ;; Manually setting the font ;; (set-frame-font "Fira Code Retina 9") ;; Default Browser (setq browse-url-browser-function 'browse-url-generic browse-url-generic-program "firefox" browse-url-new-window-flag t) (menu-bar-mode -1) ;; enable pdf-tools (pdf-tools-install))
"font-fira-code" "fontconfig"
Requires the fonts-symbola
Debian package
;; (set-fontset-font t nil "Symbola" nil 'prepend)
(set-fontset-font t nil "Noto Color Emoji" nil 'prepend) ;; (set-fontset-font t '(#x1f000 . #x1faff) ;; (font-spec :family "Noto Color Emoji"))
"font-google-noto"
(when (eq system-type 'darwin) (set-frame-font "Menlo 14") ; Use Spotlight to search with M-x locate (setq locate-command "mdfind"))Set safe themes (to execute LISP code)
(setq custom-safe-themes (quote ("df3e05e16180d77732ceab47a43f2fcdb099714c1c47e91e8089d2fcf5882ea3" "d09467d742f713443c7699a546c0300db1a75fed347e09e3f178ab2f3aa2c617" "8db4b03b9ae654d4a57804286eb3e332725c84d7cdab38463cb6b97d5762ad26" "85c59044bd46f4a0deedc8315ffe23aa46d2a967a81750360fb8600b53519b8a" default)))Configure dark-mode theme and font size
(defun dark-mode () "Default theme and font size. Pendant: (presentation-mode)." (interactive) (mapcar 'disable-theme custom-enabled-themes) ;; (set-face-attribute 'default nil :height 150) ;; Themes ;; (set-frame-parameter nil 'background-mode 'dark) ;; Dark, High Contrast <- favorite (load-theme 'wombat) (setq frame-background-mode (quote dark)) ;; Dark, Low contrast ;; (load-theme 'darktooth) ;; Dark, Lowest contrast ;; (load-theme 'zenburn) )Configure light-mode theme and font size
(defun light-mode () "Enables a light theme." (interactive) ;; (set-face-attribute 'default nil :height 100) (mapcar 'disable-theme custom-enabled-themes) (load-theme 'spacemacs-light t))
(defun presentation-mode () "Presentation friendly theme and font size." (interactive) (load-theme 'leuven t) (mapcar 'disable-theme custom-enabled-themes) (set-face-attribute 'default nil :height 150))
"emacs-spacemacs-theme"Enable default theme and font
(add-hook 'server-after-make-frame-hook (lambda () ;; Do not load the light-mode in the terminal. Some colors ;; will then be wrong for both GUI and TUI. Could be an ;; error in spacemacs-light, but I will always start the ;; GUI first, anyway. (when (display-graphic-p) (light-mode)))) (light-mode)
If I wanted to make a distinction between GUI and terminal modes, the above would be good boilerplate, too.
Create a customized time table ready for CSV export.
Usage:
#+name: ok-timetable #+BEGIN_SRC elisp (ok-export-org-timetable "2018-05-09") #+END_SRC
When evaluating the src-block above, it’ll yield a table like:
#+RESULTS: ok-timetable | date | hours | task | |------------+--------+----------------------------------| | 2018-05-09 | 0:02 | #support | | 2018-05-09 | 0:17 | #support | |------------+--------+----------------------------------|
(require 'seq) (defun ok-filter-table-by-date (tbl from-date table-row) "Filter a TBL by FROM-DATE which is found in TABLE-ROW." ;; Sort by date (seq-sort '(lambda (e1 e2) (string-lessp (nth table-row e1) (nth table-row e2))) ;; Filter to start with FROM-DATE (seq-filter (lambda (elem) (let ((date-elem (nth table-row elem))) ;; >= (when (or (string-greaterp date-elem from-date) (string-equal date-elem from-date)) elem))) tbl))) (defun ok-hm-to-hours (worktime) "Casts HH:MM WORKTIME into a floating point number." (condition-case worktime (let* ((time (split-string worktime ":")) (minutes (/ (string-to-number (second time)) 60.0)) (hours (string-to-number (first time)))) (format "%.3f" (+ hours minutes))) (error 0))) (defun ok-split-hash-and-description (text) "Given a TEXT like '#tag1 #tag2 some description' and return tags and description as a list." ;; The concat is a little hack, so that there's always a minimum ;; description to be found (let ((text (concat text " "))) ;; A hashtag can have numbers, dashes and a-z (if (string-match "\\(#[a-z-0-9]+ \\)+" text) (let* ((hashtags (match-string 0 text)) ;; Couldn't figure out how to get the description ;; through an elisp regexp, so I'm just reading the ;; remainder of the text after all hashtags here (description (substring text (length hashtags) (length text)))) (list (string-trim hashtags) (string-trim description)))))) (defun ok-find-parent-of-type (elem-type elem) "For a child ELEM, find the closes parent element of type ELEM-TYPE." (let ((parent-elem (org-element-property :parent elem))) (if (eq elem-type (car parent-elem)) parent-elem (ok-find-parent-of-type elem-type parent-elem)))) (defun ok-generate-clock-table () "Generate a list of org elements of type 'clock." (let* ((ast (org-element-parse-buffer 'element))) ;; Map a function to all elements of TYPE 'clock which extracts ;; the TITLE, DURATION and DATE of a TODO. (org-element-map ast 'clock (lambda (clock-elem) (let* ((val (org-element-property :value clock-elem)) (task (ok-find-parent-of-type 'headline clock-elem)) (hash-and-description (ok-split-hash-and-description (org-element-property :title task)))) `(,(let ((year (org-element-property :year-start val)) (month (org-element-property :month-start val)) (day (org-element-property :day-start val))) (format "%4d-%02d-%02d" year month day )) ,(ok-hm-to-hours (org-element-property :duration clock-elem)) ,(first hash-and-description) ,(second hash-and-description))))))) (defun ok-export-org-timetable (from-date) "Generate a list from 'org-mode' clock elements starting from FROM-DATE." ;; Concatenate header, element data and footer into one list which ;; will automatically be rendered by org-mode as a table. (append '(("date" "duration" "hashtags" "description")) '(hline) ;; Generate tree of all visible elements within buffer (narrowing ;; works). (ok-filter-table-by-date (ok-generate-clock-table) from-date 0))) (defun ok-export-table-to (table-name target-path) "Exports the contents of a table called TABLE-NAME to a CSV file at TARGET-PATH." (progn (save-excursion (goto-char (point-min)) (search-forward (concat "RESULTS: " table-name)) (org-cycle) (evil-next-line) (org-table-export target-path "orgtbl-to-csv") (evil-previous-line)) (org-cycle)) (message "Table export completed"))Export Org Mode TODO headers into estimation table
(setq ok-export-org-estimations-file "~/src/200ok/admin/src/org-ok-estimations/org-ok-estimations.el") (unless (file-exists-p ok-export-org-estimations-file) (load-file ok-export-org-estimations-file))
(setq org-html-validation-link nil)Export planning information
(setq org-export-with-planning t)
This exports the Org mode Agenda to a ICS file, so that it can be consumed by any calendar application.
;; Setting dummy vars for private variables. (setq org-agenda-private-local-path "/tmp/dummy.ics") (setq org-agenda-private-remote-path "/sshx:user@host:path/dummy.ics") ;; Export the agenda for the next month (default is week) (setq org-agenda-span (quote month)) ;; Override the dummy values from above. (if (file-exists-p "~/.emacs.d/private_config.el") (load-file "~/.emacs.d/private_config.el")) (setq org-agenda-custom-commands `(;; Agenda to only show personal GTD files and filter work ;; related files which is easy to add with `#+FILETAGS: 200ok`. ("c" "Custom agenda, ignore 200ok tag" ((agenda "")) ((org-agenda-tag-filter-preset '("-200ok")))) ;; Define a custom command to save the org agenda to a file ("X" agenda "" nil ,(list org-agenda-private-local-path)))) (defun org-agenda-export-to-ics () (set-org-agenda-files) ;; Run all custom agenda commands that have a file argument. (org-batch-store-agenda-views) ;; Org mode correctly exports TODO keywords as VTODO events in ICS. ;; However, some proprietary calendars do not really work with ;; standards (looking at you Google), so VTODO is ignored and only ;; VEVENT is read. (with-current-buffer (find-file-noselect org-agenda-private-local-path) (goto-char (point-min)) (while (re-search-forward "VTODO" nil t) (replace-match "VEVENT")) (save-buffer)) ;; Copy the ICS file to a remote server (Tramp paths work). (copy-file org-agenda-private-local-path org-agenda-private-remote-path t))
Since I’m editing my Org files not just with Emacs, but also with organice, I’m doing this on a regular basis in a cron job which runs this code:
#!/bin/bash emacs -batch -l ~/.emacs.d/init.el -eval "(org-agenda-export-to-ics)" -kill if [[ "$?" != 0 ]]; then notify-send -u critical "exporting org agenda failed" fi
The hourly cron job looks like this:
0 * * * * /home/munen/bin/export-org-agenda.shIntegration to Google calendar
Google calendar supports subscribing to ICS feeds out of the box. However, it only updates the feed “every few hours” which seems to be 12-24h. With this Google AppsScript, this can be configured in a fine grained way: https://github.com/derekantrican/GAS-ICS-Sync
Manage TODO/FIXME/XXX commentshttps://github.com/tarsius/hl-todo
In the past, I’ve used comment-tags-mode. It’s last commit is 2017, while hl-todo is still actively developed in 2022 by tarsius.
hl-todo
highlights and lists comment tags such as ‘TODO’, ‘FIXME’, ‘XXX’.
(require 'hl-todo) (setq hl-todo-keyword-faces '(("TODO" . "#DF5427") ; A concrete TODO with actionable steps ("FIXME" . "#DF5427") ; A non-concrete TODO. We only know something is broken/amiss. ("HACK" . "#DF5427") ; Works, but is a code smell (quick fix). Might break down the line. ("CHECK" . "#CC6437") ; Assumption that needs to be verified. ("NOTE" . "#1FDA9A") ; Use to highlight a regular, but especially important, comment. ("INFO" . "#1FDA9A") ; Use to highlight a regular, but especially important, comment. ))
Define helpful shortcuts:
(define-key hl-todo-mode-map (kbd "C-c t p") 'hl-todo-previous) (define-key hl-todo-mode-map (kbd "C-c t n") 'hl-todo-next) (define-key hl-todo-mode-map (kbd "C-c t o") 'hl-todo-occur) (define-key hl-todo-mode-map (kbd "C-c t i") 'hl-todo-insert)
Enable hl-todo
mode where required:
(add-hook 'prog-mode-hook 'hl-todo-mode) (add-hook 'conf-mode-hook 'hl-todo-mode)
"emacs-hl-todo"
The following packages would be nice, in theory. In practice something is yet amiss, but it might be different in the future. That’s why I’m keeping them around and will try them at another time.
https://github.com/bburns/clipmon
Proposition: Monitors system clipboard and puts everything in the kill-ring.
Caveat: In theory, I liked the package. However, it seemed to cause racing conditions and crashed Emacs multiple times a day. When this is re-implemented in a non-blocking mode, this would be nice.
;; (add-to-list 'after-init-hook 'clipmon-mode-start)
Theoretically this is really nice to have functionality. However, I couldn’t run it for long. Emacs started freezing a lot on the day when I added this lib. I assume, because clipmon is blocking - and I always run multiple instances of Emacs in parallel. They might be in for a classic racing condition. Might be just another bug.
Other good Emacs configurationsModes I probably could use, but haven’t tried out, yet.
Kudos SirPscl
https://github.com/pezra/rspec-mode#usage
Increase selected region by semantic unitshttps://github.com/magnars/expand-region.el
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4