The macOS-adjusted early-init.el approach is taken from https://raw.githubusercontent.com/dylanjm/teton/master/config/emacs/dotemacs.org
These settings are loaded before anything else and are intended to speed up all the actual package loading and configuration options.
Effectively disables garbage collection with the memory limits set for Emacs ages ago. Speeds up loading all of the actual code and packages in the real init file.
(defvar default-file-name-handler-alist file-name-handler-alist)
;; The default is 800 kilobytes. Measured in bytes.
(defvar extended-gc-cons-threshold most-positive-fixnum)
(defvar default-gc-cons-threshold (* 100 1024 1024))
;; Native Compilation Vars
(setq-default native-comp-speed 2
native-comp-deferred-compilation t)
;; Prevents libgccjit error
;; Solution found at: https://github.com/d12frosted/homebrew-emacs-plus/issues/323
(setenv "LIBRARY_PATH" "/opt/homebrew/opt/gcc/lib/gcc/11:/opt/homebrew/opt/libgccjit/lib/gcc/11:/opt/homebrew/opt/gcc/lib/gcc/11/gcc/aarch64-apple-darwin20/11.1.0:/usr/local/opt/gcc/lib/gcc/11:/usr/local/opt/libgccjit/lib/gcc/11:/usr/local/opt/gcc/lib/gcc/11/gcc/x86_64-apple-darwin20/11.2.0")
(setq-default auto-window-vscroll nil
bidi-display-reordering 'left-to-right
bidi-paragraph-direction 'left-to-right
frame-inhibit-implied-resize t
inhibit-default-init t
site-run-file nil
load-prefer-newer t
read-process-output-max (* 1024 1024 3))
(setq file-name-handler-alist nil
package-enable-at-startup nil
gc-cons-threshold extended-gc-cons-threshold)
(defun arco/return-gc-to-default ()
(setq-default gc-cons-threshold default-gc-cons-threshold
load-prefer-newer nil))
(defun arco/reset-file-handler-alist-h ()
(dolist (handler file-name-handler-alist)
(add-to-list 'default-file-name-handler-alist handler))
(setq file-name-handler-alist default-file-name-handler-alist))
(add-hook 'after-init-hook #'arco/reset-file-handler-alist-h)
(add-hook 'after-init-hook #'arco/return-gc-to-default)
(advice-add #'package--ensure-init-file :override #'ignore)
(defun ct/use-face (font height weight)
(let ((the-font (format "%s-%d" font height)))
(message the-font)
(setq default-frame-alist
(append (list
`(font . ,the-font)
'(min-height . 1) '(height . 45)
'(min-width . 1) '(width . 101)
'(vertical-scroll-bars . nil)
'(internal-border-width . 24) ;; frame padding around the text
'(left-fringe . 0)
'(right-fringe . 0)
'(ns-transparent-titlebar . t)
'(menu-bar-lines . 0)
'(tool-bar-lines . 0)))))
(set-face-attribute 'default nil
:font font
:weight weight
:height (* height 10))
(set-face-attribute 'variable-pitch nil
:font "SF Pro Display"
:height (* height 10))
(set-face-attribute 'fixed-pitch nil
:font font
:height (* height 10)))
(defun ct/use-medium-face ()
"Use SF Mono Medium 18pt, for smaller screens"
(interactive)
(ct/use-face "JetBrains Mono" 18 'medium)
(setq-default line-spacing .3))
(defun ct/use-regular-face ()
"Use SF Mono Regular 22pt, for larger screens."
(interactive)
(ct/use-face "JetBrains Mono" 22 'light)
(setq-default line-spacing .2))
(defun ct/toggle-face ()
"Switch between large and small font settings."
(interactive)
(let ((font-height (/ (face-attribute 'default :height) 10)))
(if (< font-height 20)
(ct/use-regular-face)
(ct/use-medium-face))))
(global-set-key (kbd "<f6>") #'ct/toggle-face)
(ct/use-regular-face)
;; Map SF Symbols to a compatible font
(set-fontset-font t '(? . ?) "SF Pro Display")
When something takes suspiciously long, it’s easy to find out what happens with the built-in profiler:
M-x profiler-start
, select CPU usage- scroll in the big file for a while
M-x profiler-stop
M-x profiler-report
to show the measurements- for a clean session:
M-x profiler-reset
Enable some debugging flags to help with package setup and maintenance.
(setq debug-on-error nil)
Original by Lars Tveito https://github.com/larstvei/dot-emacs/blob/master/init.org using vanilla emacs function to auto-tangle.
For the first run, use C-c C-v t
to run org-babel-tangle
to produce the .el file.
;;; -*- lexical-binding: t -*-
The .el file will be overwritten by the tangled version of this file. Only ever change the org file.
I want lexical scoping for the init-file, which can be specified in the header. The first line of the configuration is as follows:
(defun tangle-init ()
"If the current buffer is 'init.org' the code-blocks are
tangled, and the tangled file is compiled."
(when (equal (buffer-file-name)
(file-truename (expand-file-name (file-name-concat user-emacs-directory "00-main.org"))))
;; Avoid running hooks when tangling.
(let ((prog-mode-hook nil))
(org-babel-tangle "00-main.org" "00-main.el")
(byte-compile-file (file-name-concat user-emacs-directory "00-main.el")))))
(add-hook 'after-save-hook 'tangle-init)
Asynchronous auto-tangling when you add #+auto_tangle: t
to the file properties at the top. Bonus: it doesn’t shove the compilation window in my face!
2021-02-17: Says it tangles, but I don’t see no tangling.
(use-package org-auto-tangle :defer t :hook (org-mode . org-auto-tangle-mode))
I often forget stuff pertaining package configurations, and get confused when which part of the config is actually going to be run.
- use
:config
to set up configurations that are lazily loaded - use
:init
to set up stuff that’s eagerly loaded, like(foo-mode +1)
enablement
(use-package PACKAGE_NAME ;; when executing, load from ELPA and install :demand ;; Some packages don't expose (all) runnable commands; fix these ;; so they are available in :init :commands (command1 command2 ...) ;; hide mode line/lighter entry; explained below when the =delight= package is used :delight (foo-mode1 foo-mode2 ...) :init ;; eagerly executed commands; no need to group via progn :config ;; lazily executed after loading; no need to group via progn ;; key bindings -- note that these ARE grouped in lists for globals and maps! :bind (("C-f C-o C-o" . global-foo-command) ("M-a b c" . global-bar-command)) (:map some-key-map ("C-i" . map-relative-fizz-command)) ;; Define hooks; doesn't have to be hooks for this module, can also be ;; built-in hooks executing functions of this module :hook (after-init-hook . foo-bar-auto-setup) (foo-bar-mode-hook . hook-function-name))
(require 'use-package-ensure)
(setq use-package-always-ensure t)
(setq-default mode-line-format
'("%e"
mode-line-front-space ;; XFK inserts mode info here
mode-line-frame-identification
mode-line-buffer-identification
" " ;; Sometimes this touches in magit buffers
mode-line-position
" "
mode-line-modes ;; to just show the major mode, use `mode-name'
"%n" ;; narrowing
mode-line-misc-info
mode-line-end-spaces))
With delight, you can replace the minor mode identification string with something else. Configure per-package using :delight
. Works like diminish, but also on dynamic mode line items, like projectile project names. So it overall works more reliably.
(use-package delight
:demand)
Usage examples because I always forget how to chain delight config statements (TL;DR: just throw each on a line, no need to make a list):
;; Built-in modes that needn't be loaded: (use-package emacs :delight (foo-bar-mode) ;; de-light completely (fizz-buzz-mode " FB")) ;; replace with "FB" ;; Combine package loading with configuration (use-package foo-bar :demand :delight ;; hide completely :config (foo-config))
https://emacsredux.com/blog/2020/09/12/reinstalling-emacs-packages/
- Deactivate first
- Ignore errors (to avoid stopping the update when dependencies would be violated)
(defun ct/reinstall-package (pkg)
(interactive (list (intern (completing-read "Reinstall package: " (mapcar #'car package-alist)))))
(ignore-errors
(unload-feature pkg t))
(package-reinstall pkg)
(require pkg))
(use-package try :ensure t)
via https://emacs.stackexchange.com/a/31874/18986
(defun package-menu-find-marks ()
"Find packages marked for action in *Packages*."
(interactive)
(occur "^[A-Z]"))
;; Only in Emacs 25.1+
(defun ct/package-menu-filter-by-status (status)
"Filter the *Packages* buffer by status."
(interactive
(list (completing-read
"Status: " '("new" "installed" "dependency" "obsolete"))))
(package-menu-filter-by-keyword (concat "status:" status)))
(define-key package-menu-mode-map "s" #'ct/package-menu-filter-by-status)
(define-key package-menu-mode-map "a" #'package-menu-find-marks)
(use-package savehist
:ensure t
:hook (after-init . savehist-mode))
(use-package exec-path-from-shell
:if (memq window-system '(mac ns x))
:init
(setq exec-path-from-shell-check-startup-files nil
exec-path-from-shell-variables '("SHELL" "PATH" "MANPATH")
exec-path-from-shell-arguments '("-l"))
(exec-path-from-shell-initialize))
Reveals the current file in Finder, like Cmd+Shift+R does in a lot of macOS apps.
(use-package reveal-in-osx-finder
:if (memq window-system '(mac ns))
:ensure t
:config (global-set-key (kbd "C-c z") 'reveal-in-osx-finder))
(use-package emacs
:if (memq window-system '(mac ns))
:config
(setq dired-use-ls-dired nil))
Shared settings between Mac and Linux:
(use-package emacs
:if (memq window-system '(mac ns x))
:config
(setq mouse-wheel-scroll-amount '(1 ((shift) . hscroll) ((control)) ((meta) . 5))
;; Populate kill buffer from clipboard
select-enable-clipboard t
frame-resize-pixelwise t
;; Make non-native full-screen on macOS cover the menu bar; native fullscreen glitches into 2 workspaces when composing email
ns-use-native-fullscreen nil)
;; When toggling fullscreen, ditch all existing posframes that would linger
(advice-add #'toggle-frame-fullscreen
:after
(lambda (&rest r) (posframe-delete-all)))
;; Disable window chrome eagerly; using it as late as `after-init-hook` produces glitches.
(set-frame-position nil 0 0)
(tool-bar-mode 0)
(scroll-bar-mode 0))
(use-package emacs
:if (memq window-system '(mac ns))
:config
(setq ns-alternate-modifier 'meta
mac-option-modifier 'meta
ns-right-alternate-modifier nil
mac-right-option-modifier nil ; Pass-through to regular option key
ns-command-modifier 'super
mac-command-modifier 'super
ns-right-command-modifier 'hyper
mac-right-command-modifier 'hyper
ns-confirm-quit t))
Exposes ct/dired-finder-path
as an interactive function on macOS to open Finder’s frontmost window’s location in dired
.
(when (memq window-system '(mac ns))
(defun ct/finder-path ()
"Return path of the frontmost Finder window, or the empty string.
Asks Finder for the path using AppleScript via `osascript', so
this can take a second or two to execute."
(let ($applescript $result)
;; Script via: https://brettterpstra.com/2013/02/09/quick-tip-jumping-to-the-finder-location-in-terminal/
(setq $applescript "tell application \"Finder\" to if (count of Finder windows) > 0 then get POSIX path of (target of front Finder window as text)")
(setq $result (ns-do-applescript $applescript))
(if $result
(string-trim $result)
"")))
(defun ct/dired-finder-path ()
(interactive)
(let (($path (ct/finder-path)))
(if (string-equal "" $path)
(message "No Finder window found.")
(dired $path)))))
Helpers to reopen file with privileges and execute commands as super user.
(use-package sudo-edit
:ensure)
Keychain access via the security
command line tool by Brad Wright:
(use-package emacs
:init
(defun bw/chomp (str)
"Chomp leading and tailing whitespace from `str'."
(while (string-match "\\`\n+\\|^\\s-+\\|\\s-+$\\|\n+\\'" str)
(setq str (replace-match "" t t str))) str)
(defun ct/get-keychain-password (service-name)
"Get SERVICE-NAME password from macOS Keychain.
Return nil if password was not found."
(interactive "sService name: ")
(when (executable-find "security")
(let ((password))
(setq password (bw/chomp
(shell-command-to-string
(concat "security find-generic-password -ws " service-name " 2>/dev/null"))))
(if (string-empty-p password)
nil
password)))))
There’s apparently no way for the Emacs-mac fork to bypass the limit of 1024 file descriptors.
I don’t know why so many file descriptors and watches accumulate anyway.
To close them, use this Emacs 29 code via Ben Simon:
(defun file-notify-rm-all-watches ()
"Remove all existing file notification watches from Emacs."
(interactive)
(maphash
(lambda (key _value)
(file-notify-rm-watch key))
file-notify-descriptors))
Line spacing actually adds spacing below the line; that means at 2x line height, the text would be stuck to the top and 1x line height would be added below. That means line backgrounds only extend downwards instead of vertically centering the text in the background-colored region.
The only “fix” for this seems to be to patch your font with more vertical spacing at the top :(
See https://stackoverflow.com/questions/26437034/emacs-line-height
I find 1.2x line height to not be as ugly as to be noticeable.
(use-package emacs
:config
(setq-default line-spacing .2)
(defun ct/disable-local-line-spacing ()
"Disable line spacing buffer-local, used for hooks."
(setq-local line-spacing 0)))
These themes by Protesilaos Stavrou are based on accessibility standards to maximize contrast. Aka, they should be easy to read at all times. But with the relatively stark contrast between text and background colors, it’s probably not that easy on the eye. That’s what Solarized is for.
- Code and Documentation on GitLab / the author’s init.el:
- Documentation: use color constants in customizations
(use-package modus-themes
:ensure
:demand
:init
(require 'modus-themes)
(setq modus-themes-to-toggle '(modus-operandi-deuteranopia modus-vivendi-deuteranopia))
(setq modus-themes-italic-constructs t
modus-themes-bold-constructs t
modus-themes-variable-pitch-ui t
modus-themes-mixed-fonts t)
;; Theme overrides
(customize-set-variable 'modus-themes-common-palette-overrides
`(
;; Make the mode-line borderless
(bg-mode-line-active bg-inactive)
(fg-mode-line-active fg-main)
(bg-mode-line-inactive bg-inactive)
(fg-mode-line-active fg-dim)
(border-mode-line-active bg-inactive)
(border-mode-line-inactive bg-main)
;; macOS Selection colors
(bg-region "mac:selectedTextBackgroundColor")
(fg-region "mac:selectedTextColor")
))
;; Inherit rest from faint style
;; ,@modus-themes-preset-overrides-faint))
(customize-set-variable 'modus-operandi-deuteranopia-palette-overrides
`(
;; More subtle gray for the inactive window and modeline
(bg-inactive "#efefef")))
(customize-set-variable 'modus-vivendi-deuteranopia-palette-overrides
`(
;; More subtle gray for the inactive window and modeline
(bg-inactive "#202020")))
;; Color customizations
(setq modus-themes-prompts '(bold))
(setq modus-themes-completions nil)
(setq modus-themes-org-blocks 'gray-background)
;; Font sizes for titles and headings, including org
(setq modus-themes-headings '((1 . (light variable-pitch 1.5))
(agenda-date . (1.3))
(agenda-structure . (variable-pitch light 1.8))
(t . (medium))))
;; By default, the ~org-mode~ TODO/NEXT/DONE faces look super ugly.
;; This adjusts the colors to the modus themes.
;; (setq org-todo-keyword-faces
;; '(("TODO" . modus-themes-refine-blue)
;; ("DOING" . (:inherit modus-themes-intense-yellow :weight bold))
;; ("NEXT" . modus-themes-active-blue)
;; ("WAITING" . modus-themes-intense-red)
;; ("CANCELLED" . modus-themes-hl-line)
;; ("HOLD" . modus-themes-intense-neutral)
;; ("PROJECT" . (:inherit modus-themes-mark-symbol :weight light))
;; ("DONE" . modus-themes-refine-green)))
;; Don't load immediately, but in after-init-hook so all other modifications further down can be prepared
(defun ct/modus-themes-init ()
(load-theme (car modus-themes-to-toggle)))
:bind ("<f5>" . modus-themes-toggle)
:hook (after-init . ct/modus-themes-init))
(setq window-divider-default-right-width 2
window-divider-default-bottom-width 2
window-divider-default-places t)
(window-divider-mode)
While Emacs 29 is “experimental”, Prot will keep the variable mode-line settings as an opt-in feature. https://protesilaos.com/codelog/2021-12-02-note-modus-emacs-29/
To automatically inherit from the default Emacs mode-line on Emacs 29, implement something like:
(defun ct/modus-themes-customize-mode-line ()
"Apply padding to mode-line via box that has the background color"
(modus-themes-with-colors
(custom-set-faces
`(mode-line ((,c :box (:line-width 10 :color ,bg-mode-line-active))))
`(mode-line-inactive ((,c :box (:line-width 10 :color ,bg-mode-line-inactive)))))))
(add-hook 'modus-themes-after-load-theme-hook #'ct/modus-themes-customize-mode-line)
The default command-log colors are too bright and have next to no contrast when the log window is in “disabled” background mode.
(defun ct/modus-themes-customize-command-log-faces ()
(modus-themes-with-colors
(custom-set-faces
`(command-log-command ((,c :foreground ,fg-alt :background ,bg-dim))
`(command-log-key ((,c :foreground ,fg-main :background ,bg-active)))))))
(when (package-installed-p 'command-log-mode)
(add-hook 'modus-themes-after-load-theme-hook #'ct/modus-themes-customize-command-log-faces))
Prot’s own lin-mode
extends hl-line-mode
to override the style of per-line seleciton interfaces, like consult, notmuch, org-agends, i.e. where you inherently operate on per-line content.
This frees hl-line-mode
colors to apply to highlighting in text documents again.
(use-package lin
:config
(customize-set-variable 'lin-face 'lin-mac-override-fg)
(require 'cl-seq)
(customize-set-variable
'lin-mode-hooks
(cl-union lin-mode-hooks
'(dired-sidebar-mode-hook
dired-mode-hook
magit-status-mode-hook
magit-log-mode-hook)))
(defun ct/neotree-lin-mode-hook ()
"Prioritizes hl-line faces over neotree's before enabling lin."
(hl-line-mode -1)
(setq-local hl-line-overlay-priority +50)
(lin-mode))
(add-hook 'neotree-mode-hook #'ct/neotree-lin-mode-hook))
(defun ct/modus-themes-tab-bar-colors ()
"Override tab faces to have even less variety"
(modus-themes-with-colors
(custom-set-faces
`(tab-bar ((,c
:height 0.8
:background ,bg-main
:box nil)))
`(tab-bar-tab ((,c
:background ,bg-main
:underline (:color ,blue-intense :style line)
:box (:line-width 2 :style flat-button))))
`(tab-bar-tab-inactive ((,c
:background ,bg-main
:box (:line-width 2 :style flat-button)))))))
(add-hook 'modus-themes-after-load-theme-hook #'ct/modus-themes-tab-bar-colors)
Mapping actual emoji characters to a monochrome font that shows their outline:
;; Map Emoji to a monochrome Emoji outline font
(set-fontset-font t 'emoji "Noto Emoji")
This doesn’t insert the actual images in the text, but replaces :)
and -_-
and <3
etc. on the fly with an image. Useful to add visual hooks to plain text documents and org-agenda
.
(use-package emojify
:ensure t
:config
(setq emojify-emoji-set "openmoji-v13.0")
(defun ct/disable-emojify-mode-in-prog-mode ()
(emojify-mode -1))
:init
;; (global-emojify-mode +1)
(add-hook 'prog-mode-hook #'ct/disable-emojify-mode-in-prog-mode))
When all-the-icons produces chinese characters, that might be due to font precedence rules.
- https://github.com/domtronn/all-the-icons.el - has, well, all the icons
- Programmatic access:
- file types via
(insert (all-the-icons-icon-for-file "foo.js"=))
- direct icons by name
(all-the-icons-wicon "tornado")
- file types via
- Programmatic access:
- https://github.com/iyefrat/all-the-icons-completion - adds e.g. file icons to
find-file
(use-package all-the-icons
:delight
:demand)
(use-package all-the-icons-dired
:load-path "~/.emacs.d/src/all-the-icons-dired"
:delight
:after all-the-icons
:demand
:config
(setq all-the-icons-dired-monochrome nil)
:hook (dired-mode . all-the-icons-dired-mode))
(use-package all-the-icons-ibuffer
:delight
:after all-the-icons
:demand
:hook (ibuffer-mode . all-the-icons-ibuffer-mode))
(use-package all-the-icons-completion
:delight
:after all-the-icons
:demand
:hook (marginalia-mode . all-the-icons-completion-marginalia-setup))
(defun ct/file-join (&rest args)
"Joins ARGS into an absolute path using `expand-file-name`."
;; expand-file-name expects arg1 to be relative, arg2 to be base, so we need to reverse the order
(seq-reduce (lambda (memo next) (expand-file-name next memo))
(cdr args) (car args)))
The vanilla key binding C-x C-e
executes eval-last-sexp
by default: if you place the point at the end of a line after an elisp expression, it’ll execute that and print the output in the minibuffer, unformatted.
Karthik Chikmagalur suggests to replace this with pp-eval-last-sexp
. That will open a *Pp Output Buffer*
to display a formatted result.
(global-set-key [remap eval-last-sexp] 'pp-eval-last-sexp)
With Xah-Fly-Keys, use a separate binding instead:
(with-eval-after-load "xah-fly-keys"
;; effectively <SPC , w>
(define-key 'xah-fly-w-keymap (kbd "w") #'pp-eval-last-sexp))
Andrea’s function lets you call ct/stub-elisp-defun
to insert a defun
based on the wishful-programmed code at point.
With point in any of these functions:
(call-the-rest (message-wishes-to-some-people (make-a-list-of-people))))
… ct/stub-elisp-defun
will work by stubbing a function definition for each.
;; https://ag91.github.io/blog/2020/12/31/top-down-elisping-a-simple-snippet-to-stub-a-function-while-your-are-designing-your-code/
(defun ct/stub-elisp-defun ()
"Stub an elisp function from symbol at point."
(interactive)
(let* ((fun (thing-at-point 'list 'no-properties)))
(when fun
(let* ((fun-list (car (read-from-string fun)))
(name (symbol-name (nth 0 fun-list)))
(args (cdr fun-list)))
(save-excursion
(or (search-backward "(defun" nil 't) (goto-char (point-min)))
(insert
(s-concat
"(defun "
name
" "
(format "%s" (--map (s-concat "arg" (number-to-string it)) (number-sequence 1 (length args))))
"\n \"SomeDocs\"\n nil)\n\n")))))))
emacs-async
allows file transfers from the local computer to a remote machine to be run in the background. When uploading video to my NAS, I found that copying over hundreds of MiB was halting my work in Emacs.
(use-package async
:config
(autoload 'dired-async-mode "dired-async.el" nil t)
(dired-async-mode 1))
Automatic (or programmatic) window switching obeys different rules than manual (or interactive) buffer switching, up until Emacs 27.1, where the switch-to-buffer-obey-display-actions
option was introduced. This makes manual
– via https://www.masteringemacs.org/article/demystifying-emacs-window-manager
(setq switch-to-buffer-obey-display-actions t
switch-to-buffer-in-dedicated-window 'pop)
C-x o
goes to the next window,
C-x O
(same but with shift held) goes to the previous one.
(global-set-key (kbd "C-x O") (lambda () (interactive) (other-window -1)))
By default, there’s no interactive way to change window-dedicated-p
. via https://www.masteringemacs.org/article/demystifying-emacs-window-manager
(defun ct/toggle-window-dedication ()
"Toggles window dedication in the selected window."
(interactive)
(set-window-dedicated-p (selected-window)
(not (window-dedicated-p (selected-window))))
;; When run interactively, report the result.
(if (and (interactive-p)
(window-dedicated-p (selected-window)))
(message "Enabled window dedication")
(message "Disabled window dedication")))
(defun ct/set-window-no-delete-other-windows ()
(interactive)
(set-window-parameter (selected-window) 'no-delete-other-windows t))
(defun ct/show-buffer-in-bottom-split ()
(interactive)
(let ((consult--buffer-display (lambda (buffer &rest args)
(display-buffer-in-side-window buffer '((side . bottom) (slot . 0) (dedicated . t) (height . 10))))))
(call-interactively #'consult-buffer)))
(add-to-list 'display-buffer-alist
'("\\*Help\\*"
(display-buffer-reuse-window display-buffer-pop-up-window)
(inhibit-same-window . t)))
(defun ct/display-buffer-compilation-mode-p (buffer-name action)
"Determine whether BUFFER-NAME is a compilation buffer."
(with-current-buffer buffer-name
(or (derived-mode-p 'compilation-mode)
(string-match (rx (| "*[Cc]ompilation*"
"*Compile-Log*"))
buffer-name))))
;; Make compilation buffers share a window (if visible) and hide by default.
(add-to-list 'display-buffer-alist
'(ct/display-buffer-compilation-mode-p
(display-buffer-reuse-mode-window display-buffer-reuse-window display-buffer-no-window)
(allow-no-window . t)))
;; (mode compilation-mode))) ; My matcher should suffice and this might be too specific for the Elisp Compile-Log
(with-eval-after-load 'notmuch
(defun ct/display-buffer-notmuch-content-p (buffer-name action)
"Matches all notmuch/mail related buffers to show them in the same tab"
(with-current-buffer buffer-name
(derived-mode-p 'notmuch-hello-mode 'notmuch-search-mode 'notmuch-show-mode 'notmuch-tree-mode 'notmuch-message-mode 'message-mode)))
(add-to-list 'display-buffer-alist
'(ct/display-buffer-notmuch-content-p
(display-buffer-in-tab display-buffer-reuse-window)
(ignore-current-tab . t)
(tab-name . "Mail"))))
Shortcut for me in case I need to reset this:
(setq display-buffer-alist nil)
And interactive changes:
(customize-variable 'display-buffer-alist)
Picking the Xcode default shortcut to show/hide the sidebar: Cmd-Opt-0
(global-set-key (kbd "M-s-0") #'window-toggle-side-windows)
Recursive minibuffers enables nesting commands:
(setq enable-recursive-minibuffers +1)
Press ? to list dispatch actions
(use-package ace-window
:ensure
:demand
:config
;; Enable selector even if only 1 window is present:
(setq aw-dispatch-always t)
;; Default selectors to the home row
(setq aw-keys '(?a ?s ?d ?f ?g ?h ?j ?k ?l))
:bind (("C-o" . ace-window)))
Add bindings for XFK, if loaded:
(with-eval-after-load "ace-window"
(with-eval-after-load "xah-fly-keys"
(define-key xah-fly-leader-key-map (kbd "w") #'ace-window) ; xah-fly-comma-keymap
(define-key xah-fly-command-map (kbd "6") #'ace-window))) ; xah-fly-upcase-sentence
(use-package emacs
:hook
(after-init . winner-mode))
I used zoom-window to temporarily show window in full frame (aka temporarily “unsplit”).
Instead of zooming, I can use winner-mode’s C-c <left>
to undo and C-c <right>
to redo window changes.
When bound to e.g. the XFK leader key, I can unsplit everything with 1 key and undo the unsplitting with another.
(use-package emacs
:config
(setq cursor-in-non-selected-windows nil))
On macOS, ⌃⌘ + arrow keys usually does the trick. (Or the square brackets, but I don’t particularly like that binding.)
(global-set-key (kbd "C-s-<left>") #'previous-buffer)
(global-set-key (kbd "C-s-<right>") #'next-buffer)
(use-package ibuffer
:ensure t
:init
;; Rewrite all programmatic calls to `list-buffers`. Should work without this.
;(defalias 'list-buffers 'ibuffer-other-window)
;; Override `list-buffers` shortcut with ibuffer
(global-set-key (kbd "C-x C-b") 'ibuffer-other-window))
Add org-mode-inspired shortcuts to jump between filter groups (like “headings”). The default bindings are <TAB>
and <s-TAB>
.
(define-key ibuffer-mode-map (kbd "C-c C-n") #'ibuffer-forward-filter-group)
(define-key ibuffer-mode-map (kbd "C-c C-p") #'ibuffer-backward-filter-group)
Hide empty filter groups:
(setq ibuffer-show-empty-filter-groups t)
Define custom groups for buffers
(setq ibuffer-saved-filter-groups
`(("default"
("mail" (or
(mode . message-mode)
(mode . notmuch-hello-mode)
(mode . notmuch-search-mode)
(mode . notmuch-message-mode)
(mode . notmuch-show-mode)
(mode . notmuch-tree-mode)
(mode . bbdb-mode)
(mode . mail-mode)
(mode . mu4e-main-mode)
(mode . gnus-group-mode)
(mode . gnus-summary-mode)
(mode . gnus-article-mode)
(name . "^\\..bdb$")))
("org" (or
(mode . org-agenda-mode)
(mode . diary-mode)
(name . "^\\*Calendar\\*$")
(name . "^diary$")
(filename . "Pending/org/")))
("dired" (mode . dired-mode))
("emacs" (or
(name . "^\\*package.*results\\*$")
(name . "^\\*Shell.*Output\\*$")
(name . "^\\*Compile-Log\\*$")
(name . "^\\*Completions\\*$")
(name . "^\\*Backtrace\\*$")
(name . "^\\*dashboard\\*$")
(name . "^\\*Messages\\*$")
(name . "^\\*scratch\\*$")
(name . "^\\*Appointment Alert\\*$")
(name . "^\\*info\\*$")
(name . "^\\*Help\\*$")))
)))
(defun ct/ibuffer-enable-saved-filter-groups ()
(ibuffer-switch-to-saved-filter-groups "default"))
(add-hook 'ibuffer-mode-hook #'ct/ibuffer-enable-saved-filter-groups)
;; Modify the default ibuffer column format
(setq ibuffer-formats
'((mark modified read-only locked " "
(name 20 20 :left :elide)
" "
(mode 16 16 :left :elide)
" "
filename-and-process)
(mark " "
(name 16 -1)
" " filename)))
(defun ct/split-window-below (arg)
(interactive "P")
(split-window-below arg)
(other-window 1))
(defun ct/split-window-right (arg)
(interactive "P")
(split-window-right arg)
(other-window 1))
(global-set-key [remap split-window-below] #'ct/split-window-below)
(global-set-key [remap split-window-right] #'ct/split-window-right)
The “modus” themes support this out of the box: the background of all inactive buffer windows are dimmed. Not the inactive windows, but the inactive buffers. That means if you open 2 windows for the same buffer, both are using the light color; if you have 2 windows for 2 buffers, one is darkened.
(use-package auto-dim-other-buffers
:ensure
:defer
:commands auto-dim-other-buffers-mode
:config
(setq auto-dim-other-buffers-dim-on-switch-to-minibuffer nil)
(setq auto-dim-other-buffers-dim-on-focus-out t))
I never know where “other window” is going to be.
Code from David Wilson that favors the current window”
(setq display-buffer-base-action
'((display-buffer-reuse-window ;; Without the `popper' settings, this full-screens the org calendar
display-buffer-reuse-mode-window
display-buffer-same-window
display-buffer-in-previous-window)))
Then when the compilation window jumps at me, I can hit C-`
to close it. Think of it like the Quake console, or the iTerm floating window.
There are tons of example options at: https://github.com/karthink/popper
(use-package popper
:ensure t
:bind (("C-`" . popper-toggle-latest)
("C-s-`" . popper-cycle) ;; Use echo area's M- key bindings instead
("C-~" . popper-toggle-type))
:config
;; If I move the defun and setq calls into config, popper won't work right away on init buffers.
:init
(defun ct/popper-popup-at-bottom-noselect (buffer &optional _alist)
"Display popup-buffer BUFFER at the bottom of the screen."
(save-selected-window
(display-buffer-in-side-window
buffer
`((window-height . ,popper-window-height)
(side . bottom)
(slot . 1)))))
(setq popper-reference-buffers
'("\\*Messages\\*"
"Output\\*$"
"Calendar"
"\\*Async Shell Command\\*"
help-mode
;; With 'user, these are actually handled by my display-buffer-alist
(emacs-lisp-compilation-mode . hide)
(compilation-mode . hide))
popper-display-control 'user
popper-display-function #'ct/popper-popup-at-bottom-noselect)
(popper-mode +1)
;; Show instructions at the bottom.
(popper-echo-mode +1))
Got the wrong splits? Rotate the layout with s-r
:
(use-package rotate
:demand
:bind ("s-r" . #'rotate-layout))
Via http://ergoemacs.org/emacs/emacs_new_empty_buffer.html – I bind this to <f1>
for convenience.
(defun xah-new-empty-buffer ()
"Create a new empty buffer.
New buffer will be named “untitled” or “untitled<2>”, “untitled<3>”, etc.
It returns the buffer (for elisp programing).
URL `http://ergoemacs.org/emacs/emacs_new_empty_buffer.html'
Version 2017-11-01"
(interactive)
(let (($buf (generate-new-buffer "untitled")))
(switch-to-buffer $buf)
(funcall initial-major-mode)
(setq buffer-offer-save t)
$buf))
;; (global-set-key (kbd "<f1>") #'xah-new-empty-buffer)
;; macOS-Specific Setting using the Cmd key
(when (memq window-system '(mac ns))
(global-set-key (kbd "s-n") #'xah-new-empty-buffer))
Since I can create buffers that don’t visit a file as scratchpads, I have to make sure I don’t accidentally kill them before having had a chance to save their contents. Buffers that aren’t visiting a file don’t ask to be saved. They just go away.
(defun ct/kill-current-buffer-ask-if-modified ()
"Kills current buffer immediately, unless it is modified. Ask for confirmation then."
(interactive)
(let ((buf (current-buffer)))
(if (buffer-modified-p buf)
(when (yes-or-no-p "Buffer modified. Kill anyway? ")
(kill-buffer buf))
(kill-buffer buf))))
;; macOS-Specific Setting using the Cmd key
(when (memq window-system '(mac ns))
(global-set-key (kbd "s-w") #'ct/kill-current-buffer-ask-if-modified)
;; s-w was delete-frame before, but Cmd-Shift-W to close macOS window with all its tabs fits better
(global-set-key (kbd "s-W") #'delete-frame))
The whole code is from https://gist.github.com/ieure/80638
(defun ct/frame-recenter (&optional frame)
"Center FRAME on the screen.
FRAME can be a frame name, a terminal name, or a frame.
If FRAME is omitted or nil, use currently selected frame."
(interactive)
(unless (eq 'maximised (frame-parameter nil 'fullscreen))
(let* ((frame (or frame
(selected-frame)))
(frame-w (frame-pixel-width frame))
(frame-h (frame-pixel-height frame))
;; frame-monitor-workarea returns (x y width height) for the monitor
(monitor-w (nth 2 (frame-monitor-workarea frame)))
(monitor-h (nth 3 (frame-monitor-workarea frame)))
(center (list (/ (- monitor-w frame-w) 2)
(/ (- monitor-h frame-h) 2))))
(apply 'set-frame-position (flatten-list (list frame center))))))
To automatically center the frame after creation and at startuip:
(add-hook 'after-init-hook #'ct/frame-recenter) ;; Is actually too early, need to wait for font and rest
(add-hook 'after-make-frame-functions #'ct/frame-recenter)
The default to split off a new frame, C-x 5 2
, is a bit cumbersome.
;; macOS-Specific Setting using the Cmd key
(when (memq window-system '(mac ns))
(global-set-key (kbd "s-n") #'make-frame-command))
If unsaved buffers exist, list them.
Source: https://archive.casouri.cat/note/2021/clean-exit/index.html
(defun ct/clean-exit ()
"Exit Emacs cleanly.
If there are unsaved buffer, pop up a list for them to be saved
before existing. Replaces ‘save-buffers-kill-terminal’."
(interactive)
(if (frame-parameter nil 'client)
(server-save-buffers-kill-terminal arg)
(if-let ((buf-list (seq-filter (lambda (buf)
(and (buffer-modified-p buf)
(buffer-file-name buf)))
(buffer-list))))
(progn
(pop-to-buffer (list-buffers-noselect t buf-list))
(message "s to save, C-k to kill, x to execute"))
(save-buffers-kill-emacs))))
(global-set-key [remap save-buffers-kill-terminal] #'ct/clean-exit)
tab-bar
since Emacs 27 means a list of window configurations. It’s displayed at the very top of the frame, outside the padding. Manage workspaces and appearances.tab-line
since Emacs 27 manages buffers, grouped by a criterion.
(use-package emacs
:config
(defface ct/tab-bar-numbers
'((t
:inherit tab-bar
:family "SF Compact"
:weight light))
"Face for tab numbers in both active and inactive tabs.")
(defvar ct/circle-numbers-alist
'((0 . "⓪")
(1 . "①")
(2 . "②")
(3 . "③")
(4 . "④")
(5 . "⑤")
(6 . "⑥")
(7 . "⑦")
(8 . "⑧")
(9 . "⑨"))
"Alist of integers to strings of circled unicode numbers.")
(defvar ct/box-numbers-alist
'((1 . "")
(2 . "")
(3 . "")
(4 . "")
(5 . "")
(6 . "")
(7 . "")
(8 . "")
(9 . "")
(0 . ""))
"Alist of integers to strings of SF Symbols with numbers in boxes.")
(defun ct/tab-bar-tab-name-format-default (tab i)
(let ((current-p (eq (car tab) 'current-tab)))
(concat
(propertize
(when (and tab-bar-tab-hints (< i 10)) (alist-get i ct/box-numbers-alist))
'face 'ct/tab-bar-numbers)
" "
(propertize
(concat (alist-get 'name tab)
(or (and tab-bar-close-button-show
(not (eq tab-bar-close-button-show
(if current-p 'non-selected 'selected)))
tab-bar-close-button)
""))
'face (funcall tab-bar-tab-face-function tab))
" ")))
(setq tab-bar-tab-name-format-function #'ct/tab-bar-tab-name-format-default
tab-bar-tab-hints t)
(setq tab-bar-close-button-show nil
tab-bar-close-button " \x00d7 ") ;; Cross multiplication character
(setq tab-bar-new-button-show nil
tab-bar-new-button " + ") ;; Thicker + than the flimsy default
(setq tab-bar-separator nil)
(setq tab-bar-format
'(;;tab-bar-format-history ;; forward/back buttons
tab-bar-format-tabs-groups
tab-bar-separator
;; tab-bar-format-add-tab ;; new tab button
tab-bar-format-align-right
tab-bar-format-global))
;; Display battery and time in `tab-bar-format-global' section:
(display-battery-mode +1)
(setq display-time-format "%Y-%m-%d %H:%M") ;; Override time format.
(setq display-time-default-load-average nil) ;; Hide load average.
(display-time-mode +1)
;; Bind 1-9 in the tab prefix map to switch to that tab.
(mapcar (lambda (tab-number)
(let ((funname (intern (format "ct/tab-bar-select-%d" tab-number)))
(docstring (format "Select tab %d by its absolute number." tab-number))
(key (kbd (format "%d" tab-number)))
(super-key (kbd (format "s-%d" tab-number))))
(eval-expression `(defun ,funname ()
,docstring
(interactive)
(tab-bar-select-tab ,tab-number)))
(eval-expression `(define-key tab-prefix-map ,key ',funname))
(eval-expression `(global-set-key ,super-key ',funname))))
(number-sequence 1 9))
:hook
(tab-bar-mode . tab-bar-history-mode)
(after-init . tab-bar-mode)
:bind
;; ("s-{" . tab-bar-switch-to-prev-tab)
;; ("s-}" . tab-bar-switch-to-next-tab)
("s-t" . tab-bar-new-tab)
("s-w" . tab-bar-close-tab) ; I constantly want to close a buffer this way.
("s-T" . tab-bar-undo-close-tab)
(:map tab-prefix-map
("<left>" . tab-bar-switch-to-prev-tab)
("<right>" . tab-bar-switch-to-next-tab)
("n" . tab-bar-new-tab)
))
You can call this similar to save-buffer-excursion
, only the tab will continue to exist; it’ll e.g. open a file in the background:
(save-tab-excursion (find-file "test.txt"))
This could potentially be bound to a prefix key, like other-tab-prefix
, but doing the work in the background.
(defmacro save-tab-excursion (&rest body)
"Opens a new tab in tab-line in the background and executes BODY
inside, then restores the previously selected tab."
`(progn
(tab-bar-new-tab)
(unwind-protect (progn ,@body)
(tab-bar-switch-to-recent-tab))))
(use-package emacs
:defer t
:if (memq window-system '(mac ns x))
:bind
(:map help-mode-map
("<swipe-left>" . help-go-back)
("<swipe-right>" . help-go-forward))
(:map Info-mode-map
("<swipe-left>" . Info-history-back)
("<swipe-right>" . Info-history-forward)))
(use-package emacs
:config
;; No file dialog
(setq use-file-dialog nil)
;; No dialog box for y-or-n etc.
(setq use-dialog-box nil)
;; Popup windows; set to nil to get ibuffer in a new frame instead
(setq pop-up-windows t))
(use-package emacs
:init
;; Disable auto-cleanup to fix a TRAMP related host lookup before we start recentf
;; URL https://stackoverflow.com/a/883010/1460929
;; URL https://www.emacswiki.org/emacs/RecentFiles#h5o-12
(setq recentf-auto-cleanup 'never)
(setq recentf-keep '(file-remote-p file-readable-p))
(recentf-mode 1)
(setq recentf-max-menu-items 50)
(setq recentf-max-saved-items 50))
Unlike auto-save-mode
that saves to a temporary file, I auto-save-visited-mode
cannot be disabled on a major mode basis in a hook. It’s a global setting and doesn’t work on a per-buffer/per-file basis.
I don’t want auto-save-visited-mode
to be enabled for message-mode
email drafts, for example.
- real-auto-save is like
auto-save-visited
but allows local enablement/disablement - super-save has customizable triggers (e.g. when finding files, switching buffers, …) on top of save intervals
(use-package real-auto-save
:demand
:delight
:init
(setq real-auto-save-interval 10) ;; in seconds
;; Patching this in until https://github.com/ChillarAnand/real-auto-save/pull/55 is merged
(defun turn-on-real-auto-save () (real-auto-save-mode 1))
(defun turn-off-real-auto-save () (real-auto-save-mode -1))
(define-globalized-minor-mode global-real-auto-save-mode
real-auto-save-mode turn-on-real-auto-save)
(global-real-auto-save-mode 1)
(add-hook 'message-mode-hook #'turn-off-real-auto-save))
My main org folder contains a .dir-locals.el
with (auto-revert-mode 1)
in it so these files automatically revert when the disk contents change.
Could use (global-auto-revert-mode t)
like https://www.nicklanasa.com/posts/emacs-syncing-dropbox-beorg suggests, but with revbufs
, one can manually revert all buffers and get a conflict resolution list:
(use-package revbufs
:commands (revbufs))
(setq backup-directory-alist
`((".*" . ,temporary-file-directory)))
(setq auto-save-file-name-transforms
`((".*" ,temporary-file-directory t)))
(setq save-silently t)
Beginning with Emacs 28, can also use C-x x g
(revert-buffer-quickly
)
(defun reload-file (&optional file)
"Revert buffer for FILE without confirmation.
When FILE is nil, revert the current buffer. When no FILE is
non-nil but no buffer was found, do nothing."
(interactive)
(if file
(if-let ((buffer (find-buffer-visiting file)))
(if (and (boundp 'auto-revert-mode) auto-revert-mode)
(auto-revert-buffer buffer)
(with-current-buffer buffer
(revert-buffer :ignore-auto :noconfirm)))
(message "No buffer found for file"))
(message "Reverting current buffer")
(revert-buffer :ignore-auto :noconfirm)))
move-file-to-trash
is an interactive function that can be used to trash instead of delete/rm’ing files. Emacs (for macOS at least) doesn’t define system-move-file-to-trash
which would be called first, so the trash-directory
value is used as the destination.
(when (memq window-system '(mac ns))
(defun system-move-file-to-trash (path)
"Moves file at PATH to the macOS Trash according to `move-file-to-trash' convention.
Relies on the command-line utility 'trash' to be installed.
Get it from: <http://hasseg.org/trash/>"
(shell-command (concat "trash -vF \"" path "\""
"| sed -e 's/^/Trashed: /'")
nil ;; Name of output buffer
"*Trash Error Buffer*")))
(setq trash-directory "~/.Trash") ;; fallback for `move-file-to-trash'
(setq delete-by-moving-to-trash t) ;; enable trash by default
Useful to restore cursor position in a file when revisiting. Idea from Xah Lee.
(use-package emacs
:init
(save-place-mode +1))
No audio notifications within emacs.
(setq ring-bell-function 'ignore)
Enable using a
to open the selected file/directory and exit the current buffer, instead of pushing another buffer on top of the stack:
(put 'dired-find-alternate-file 'disabled nil)
Enable guessing what to do (DWIM) with actions, like when 2 dired windows are next to each other, copy and rename/move will suggest the other side:
(setq dired-dwim-target t)
Notable shortcuts:
<a>
to open directory and bury current buffer<W>
forbrowse-url-of-dired-file
will open the system default application<w>
will enter wdired for text-based changes
Analog to C-x f
which opens a file, C-c f
shows files in dired.
Use this to select a directory, and then a wildcard pattern to find files in the directory hierarchy below the selection. You can then operate on the flat file listing as you would work with regular dired buffers.
(global-set-key (kbd "C-c f") #'find-name-dired)
The output of find
takes up more horizontal space than plain ls
listings; but dired-hide-details-mode
hides these columns just as well and makes the output usable.
(
inside of dired buffers toggles detail hiding. Hide details by default.
(require 'dired-x)
(add-hook 'dired-mode-hook #'dired-omit-mode)
(defun turn-on-dired-hide-details-mode () (dired-hide-details-mode +1))
(add-hook 'dired-mode-hook #'turn-on-dired-hide-details-mode)
Think Quicklook or mu4e/notmuch message list, where the file content is shown in a split-off window as you move the selection.
(use-package diranged
:load-path "/Users/ctm/.dotfiles/emacs.d/src/diranged"
:config
(setq diranged-kill-on-move t ; cleanup spawed buffers as we go
diranged-kill-on-exit t ; cleanup spawed buffers on exit
diranged-max-file-size 10 ; MB size limit for previewing files
diranged-steal-all-the-keys t) ; overwrite non-dired bindings
(defun ct/diranged-quit ()
(interactive)
"Toggles `diranged-mode' off."
(diranged-mode -1))
:bind
(:map dired-mode-map ("SPC" . diranged-mode))
(:map diranged-mode-map ("q" . ct/diranged-quit)))
(use-package dired-subtree
:ensure
:defer
:bind (:map dired-mode-map ("TAB" . dired-subtree-toggle)))
From source via GitHub: https://github.com/amno1/dired-auto-readme
Displays README files inline, like i
in dired would insert a sub directory at the bottom. Works with org and Markdown files.
(use-package dired-auto-readme
:load-path "~/.emacs.d/src/dired-auto-readme"
:demand
:hook (dired-mode . dired-auto-readme-mode)
:config)
(with-eval-after-load 'dired-auto-readme
(with-eval-after-load 'org-view-mode
(add-to-list 'dired-auto-readme-alist
'(org-mode . org-view-mode))))
(with-eval-after-load 'dired-auto-readme
(with-eval-after-load 'markdown-mode
(add-to-list 'dired-auto-readme-alist
'(markdown-mode . (lambda ()
(markdown-view-mode)
(font-lock-ensure))))))
(use-package dired-hist
:load-path "~/.emacs.d/src/dired-hist"
:hook (dired-mode . dired-hist-mode)
:bind
(:map dired-mode-map
("l". #'dired-hist-go-back)
("r" . #'dired-hist-go-forward)))
Posts and examples via xenodium.com:
(use-package dwim-shell-command
:ensure t
:bind (([remap shell-command] . dwim-shell-command)
:map dired-mode-map
([remap dired-do-async-shell-command] . dwim-shell-command)
([remap dired-do-shell-command] . dwim-shell-command)
([remap dired-smart-shell-command] . dwim-shell-command)))
Include the optional conversion utilities that ship with the package:
(with-eval-after-load 'dwim-shell-command
(require 'dwim-shell-commands))
;; Disable highlight of bookmarked line (since v28)
(setq-default bookmark-set-fringe-mark nil)
https://www.projectile.mx/en/latest/ https://www.emacswiki.org/emacs/NeoTree#toc7
Standard keybindings:
C-c p p counsel-projectile-switch-project Switch project C-c p f counsel-projectile-find-file Jump to a project file C-c p g counsel-projectile-find-file-dwim Jump to a project file using completion based on context C-c p d counsel-projectile-find-dir Jump to a project directory C-c p b counsel-projectile-switch-to-buffer Jump to a project buffer C-c p s g counsel-projectile-grep Search project with grep C-c p s s counsel-projectile-ag Search project with ag C-c p s r counsel-projectile-rg Search project with rg
(use-package projectile
:demand
:delight
:config
;; Global configuration
(setq projectile-switch-project-action 'treemacs-projectile ;'neotree-projectile-action
projectile-enable-caching t
projectile-create-missing-test-files t
projectile-switch-project-action #'projectile-commander
projectile-ignored-project-function 'file-remote-p)
;; PHP project type
(projectile-register-project-type 'php '("composer.json")
:src-dir "src"
:test "composer test"
:run "composer serve"
:test-suffix "Test"
:test-dir "tests")
;; PHP/Laravel project type
(projectile-register-project-type 'php-laravel '("composer.json" "artisan" "app" "routes")
:src-dir "app"
:run "php artisan serve"
:test "php artisan test"
:test-suffix "Test"
:test-dir "tests")
;; Nanoc website settings
(projectile-register-project-type 'ruby-nanoc '("nanoc.yaml" "Rules" "Gemfile")
:src-dir "content"
:compile "bundle exec nanoc compile"
:run "bundle exec nanoc view")
;; Always enable
(projectile-mode 1)
;; Don't try to find projectile project roots in remote/SSH projects via Tramp
;; https://www.murilopereira.com/how-to-open-a-file-in-emacs/#searching-files
(defadvice projectile-project-root (around ignore-remote first activate)
(unless (file-remote-p default-directory 'no-identification) ad-do-it))
:bind-keymap
("C-c p" . projectile-command-map))
Similar in prefix to Projectile, since Emacs 28.1, C-x p p
will switch between projects, and C-x p f
visits files in a project via the basename of the file (So C-x p f index
will show all index.html files in all directories).
Like Projectile, it has C-x p c
to compile, but nothing to distinguish different commands, e.g. compilation from testing.
The Silver Searcher aka “ag” search (which I can actually use as opposed to grep).
Bound to “a” because of “ag”.
(with-eval-after-load 'ag
(define-key project-prefix-map (kbd "a") #'ag-project))
Ripgrep sadly only is available via projectile, not via programmatic deadgrep invocation. Bound to “q” because it’s close to “a” on QWERTY keyboards, and not occupied.
(with-eval-after-load 'projectile
(define-key project-prefix-map (kbd "q") #'projectile-ripgrep))
With m
, go right to Magit status.
(with-eval-after-load 'project
;; Replacement for `with-eval-after-load 'magit` that doesn't require Magit to have been used prior to adding the shortcut
(when (fboundp 'magit)
(defun ct/project-magit-status (&rest args)
"Open `magit-status' in the project."
(interactive)
(magit-status (project-root (project-current t))))
;; The project-prefix-map binding is inherited for the project switching quick commands.
(define-key project-prefix-map "m" #'ct/project-magit-status)
(add-to-list 'project-switch-commands '(ct/project-magit-status "Magit") t)))
I often switch to a blog post project directory to create a new post. This involved finding a file that doesn’t exist yet for the current date’s content/posts/YYYY/MM/
pattern – so even the folder might not exist at the beginning of a new month. This key binding fixes that.
(with-eval-after-load 'project
;; The project-prefix-map binding is inherited for the project switching quick commands.
(define-key project-prefix-map "n" #'ct/project-create-post)
(add-to-list 'project-switch-commands '(ct/project-create-post "New post") t))
(defgroup project-local nil
"Local, non-VC-backed project.el root directories."
:group 'project)
(defcustom project-local-identifier ".project"
"Filename(s) that identifies a directory as a project.
You can specify a single filename or a list of names."
:type '(choice (string :tag "Single file")
(repeat (string :tag "Filename")))
:group 'project-local)
(cl-defmethod project-root ((project (head local)))
"Return root directory of current PROJECT."
(cdr project))
(defun project-local-try-local (dir)
"Determine if DIR is a non-VC project.
DIR must include a file with the name determined by the
variable `project-local-identifier' to be considered a project."
(if-let ((root (if (listp project-local-identifier)
(seq-some (lambda (n)
(locate-dominating-file dir n))
project-local-identifier)
(locate-dominating-file dir project-local-identifier))))
(cons 'local root)))
(customize-set-variable 'project-find-functions
(list #'project-try-vc
#'project-local-try-local))
Unlike NeoTree, Treemacs defines a workspace of directories. NeoTree shows the directory structure on the side, and that’s it. Treemacs shows the structure of any directory you add to the workspace next to each other.
I used neotree for about 2 years until I found that you can dig into documents with treemacs: TAB
will not only expand folder structures, but also expand files to show their TAGS – in Markdown documents, that means headings. Super useful to browse a project!
Remember to hit ?
in treemacs to get a comprehensive list of key bindings.
(use-package treemacs
:defer
:demand
:config
(setq treemacs-position 'left
treemacs-width 30
treemacs-show-hidden-files t)
;(setq treemacs--icon-size 16)
(treemacs-resize-icons 16)
;; Don't always focus the currently visited file
(treemacs-follow-mode -1)
(defun ct/treemacs-decrease-text-scale ()
(text-scale-decrease 1))
:bind
("<f8>" . treemacs)
:hook
(treemacs-mode . ct/treemacs-decrease-text-scale))
Use all-the-icons
to keep a consistent look across the system.
(use-package treemacs-all-the-icons
:after treemacs
:config
(treemacs-load-theme "all-the-icons")
;; Have to rely on customize to override the face to fix slanted inheritance form modus-theme
; '(treemacs-all-the-icons-file-face ((t (:inherit treemacs-file-face))))
)
C-c C-p p
to add projectile project to workspaceC-c C-p d
to delete project from workspace
(use-package treemacs-projectile
:demand
:after (projectile treemacs))
Neotree displays a foldable directory structure in a small-ish sidebar window. I prefer to keep the directory listing to the right; and I don’t want neotree to automatically reflect the directory of the current file within a project, but instead keep the project open.
(use-package neotree
:defer t
:after (all-the-icons)
:config
(defun ct/neotree-decrease-text-scale ()
(text-scale-decrease 1))
(setq neo-window-position 'left
neo-window-width 30
neo-mode-line-type 'none
neo-hide-cursor t ;; Use hl-line instead
;; When the neotree window is opened, let it find current file and jump to node.
neo-smart-open t
neo-autorefresh nil
;; Show all-the-icons icons in tree mode; use 'nerd when not available
neo-theme 'icons)
:bind
;; Shift-F8 bound to projectile below (if available)
(("<f8>" . neotree-toogle))
(:map neotree-mode-map
("M-<up>" . neotree-select-up-node)
("M-<down>" . neotree-change-root)
("M-n" . neotree-create-node))
:hook
(neotree-mode . ct/neotree-decrease-text-scale)
(neotree-mode . ct/disable-local-line-spacing)
(neotree-mode . turn-off-visual-line-mode))
Summon neotree
for the project root with S-<f8>
:
(with-eval-after-load "projectile"
(with-eval-after-load "neotree"
(defun ct/neotree-using-projectile-dir ()
"Open NeoTree using the projectile dir/git root instead of the current directory."
(interactive)
(let ((project-dir (projectile-project-root))
(file-name (buffer-file-name)))
(neotree-toggle)
(if project-dir
(if (neo-global--window-exists-p)
(progn
(neotree-dir project-dir)
(neotree-find file-name)))
(message "Could not find git project root."))))
(global-set-key (kbd "<S-f8>") #'ct/neotree-using-projectile-dir)))
(with-eval-after-load "xah-fly-keys"
(with-eval-after-load "neotree"
(define-key xah-fly-command-map (kbd "<f8>") #'neotree-toggle)
(define-key xah-fly-insert-map (kbd "<f8>") #'neotree-toggle)))
(with-eval-after-load "neotree"
(defun ct/neotree-variable-pitch-faces ()
(let ((sans-serif-family (face-attribute 'variable-pitch :family))
(all-neotree-faces '(neo-file-link-face neo-banner-face neo-button-face neo-header-face neo-vc-user-face neo-dir-link-face neo-root-dir-face neo-vc-added-face neo-vc-edited-face neo-expand-btn-face neo-vc-default-face neo-vc-ignored-face neo-vc-missing-face neo-vc-removed-face neo-vc-conflict-face neo-vc-up-to-date-face neo-vc-needs-merge-face neo-vc-needs-update-face neo-vc-unregistered-face neo-vc-unlocked-changes-face)))
(dolist (face all-neotree-faces)
(set-face-attribute face nil :family sans-serif-family))))
(add-hook 'neotree-mode-hook #'ct/neotree-variable-pitch-faces)
;; When toggling light/dark modus-theme, reset the neotree fonts as well.
(with-eval-after-load "modus-themes"
(add-hook 'modus-themes-after-load-theme-hook #'ct/neotree-variable-pitch-faces)))
Built-in speedbar
is capable of listing directory entries in a separate frame.
(require 'speedbar)
;; Enable file listing (as opposed to only directory listing)
(setq speedbar-show-unknown-files t)
(setq speedbar-indentation-width 4)
(set-face-attribute 'speedbar-button-face nil :bold nil)
(set-face-attribute 'speedbar-directory-face nil :bold nil)
(set-face-attribute 'speedbar-file-face nil :bold nil)
;; Hide commands that do not work in the current mode when calling M-x (via vertico.el README)
; (setq read-extended-command-predicate
; #'command-completion-default-include-p)
;; via https://github.com/milkypostman/dotemacs/blob/main/init.el
(defun ct/delete-backward-updir ()
"Delete char before or go up directory. Unlike deleting a word, skips file extension dot."
(interactive)
(if (eq (char-before) ?/)
(save-excursion
(goto-char (1- (point)))
(when (search-backward "/" (point-min) t)
(delete-region (1+ (point)) (point-max))))
(call-interactively 'backward-kill-word)))
Selectrum was quite unintrusive and minimal compared to ivy; vertico does even less and relies upon Emacs’s default completion and filtering system. Which is quite good!
Just displays a vertical completion list, instead of e.g. ido
’s minibuffer in-line completions.
(use-package vertico
:demand
:ensure
:delight
:config
(setq vertico-resize t)
;; Use `consult-completion-in-region', e.g for completions of M-: (eval-expression)
(setq completion-in-region-function
(lambda (&rest args)
(apply (if vertico-mode
#'consult-completion-in-region
#'completion--in-region)
args)))
:init
(vertico-mode +1)
:bind
(:map vertico-map
("C-p" . vertico-previous)
("C-n" . vertico-next)
("s-DEL" . ct/delete-backward-updir)))
Also enable mouse mode; am maintaining the package’s clone directly because the MELPA version does not include any of the extensions, and I want the mouse mode as backup.
(use-package vertico-mouse
:load-path "~/.emacs.d/src/vertico/extensions"
:demand
:commands (vertico-mouse-mode)
:after (vertico)
:init
(add-hook 'vertico-mode-hook #'vertico-mouse-mode))
Selectrum had similar capabilities out of the box. Vetico defaults to strict prefix matches.
When I remove this, check that this works the way I expect:
- Switching buffers: “Mess” should match
*Messages*
- Selecting headings in org file: substring matches should work, e.g. “vert” should match
**=vertico= bla bla
Orderless behaves like fzf
in the shell: fuzzy matching with space-separated substrings.
Daniel Meindlers config: https://github.com/minad/consult/wiki#minads-orderless-configuration
(use-package orderless
:demand t
:config
(defvar +orderless-dispatch-alist
'((?% . char-fold-to-regexp)
(?! . orderless-without-literal)
(?` . orderless-initialism)
(?= . orderless-literal)
(?~ . orderless-flex)))
(defun +orderless--suffix-regexp ()
(if (and (boundp 'consult--tofu-char) (boundp 'consult--tofu-range))
(format "[%c-%c]*$"
consult--tofu-char
(+ consult--tofu-char consult--tofu-range -1))
"$"))
;; Recognizes the following patterns:
;; * ~flex flex~
;; * =literal literal=
;; * %char-fold char-fold%
;; * `initialism initialism`
;; * !without-literal without-literal!
;; * .ext (file extension)
;; * regexp$ (regexp matching at end)
(defun +orderless-dispatch (word _index _total)
(cond
;; Ensure that $ works with Consult commands, which add disambiguation suffixes
((string-suffix-p "$" word)
`(orderless-regexp . ,(concat (substring word 0 -1) (+orderless--suffix-regexp))))
;; File extensions
((and (or minibuffer-completing-file-name
(derived-mode-p 'eshell-mode))
(string-match-p "\\`\\.." word))
`(orderless-regexp . ,(concat "\\." (substring word 1) (+orderless--suffix-regexp))))
;; Ignore single !
((equal "!" word) `(orderless-literal . ""))
;; Prefix and suffix
((if-let (x (assq (aref word 0) +orderless-dispatch-alist))
(cons (cdr x) (substring word 1))
(when-let (x (assq (aref word (1- (length word))) +orderless-dispatch-alist))
(cons (cdr x) (substring word 0 -1)))))))
;; Define orderless style with initialism by default
(orderless-define-completion-style +orderless-with-initialism
(orderless-matching-styles '(orderless-initialism orderless-literal orderless-regexp)))
;; You may want to combine the `orderless` style with `substring` and/or `basic`.
;; There are many details to consider, but the following configurations all work well.
;; Personally I (@minad) use option 3 currently. Also note that you may want to configure
;; special styles for special completion categories, e.g., partial-completion for files.
;;
;; 1. (setq completion-styles '(orderless))
;; This configuration results in a very coherent completion experience,
;; since orderless is used always and exclusively. But it may not work
;; in all scenarios. Prefix expansion with TAB is not possible.
;;
;; 2. (setq completion-styles '(substring orderless))
;; By trying substring before orderless, TAB expansion is possible.
;; The downside is that you can observe the switch from substring to orderless
;; during completion, less coherent.
;;
;; 3. (setq completion-styles '(orderless basic))
;; Certain dynamic completion tables (completion-table-dynamic)
;; do not work properly with orderless. One can add basic as a fallback.
;; Basic will only be used when orderless fails, which happens only for
;; these special tables.
;;
;; 4. (setq completion-styles '(substring orderless basic))
;; Combine substring, orderless and basic.
;;
(setq completion-styles '(orderless basic)
completion-category-defaults nil
;;; Enable partial-completion for files.
;;; Either give orderless precedence or partial-completion.
;;; Note that completion-category-overrides is not really an override,
;;; but rather prepended to the default completion-styles.
;; completion-category-overrides '((file (styles orderless partial-completion))) ;; orderless is tried first
completion-category-overrides '((file (styles partial-completion)) ;; partial-completion is tried first
;; enable initialism by default for symbols
(command (styles +orderless-with-initialism))
(variable (styles +orderless-with-initialism))
(symbol (styles +orderless-with-initialism)))
orderless-component-separator #'orderless-escapable-split-on-space ;; allow escaping space with backslash!
orderless-style-dispatchers '(+orderless-dispatch)))
This is pretty nifty:
C-b
to switch buffers, then use narrowing to start switching to stuff based on its type:- default:
b SPC
/f SPC
/m SPC
/v SPC
to narrow to buffers, files, bookmarks and views respectively. - via config below:
<
is the “narrow” prefix, so< b
will narrow to buffers, too.
- default:
C-c o
to navigate outlines navigates imenu tags well; not sure if I prefer my trusty oldC-. C-.
where I can see the breadcrumb trail, or less cramped lines instead.M-g l
to go to line (with matched text in it)M-g e
to go to error (relevant toprog-mode
)
(use-package consult
:bind (("C-. C-." . consult-imenu) ;; Got used to use this for jumping to symbols
("C-c h" . consult-history)
("C-x b" . consult-buffer)
("C-x 4 b" . consult-buffer-other-window)
("C-x 5 b" . consult-buffer-other-frame)
("C-x r x" . consult-register)
; ("C-x r b" . consult-bookmark) ;; Had trouble with this; ust `C-b < m` instead
("M-g g" . consult-goto-line)
("M-g M-g" . consult-goto-line)
;; M-g for "goto", but M-s for "search" also makes sense
("M-g o" . consult-outline) ;; "M-s o" is a good alternative
("M-g m" . consult-mark) ;; "M-s m" is a good alternative
("M-g l" . consult-line) ;; "M-s l" is a good alternative
("M-g i" . consult-imenu) ;; "M-s i" is a good alternative
("M-g e" . consult-error) ;; "M-s e" is a good alternative
("M-g m" . consult-multi-occur)
("<help> a" . consult-apropos)
("M-y" . consult-yank-from-kill-ring))
:init
;; Replace functions (consult-multi-occur is a drop-in replacement)
(fset 'multi-occur #'consult-multi-occur)
:config
;; Optionally configure narrowing and widening keys.
;; Both < and C-+ work reasonably well.
(setq consult-narrow-key "<"
consult-widen-key ">")
;; Disable previews.
(setq consult-preview-key nil)
;; Optional configure a "view" library to be used by `consult-buffer`.
;; The view library must provide two functions, one to open the view by name,
;; and one function which must return a list of views as strings.
;; Example: https://github.com/minad/bookmark-view/
;; (setq consult-view-open-function #'bookmark-jump
;; consult-view-list-function #'bookmark-view-names)
)
With consult-dir
called directly, I can switch context to a directory and find a file inside. That’s like a poor man’s project.el
During minibuffer completions, consult-dir
will allow picking a directory and insert its full path into the minibuffer. E.g. when saving an email attachment, this allows picking a recent target folder.
(use-package consult-dir
:ensure
:demand
:config
(define-key minibuffer-local-completion-map (kbd "C-x C-d") #'consult-dir)
(define-key minibuffer-local-completion-map (kbd "C-x C-j") #'consult-dir-jump-file)
(define-key global-map (kbd "C-x C-d") #'consult-dir))
(with-eval-after-load "xah-fly-keys"
(with-eval-after-load "consult-dir"
;; SPC i o
(define-key xah-fly-c-keymap (kbd "o") #'consult-dir)))
(use-package consult-flycheck
:demand
:after consult
:bind (:map flycheck-command-map
("!" . consult-flycheck)))
Shows parts of docstrings for commands, and some buffer metadata when switching buffers.
(use-package marginalia
:after (vertico consult)
:demand
:init
(marginalia-mode)
(setq marginalia-annotators '(marginalia-annotators-heavy marginalia-annotators-light nil)))
- Prefix
embark-act
(C-,=) with =C-u
to not leave the current context and act ‘transiently’, by which I mean: branch off temporaliry, but continue where you are once finished.- Example:
C-x C-f
to find a file, complete a path, and while you’re at it,C-u C-,= act on a file. When you're done acting on the file, you can continue with the completion in the minibuffer. If you omit the universal argument prefix, =embark-act
replaces the completion context, so you’d have to start over again. You can add files as you find them in the completion to the kill ring viaC-u C-, w
for example, or attach a file to an email, etc.
- Example:
- In any “embark” context, you can use
C-h
to show a list of available actions and use auto-completion to explore what’s available. - Fifteen ways to use embark does a great job demonstrating its use!
- embark-act
- Open any buffer by splitting any window
- Copy a file to a remote location when finding a file
- Insert a minibuffer candidate into the buffer
- Run a shell command on a minibuffer candidate file without losing your session
- Open a file as root without losing your session
- Upload a region of text to 0x0
- Visit a package’s URL from the minibuffer
- Set a variable from anywhere it appears in a buffer
- Add a keybinding for a command name from anywhere it appears
- embark-export
- Export Emacs package candidates to a package menu
- Collect imenu candidates in an “imenu-list”
- perists imenu candidates in a buffer: like a table of contents; with
embark-collect-direct-action-minor-mode
turned on, allows you to use theembark-act
key bindings directly from the collect buffer!
- perists imenu candidates in a buffer: like a table of contents; with
- Export file candidates to a dired-buffer
- Export buffer candidates to ibuffer
- Export variable candidates to a customize buffer
- Export grep or line candidates to a grep buffer
- embark-act
(use-package embark
:ensure t
:config
;; Noob mode: Always display available actions
(setq embark-prompter #'embark-keymap-prompter)
;; Show a live-filterable list of key bindings when C-h'ing with active prefix/leader key
(setq prefix-help-command #'embark-prefix-help-command)
;; Custom mailto: link matcher for email actions
(defun embark-refine-mailto-url-type (_type url)
"If URL is a mailto link, strip the mailto: and classify it as `mailto'."
(if (string-prefix-p "mailto:" url)
(cons 'mailto (string-remove-prefix "mailto:" url))
(cons 'url url)))
(add-to-list 'embark-transformer-alist '(url . embark-refine-mailto-url-type))
(defun mailto-compose-new (recipient)
"Compose mail to RECIPIENT."
(interactive "sTo: ")
(compose-mail recipient))
(defvar-keymap embark-mailto-map
:doc "Keymap for Embark mailto actions."
:parent embark-url-map
"c" #'mailto-compose-new)
(add-to-list 'embark-keymap-alist '(mailto . embark-mailto-map))
(defun ct/attach-file (file)
"Attach FILE to an email message.
The message to which FILE is attached is chosen as for `gnus-dired-attach`."
(interactive "fAttach: ")
(require 'gnus-dired)
(ct/with-notmuch-as-compose-mail
(gnus-dired-attach (list file))))
(defmacro ct/embark-split-action (fn split-type)
`(defun ,(intern (concat "ct/embark-"
(symbol-name fn)
"-"
(car (last (split-string
(symbol-name split-type) "-"))))) ()
(interactive)
(funcall #',split-type)
(call-interactively #',fn)))
(define-key embark-file-map (kbd "2") (ct/embark-split-action find-file split-window-below))
(define-key embark-file-map (kbd "3") (ct/embark-split-action find-file split-window-right))
(define-key embark-buffer-map (kbd "2") (ct/embark-split-action switch-to-buffer split-window-below))
(define-key embark-buffer-map (kbd "3") (ct/embark-split-action switch-to-buffer split-window-right))
(define-key embark-bookmark-map (kbd "2") (ct/embark-split-action bookmark-jump split-window-below))
(define-key embark-bookmark-map (kbd "3") (ct/embark-split-action bookmark-jump split-window-right))
:hook
(minibuffer-setup-hook . embark-collect-completions-after-input) ;;embark-live-occur-after-input)
:bind
(("C-," . embark-act)
("M-," . embark-dwim) ;; Execute DWIM action w/o prompting for action
(:map minibuffer-local-completion-map
("C-," . embark-act)
("C-o" . embark-export))
(:map embark-region-map
("Xc" . calc-grab-region))
(:map embark-file-map
("r" . reload-file)
("R" . rename-file)
("s" . sudo-edit-find-file)
("a" . ct/attach-file))
(:map embark-become-file+buffer-map
("s" . sudo-edit-find-file))
)
)
(with-eval-after-load "embark"
(with-eval-after-load "ace-window"
(message "both loaded")
(defmacro ct/embark-ace-action (fn)
`(defun ,(intern (concat "ct/embark-ace-" (symbol-name fn))) ()
(interactive)
(with-demoted-errors "%s"
(require 'ace-window)
(aw-switch-to-window (aw-select nil))
(call-interactively (symbol-function ',fn)))))
(define-key embark-bookmark-map (kbd "o") (ct/embark-ace-action bookmark-jump))
(define-key embark-buffer-map (kbd "o") (ct/embark-ace-action switch-to-buffer))
(define-key embark-file-map (kbd "o") (ct/embark-ace-action find-file))
))
Makes embark-export work better with the results from e.g. consult-ripgrep
or consult-find-file
.
(use-package embark-consult
:after (embark consult))
Dabbrev keeps a track of buffer contents.
When I manage changes in Magit, dabbrev is responsible for suggesting type names as I write commit messages.
On its own, dabbrev only offers cycling through candidates via M-/
and C-M-/
, but with packages like fancy-dabbrev
, this can become a completion framework with a pop-up UI on its own. The matching is just really basic.
(use-package dabbrev
:custom
;; Don't try to populate symbols/words/... from images and PDFs.
(dabbrev-ignored-buffer-regexps '("\\.\\(?:pdf\\|jpe?g\\|png\\)\\'")))
On macOS, install a global ctags database:
brew install --HEAD universal-ctags/universal-ctags/universal-ctags
On Linux, use ctags-snap, on Windows ctags-win32.
Was initially looking into Corfu for fuzzy completions and found this config https://pastebin.com/raw/pndhHBUL originally via Reddit.
Unlike company, which I used for 3 years, Corfu turns out to come with posframe-based UI, hooks into (modern) Emacs completion functions, and plays well with other packages I have installed: orderless
in particular which already provides some kind of fuzzy matching.
Corfu suggests to bind the tab key (maybe even on a per-buffer basic) to complete. I find robust indentation to be more important, so I rely on M-i
.
M-h
: show help buffer for function/suggestionM-g
: show definitionM-d
: show doc popup
(setq-local tab-always-indent 'complete)
(use-package corfu
:ensure t
:demand t
:config
;; (global-corfu-mode)
(setq
corfu-auto t
corfu-auto-delay 0.1
corfu-auto-prefix 3
corfu-cycle t
corfu-separator "*"
corfu-quit-at-boundary 'separator ; Stop completion at e.g. spaces, but not at '*'.
corfu-preselect-first t
corfu-quit-no-match t
corfu-preview-current 'insert
corfu-on-exact-match 'insert
corfu-scroll-margin 3)
(defun corfu-move-to-minibuffer ()
"Moves the completion candidates into the minibuffer.
This provides more robust fuzzy matching."
(interactive)
(let ((completion-extra-properties corfu--extra)
completion-cycle-threshold completion-cycling)
(apply #'consult-completion-in-region completion-in-region--data)))
:hook
(after-init . global-corfu-mode)
:bind
("M-i" . completion-at-point)
(:map corfu-map
("M-p" . corfu-doc-scroll-down) ;; was: corfu-next
("M-n" . corfu-doc-scroll-up) ;; was: corfu-previous
("M-m" . corfu-move-to-minibuffer)))
The best component separator for orderless
is space – but only in minibuffer completions for commands, filtering variables/functions, and finding files. This is the default, and it’s good.
When using orderless
with corfu
for completions in the buffer, I need a non-space character there to trigger somewhat “fuzzy” component matching.
Using *
enables me to write Ba*Br
to match BananaBread
.
In the minibuffer, this would work with space just fine. But I didn’t find a robust configuration that would allow space in the buffer without quitting Corfu’s completion UI when using space to separate words. While writing, this is getting annoying quickly.
On Reddit, there was a discussion using M-SPC
to insert a Unicode character as a separator. Might as well use *
or something else. (Non-breaking space, easily inserted via A-SPC
on Mac, doesn’t work by the way.)
(with-eval-after-load 'corfu
(with-eval-after-load 'orderless
;; Configure orderless default (→ in buffer) to separate with an asterisk:
(setq orderless-component-separator "*")
;; Then re-enable SPC in the minibuffer as the completion separator.
(add-hook 'minibuffer-setup-hook
(lambda () (setq-local orderless-component-separator " ")))
))
https://github.com/minad/corfu#completing-in-the-eshell-or-shell
(with-eval-after-load 'corfu
(add-hook 'eshell-mode-hook
(lambda ()
(setq-local corfu-auto nil)
(corfu-mode))))
Similar to how company
ingests many sources, CAPE can patch multiple sources into the default Emacs completion-at-point
mechanism, and thus into Corfu as well.
CAPE could also import company backends:
;; For `company-yasnippet'. (use-package company :ensure t) (defvar cape-yasnippet (cape-company-to-capf 'company-yasnippet)) (defun cape-setup () "Set up `cape'." (let* ((has-t (memq 't completion-at-point-functions)) (copy (delete 't completion-at-point-functions)) (caps `(,@copy cape-dabbrev cape-keyword ,cape-yasnippet))) (when (memq major-mode '(emacs-lisp-mode)) (setq caps (append caps '(cape-symbol)))) (when caps (setq-local completion-at-point-functions `(,(apply 'cape-super-capf caps)))) (when has-t (setq-local completion-at-point-functions (append completion-at-point-functions '(t)))))) (add-hook 'prog-mode-hook 'cape-setup 100) (add-hook 'org-mode-hook 'cape-setup 100)
Actual setup for a general-purpose setup:
(use-package cape
:ensure t
:init
:config
(setq cape-dabbrev-min-length 1)
:init
(add-to-list 'completion-at-point-functions #'cape-dabbrev)
(add-to-list 'completion-at-point-functions #'cape-file)
(add-to-list 'completion-at-point-functions #'cape-history)
(add-to-list 'completion-at-point-functions #'cape-keyword)
;;(add-to-list 'completion-at-point-functions #'cape-tex)
;;(add-to-list 'completion-at-point-functions #'cape-sgml)
;;(add-to-list 'completion-at-point-functions #'cape-rfc1345)
;;(add-to-list 'completion-at-point-functions #'cape-abbrev)
;;(add-to-list 'completion-at-point-functions #'cape-ispell)
;;(add-to-list 'completion-at-point-functions #'cape-dict)
;;(add-to-list 'completion-at-point-functions #'cape-symbol)
;;(add-to-list 'completion-at-point-functions #'cape-line)
(define-prefix-command 'ct/cape-map)
(global-set-key (kbd "M-I") 'ct/cape-map)
(when (window-system)
;; unbind C-i from TAB in GUI emacs
(define-key input-decode-map [?\C-i] [C-i])
(global-set-key (kbd "<C-i>") 'indent-region))
(define-key ct/cape-map "p" #'completion-at-point)
(define-key ct/cape-map "t" #'complete-tag) ; etags
(define-key ct/cape-map "d" #'cape-dabbrev) ; basically `dabbrev-completion'
(define-key ct/cape-map "f" #'cape-file)
(define-key ct/cape-map "k" #'cape-keyword)
(define-key ct/cape-map "s" #'cape-symbol)
(define-key ct/cape-map "a" #'cape-abbrev)
(define-key ct/cape-map "h" #'cape-history) ; prefer C-c h 'consult-history
(define-key ct/cape-map "i" #'cape-ispell)
(define-key ct/cape-map "l" #'cape-line)
(define-key ct/cape-map "w" #'cape-dict)
(define-key ct/cape-map "\\" #'cape-tex)
(define-key ct/cape-map "_" #'cape-tex)
(define-key ct/cape-map "^" #'cape-tex)
(define-key ct/cape-map "&" #'cape-sgml)
(define-key ct/cape-map "r" #'cape-rfc1345)
)
Trigger with the completion hotkey (M-i
in my case) after typing the <
opening angle bracket used from org-tempo.
(use-package org-block-capf
:load-path "~/.emacs.d/src/org-block-capf"
:config
(add-hook 'org-mode-hook #'org-block-capf-add-to-completion-at-point-functions))
Set the first day of the week to Monday and time to 24h mode.
(use-package emacs
:config
(setq calendar-week-start-day 1)
(setq display-time-24hr-format t))
(with-eval-after-load 'eshell
(defun ct/eshell-setup ()
;; Populate imenu; via <https://xenodium.com/imenu-on-emacs-eshell/>
(setq-local imenu-generic-expression
'(("Prompt" " $ \\(.*\\)" 1))))
(add-hook 'eshell-mode-hook #'ct/eshell-setup)
;; Set pager to `cat` (instead of less) because that works better in Emacs buffers
(add-hook 'eshell-load-hook
(lambda () (setenv "PAGER" "/bin/cat"))))
Keymap to add interactive files and directories via: https://www.emacswiki.org/emacs/EshellEnhancedLS
(eval-after-load "em-ls"
'(progn
(defun ted-eshell-ls-find-file-at-point (point)
"RET on Eshell's `ls' output to open files."
(interactive "d")
(find-file (buffer-substring-no-properties
(previous-single-property-change point 'help-echo)
(next-single-property-change point 'help-echo))))
(defun pat-eshell-ls-find-file-at-mouse-click (event)
"Middle click on Eshell's `ls' output to open files.
From Patrick Anderson via the wiki."
(interactive "e")
(ted-eshell-ls-find-file-at-point (posn-point (event-end event))))
(let ((map (make-sparse-keymap)))
(define-key map (kbd "RET") 'ted-eshell-ls-find-file-at-point)
(define-key map (kbd "<return>") 'ted-eshell-ls-find-file-at-point)
(define-key map (kbd "<mouse-2>") 'pat-eshell-ls-find-file-at-mouse-click)
(defvar eshell-ls-file-keymap map))
(defadvice eshell-ls-decorated-name (after ted-electrify-ls activate)
"Eshell's `ls' now lets you click or RET on file names to open them."
(add-text-properties 0 (length ad-return-value)
(list 'help-echo "RET, mouse-2: visit this file"
'mouse-face 'highlight
'keymap eshell-ls-file-keymap)
ad-return-value)
ad-return-value)))
Code from https://www.reddit.com/r/emacs/comments/xboh0y/how_to_put_icons_into_eshell_ls/
(with-eval-after-load 'all-the-icons
(with-eval-after-load 'em-ls
(defun lem-eshell-prettify (file)
"Add features to listings in `eshell/ls' output.
The features are:
1. Add decoration like 'ls -F':
* Mark directories with a `/'
* Mark executables with a `*'
2. Make each listing into a clickable link to open the
corresponding file or directory.
3. Add icons (requires `all-the-icons`)
This function is meant to be used as advice around
`eshell-ls-annotate', where FILE is the cons describing the file."
(let* ((name (car file))
(icon (if (eq (cadr file) t)
(all-the-icons-icon-for-dir name)
(all-the-icons-icon-for-file name)))
(suffix
(cond
;; Directory
((eq (cadr file) t)
"/")
;; Executable
((and (/= (user-uid) 0) ; root can execute anything
(eshell-ls-applicable (cdr file) 3 #'file-executable-p (car file)))
"*"))))
(cons
(concat " "
icon
" "
(propertize name
'keymap eshell-ls-file-keymap
'mouse-face 'highlight
'file-name (expand-file-name (substring-no-properties (car file)) default-directory))
(when (and suffix (not (string-suffix-p suffix name)))
(propertize suffix 'face 'shadow)))
(cdr file)
)))
(advice-add #'eshell-ls-annotate :filter-return #'lem-eshell-prettify)))
- INACTIVE timestamp (
C-c !
): Insert a date but don’t process for the agenda at all. - PLAIN timestamp (
C-c .
): Appointments/calendar events that don’t carry over after their time - SCHEDULED timestamp (
C-c C-s
): Shows on the agenda; if not completed, will show up until DONE. - DEADLINE timestamp (
C-c C-d
): By when something must be completed; comes with a warning ahead of time in the agenda.
(require 'org)
(require 'org-mouse) ;; Require mouse support before loading any org files
;; :load-path "~/.emacs.d/src/org-mode/lisp/"
(delight 'org-indent-mode)
(setq org-directory (ct/file-join "~" "Pending" "org")
ct/org-gtd-directory (ct/file-join org-directory "gtd")
org-default-notes-file (ct/file-join ct/org-gtd-directory "inbox.org")
org-agenda-files (list ct/org-gtd-directory)
org-imenu-depth 4
org-startup-folded 'fold ; per file: #+STARTUP: overview
org-startup-indented t ; per file: #+STARTUP: indent
org-startup-with-inline-images t ; #+STARTUP: inlineimages
org-hide-leading-stars t ; per file: #+STARTUP: hidestars
org-cycle-separator-lines 0 ; 0=Hide all blank lines between collapsed items
org-fontify-whole-heading-line t ; Apply style to whole line for overlines/bg color
org-tags-sort-function 'org-string-collate-lessp ; Sort tags
;; t : Toggle visibility only in headlines
;; 'whitestart: Toggle outlines everywhere except at line beginnings (to indent there)
org-cycle-emulate-tab t
;; Instead of "..." to expand, show this:
org-ellipsis "•••")
;; Disable electric indentation of list items (inserting spaces at line start)
(defun ct/disable-electric-indent-local-mode () (electric-indent-local-mode -1))
(add-hook 'org-mode-hook #'ct/disable-electric-indent-local-mode)
(defun ct/turn-off-eldoc-mode () (eldoc-mode -1))
(add-hook 'org-mode-hook #'ct/turn-off-eldoc-mode)
;; I don't cycle through org files and want to free this shortcut.
(unbind-key "C-," org-mode-map)
(unbind-key "C-'" org-mode-map)
;; I also prefer shift-allowkey selection over priority shifts and TODO tag shifts
(setq org-support-shift-select t)
(unbind-key "S-<left>" org-mode-map)
(unbind-key "S-<right>" org-mode-map)
(unbind-key "S-<up>" org-mode-map)
(unbind-key "S-<down>" org-mode-map)
;; Free the prev/next tab keys on macOS
(unbind-key "M-{" org-mode-map)
(define-key org-mode-map (kbd "M-[") #'org-backward-element)
(unbind-key "M-}" org-mode-map)
(define-key org-mode-map (kbd "M-]") #'org-backward-element)
(global-set-key (kbd "<f12>") #'org-capture)
(global-set-key (kbd "C-c b") #'org-switchb)
C-u 4 org-refile
will not move the current item, but jump to the destination instead. Yiming Chen made a helper to do this programmatically. I want auto-completion to suggest this for “org-goto”
(defun org-goto-global/ct ()
"Jump to org heading in any org buffer using `org-refile'."
(interactive)
(org-refile '(4)))
(defun ct/org-foldup ()
"Hide the entire subtree from root headline at point."
(interactive)
(while (ignore-errors (outline-up-heading 1)))
; Hide/fold subtree
(org-flag-subtree t))
(defun ct/org-shifttab (&optional arg)
(interactive "P")
(if (or (null (org-current-level)) ; point is before 1st heading, or
(and (= 1 (org-current-level)) ; at level-1 heading, or
(org-at-heading-p))
(org-at-table-p)) ; in a table (to preserve cell movement)
; perform org-shifttab at root level elements and inside tables
(org-shifttab arg)
; try to fold up elsewhere
(ct/org-foldup)))
(org-defkey org-mode-map (kbd "S-<tab>") #'ct/org-shifttab)
(defun ct/org-emphasize-below-point (&optional char)
(interactive)
(unless (region-active-p)
(backward-word)
(mark-word))
(org-emphasize char))
(define-key org-mode-map [remap org-emphasize] #'ct/org-emphasize-below-point)
(setq org-hide-emphasis-markers t)
The auto-hiding makes editing emphasized text hard, though, because the point position doesn’t indicate if you’re inside or outside of the emphasis pairs. Make them appear when point is in emphasized text to fix this:
(use-package org-appear
:demand
:ensure
:hook (org-mode . org-appear-mode))
Stole the idea for negative boxes and small tags from org-modern: https://github.com/minad/org-modern
(with-eval-after-load 'org
(set-face-attribute
'org-tag
nil
:family 'variable-pitch
:width 'condensed :height 0.6 :weight 'medium
:foreground (face-attribute 'default :foreground nil t)
:background "#efefef"
:box
(let ((border (max 3 (cond
((integerp line-spacing)
line-spacing)
((floatp line-spacing)
(ceiling (* line-spacing (frame-char-height))))
(t (/ (frame-char-height) 10))))))
(list :color (face-attribute 'default :background nil t)
:line-width
;; Emacs 28 supports different line horizontal and vertical line widths
(if (eval-when-compile (>= emacs-major-version 28))
(cons 0 (- border))
(- border))))))
(org-babel-do-load-languages
'org-babel-load-languages
'((dot . t)
(emacs-lisp . t)
(ruby . t)
(markdown . t)
(python . t)
(shell . t)))
(setq org-src-tab-acts-natively t
org-src-fontify-natively t
org-edit-src-content-indentation 0
;; Threshold when to stop prepending lines with colons and make EXAMPLE block instead
org-babel-min-lines-for-block-output 5)
;; Update images when running babel blocks
(add-hook 'org-babel-after-execute-hook #'org-redisplay-inline-images)
ob-markdown
is not part of any package repository, so I just downloaded it locally from source:
(use-package emacs
:after (org markdown-mode)
:ensure markdown-mode
:init
(load-file "~/.emacs.d/src/ob-markdown.el"))
See the org-babel :result header parameter docs: I don’t want the default where colons are prepended. A HTML block makes most sense for Markdown output.
;; (setq org-babel-default-header-args:markdown '())
(add-to-list 'org-babel-default-header-args:markdown
'(:results . "output verbatim html"))
(use-package plantuml-mode
:defer :demand
:config
(setq org-plantuml-exec-mode 'plantuml) ;; Use the plantuml executable instead of the Java JAR
(add-to-list 'org-src-lang-modes '("plantuml" . plantuml))
(org-babel-do-load-languages 'org-babel-load-languages '((plantuml . t))))
The string format of the alist (("orange" . 111) ("purple" . 112) ("blue" . 98) ("yellow" . 121) ("green" . 103) ("red" . 114))
as a string via (org-tag-alist-to-string org-tag-alist)
is: ”orange(o) purple(p) blue(b) yellow(y) green(g) red(r)
”.
;; Tags with fast selection keys
(setq org-tag-alist nil)
(setq org-tag-persistent-alist '(;; Development
(:startgroup . nil)
("feature" . ?f)
("bug" . ?b)
(:endgroup . nil)))
;; Allow setting single tags without the menu
(setq org-fast-tag-selection-single-key 'expert)
;; For tag searches ignore tasks with scheduled and deadline dates
(setq org-agenda-tags-todo-honor-ignore-options t)
See: https://www.gnu.org/software/emacs/manual/html_node/org/Handling-links.html
(global-set-key (kbd "C-c l") 'org-store-link)
(defun org-thearchive-open (str)
(browse-url (concat "thearchive://match/" str)))
(org-link-set-parameters
"zettel"
:follow #'org-thearchive-open
;; Don't fold links but show ID and description:
;; [[zettel:202102101021][Title or description here]]”
;;:display 'full
)
(use-package ol-notmuch
:after (org notmuch)
:ensure notmuch)
(global-set-key (kbd "C-c c") 'org-capture)
(defvar org-created-property-name "CREATED"
"The name of the org-mode property that stores the creation date of the entry")
(defun org-set-created-property (&optional active NAME)
"Set a property on the entry giving the creation time.
By default the property is called CREATED. If given the `NAME'
argument will be used instead. If the property already exists, it
will not be modified."
(interactive)
(let* ((created (or NAME org-created-property-name))
(fmt (if active "<%s>" "[%s]"))
(now (format fmt (format-time-string "%Y-%m-%d %a %H:%M"))))
(unless (org-entry-get (point) created nil)
(org-set-property created now))))
(add-hook 'org-capture-before-finalize-hook #'org-set-created-property)
(setq ct/org-diary-file (ct/file-join org-directory "diary" "diary.org"))
(setq org-capture-templates
`(("t" "Task" entry (file org-default-notes-file)
"* TODO %?\n%a\n")
("w" "Waiting for" entry (file ,(ct/file-join ct/org-gtd-directory "tickler.org"))
"* WAITING %?\n%a\n")
("e" "Event" entry
(file+headline ,(ct/file-join ct/org-gtd-directory "privat.org") "Events")
"** %? \n %^T \n")
("j" "Journal" entry (file+datetree ct/org-diary-file)
"* %?\n\n")
("n" "Naikan Therapy" entry (file+datetree ct/org-diary-file)
"* Naikan :naikan:\n- What have I received from ...?\n- What have I given to ...?\n- What troubles and difficulties have I caused ...?\n- Affirmations:")
("w" "Work review" entry (file+datetree ct/org-diary-file)
(file "~/.emacs.d/capture-templates/work-review.org"))))
(with-eval-after-load 'org-capture
;; Source: https://emacs.stackexchange.com/a/5931/189b86
(defun ct/org-get-target-headline (&optional targets prompt)
"Prompt for a location in an org file and jump to it.
This is for promping for refile targets when doing captures.
Targets are selected from `org-refile-targets'. If TARGETS is
given it temporarily overrides `org-refile-targets'. PROMPT will
replace the default prompt message.
If CAPTURE-LOC is is given, capture to that location instead of
prompting."
(let ((org-refile-targets (or targets org-refile-targets))
(prompt (or prompt "Capture Location")))
(org-refile t nil nil prompt)))
(push '("f" "File email for apps") org-capture-templates)
(push `("fa" "File > The Archive"
entry (file+function ,(ct/file-join ct/org-gtd-directory "thearchive.org") 'ct/org-get-target-headline)
"* TODO %?\n- %:from %u\n"
:jump-to-captured t)
org-capture-templates)
(push `("ft" "File > TableFlip"
entry (file+function ,(ct/file-join ct/org-gtd-directory "tableflip.org") 'ct/org-get-target-headline)
"* TODO %?\n- %:from %u\n"
:jump-to-captured t)
org-capture-templates))
(via https://blog.aaronbieber.com/2017/03/19/organizing-notes-with-refile.html)
;; Refile into agenda aware files & into the current file
(setq org-refile-targets (quote ((nil :maxlevel . 9)
(org-agenda-files :maxlevel . 3))))
(setq org-refile-use-outline-path 'file) ;t) ;; 'file=Allow prepending file names when refiling
(setq org-outline-path-complete-in-steps nil) ;; nil=Generate all files and headings at once
(setq org-refile-allow-creating-parent-nodes 'confirm) ;; Allow subheading creation
(setq org-completion-use-ido nil) ;; I'm using ivy
from http://doc.norang.ca/org-mode.html#RefileSetup:
(defun bh/verify-refile-target ()
"Exclude todo keywords with a done state from refile targets"
(not (member (nth 2 (org-heading-components)) org-done-keywords)))
(setq org-refile-target-verify-function 'bh/verify-refile-target)
By default, checkboxes would inherit the variable-pitch-mode
’s value; but I find monospaced checkboxes look better:
(set-face-attribute 'org-checkbox nil :inherit 'fixed-pitch)
Level 1 is set to large; skip repeating level-1 when encountering level-8.
Post: https://christiantietze.de/posts/2022/11/org-mode-more-outline-levels/
(defface org-level-9 '((t :inherit org-level-2))
"Face used for level 9 headlines."
:group 'org-faces)
(defface org-level-10 '((t :inherit org-level-3))
"Face used for level 10 headlines."
:group 'org-faces)
(defface org-level-11 '((t :inherit org-level-4))
"Face used for level 11 headlines."
:group 'org-faces)
(defface org-level-12 '((t :inherit org-level-5))
"Face used for level 12 headlines."
:group 'org-faces)
(defface org-level-13 '((t :inherit org-level-6))
"Face used for level 13 headlines."
:group 'org-faces)
(defface org-level-14 '((t :inherit org-level-7))
"Face used for level 14 headlines."
:group 'org-faces)
(defface org-level-15 '((t :inherit org-level-8))
"Face used for level 15 headlines."
:group 'org-faces)
(setq org-level-faces (append org-level-faces (list 'org-level-9 'org-level-10 'org-level-11 'org-level-12 'org-level-13 'org-level-14 'org-level-15)))
(setq org-n-level-faces (length org-level-faces))
Docs: When defining multiple sequences, make it so that no element appears in multiple sequences. Otherwise, org won’t be able to calculate the number of done/not-done items in callbacks properly, for example.
(setq org-deadline-warning-days 30)
;; Attach files as copies with UUID
(setq org-id-method (quote uuidgen))
(setq org-todo-keywords
(quote ((sequence "TODO(t)" "NEXT(n)" "DOING(g)" "|" "DONE(d)")
(sequence "WAITING(w@/!)" "HOLD(h@/!)")
(sequence "REPORT(r)" "BUG(b)" "KNOWNCAUSE(k)" "|" "FIXED(f)")
(sequence "|" "CANCELLED(c@/!)")
(sequence "PROJECT(p)"))))
(setq org-todo-state-tags-triggers
(quote (("CANCELLED" ("cancelled" . t))
("WAITING" ("waiting" . t))
("BUG" ("waiting") ("bug" . t))
("HOLD" ("waiting") ("hold" . t))
("TODO" ("waiting") ("cancelled") ("hold"))
("NEXT" ("waiting") ("cancelled") ("hold"))
("DOING" ("waiting") ("cancelled") ("hold"))
("DONE" ("waiting") ("cancelled") ("hold")))))
(setq org-use-fast-todo-selection t) ;; C-c C-t KEY
Make M-S-RET
insert TODO item without remembering the current item’s state. That’s not useful in 99% of the cases.
(defun ct/org-insert-initial-todo-heading (&rest args)
"Insert a new heading with the same level and initial TODO state.
Behaves like `org-insert-todo-heading' when called with prefix
argument: it doesn't keep the current heading's TODO state.
Actual prefix arguments will be forwarded, but defaults to universal prefix:
With two prefix args, force inserting at end of parent subtree."
(interactive "P")
(let ((current-prefix-arg (or current-prefix-arg
'(4))))
(require 'org)
(call-interactively #'org-insert-todo-heading)))
(define-key org-mode-map [remap org-insert-todo-heading] #'ct/org-insert-initial-todo-heading)
(defun org-todo-if-needed (state)
"Change header state to STATE unless the current item is in STATE already."
(unless (string-equal (org-get-todo-state) state)
(org-todo state)))
(defun ct/org-summary-todo-cookie (n-done n-not-done)
"Switch header state to DONE when all subentries are DONE, to TODO when none are DONE, and to DOING otherwise"
(let (org-log-done org-log-states) ; turn off logging
(org-todo-if-needed (cond ((= n-done 0)
"TODO")
((= n-not-done 0)
"DONE")
(t
"DOING")))))
(add-hook 'org-after-todo-statistics-hook #'ct/org-summary-todo-cookie)
(defun ct/org-summary-checkbox-cookie ()
"Switch header state to DONE when all checkboxes are ticked, to TODO when none are ticked, and to DOING otherwise"
(let (beg end)
(unless (not (org-get-todo-state))
(save-excursion
(org-back-to-heading t)
(setq beg (point))
(end-of-line)
(setq end (point))
(goto-char beg)
;; Regex group 1: %-based cookie
;; Regex group 2 and 3: x/y cookie
(if (re-search-forward "\\[\\([0-9]*%\\)\\]\\|\\[\\([0-9]*\\)/\\([0-9]*\\)\\]"
end t)
(if (match-end 1)
;; [xx%] cookie support
(cond ((equal (match-string 1) "100%")
(org-todo-if-needed "DONE"))
((equal (match-string 1) "0%")
(org-todo-if-needed "TODO"))
(t
(org-todo-if-needed "DOING")))
;; [x/y] cookie support
(if (> (match-end 2) (match-beginning 2)) ; = if not empty
(cond ((equal (match-string 2) (match-string 3))
(org-todo-if-needed "DONE"))
((or (equal (string-trim (match-string 2)) "")
(equal (match-string 2) "0"))
(org-todo-if-needed "TODO"))
(t
(org-todo-if-needed "DOING")))
(org-todo-if-needed "DOING"))))))))
(add-hook 'org-checkbox-statistics-hook #'ct/org-summary-checkbox-cookie)
(global-set-key (kbd "C-c a") #'org-agenda)
;; Focus on 1 week by default
(setq org-agenda-span 'week)
(setq org-agenda-window-setup 'current-window)
(setq org-icalendar-combined-agenda-file "~/Pending/org/calendar-export.ics")
;; Limit org-agenda tags to show flush-right right at the 100th column via `-100`
;;and auto-enable olivetti-mode with that width.
(defvar ct/org-width 100)
(defvar ct/org-agenda-width ct/org-width)
(setq org-agenda-tags-column (- ct/org-agenda-width))
(setq org-tags-column 0) ;; Show tags right after heading; the spacing only causes layout problems
(setq org-agenda-remove-tags t)
;; Draw horizontal rule between agenda items (can also be set per agenda command in shared settings)
(setq org-agenda-compact-blocks nil)
;; Using a character value instead of a string with multiple chars would end up adding more characters than fit Olivetti's width.
(setq org-agenda-block-separator (make-string ct/org-agenda-width (string-to-char "━")))
;; Show inbox item counter at top of agenda
(defun ct/count-org-items (path)
"Return the number of outline items in a file at PATH."
(with-temp-buffer
(insert-file-contents path)
(let* ((start 0)
(counted 0))
(while (string-match "^* " (buffer-string) start)
(setq start (match-end 0))
(setq counted (+ 1 counted)))
counted)))
(defun ct/org-agenda-show-inbox-counter (&rest _ignore)
(let* ((file org-default-notes-file)
(counter (ct/count-org-items file))
(label (cond ((eq 1 counter) "Item") (t "Items"))))
(if (eq counter 0)
(insert (propertize "Inbox Empty!\n" 'face 'org-agenda-structure))
(insert (propertize (concat "["
;; link to inbox file
"[file:" file "]"
;; display inbox item count
"[" (number-to-string counter) " " label " in the Inbox]"
"]\n")
'face 'org-agenda-structure)))))
(defun ct/iconified-header (icon text &optional skip-leading-newline)
(concat (unless skip-leading-newline "\n")
(all-the-icons-faicon icon :v-adjust 0.01) " " text))
(defun ct/inbox-task-header ()
(concat (number-to-string (ct/count-org-items org-default-notes-file)) " Tasks in Inbox"))
(defun ct/org-agenda-set-wrap-prefix ()
(setq-local wrap-prefix " "))
(add-hook 'org-agenda-mode-hook #'ct/org-agenda-set-wrap-prefix)
(setq org-agenda-custom-commands (quote
;; `C-a d` is my main Agenda
(("d" "Daily Action List"
(
(tags "+focus"
((org-agenda-overriding-header (ct/iconified-header "compass" "Foci" t))
(org-tags-match-list-sublevels nil)
(org-agenda-todo-ignore-scheduled 'future)))
(agenda "" ((org-agenda-overriding-header " ") ;; Non-empty string
(org-agenda-span 'day)
(org-agenda-ndays 1)
(org-agenda-sorting-strategy (quote ((agenda time-up priority-down tag-up))))
;; Do not include scheduled, due or overdue items here
(org-deadline-warning-days 0)
(org-scheduled-past-days 0)
(org-deadline-past-days 0)
(org-agenda-skip-scheduled-if-done t)
(org-agenda-skip-timestamp-if-done t)
(org-agenda-skip-deadline-if-done t)))
(agenda "" ((org-agenda-overriding-header "\nNext three days\n")
(org-agenda-start-on-weekday nil)
(org-agenda-start-day "+1d")
(org-agenda-span 3)
(org-deadline-warning-days 0)
(org-agenda-block-separator nil)
(org-agenda-skip-function '(org-agenda-skip-entry-if 'todo 'done))))
(agenda "" ((org-agenda-overriding-header "\nOverdue")
(org-agenda-time-grid nil)
(org-agenda-start-on-weekday nil)
(org-agenda-show-all-dates nil)
(org-agenda-format-date "") ;; Skip the date
(org-agenda-span 1)
(org-agenda-skip-function '(org-agenda-skip-entry-if 'todo 'done))
(org-agenda-entry-types '(:deadline :scheduled))
(org-scheduled-past-days 999)
(org-deadline-past-days 999)
(org-deadline-warning-days 0)))
(agenda "" ((org-agenda-overriding-header "\nUpcoming deadlines (+14d)\n")
(org-agenda-time-grid nil)
(org-agenda-start-on-weekday nil)
;; We don't want to replicate the previous section's
;; three days, so we start counting from the day after.
(org-agenda-start-day "+3d")
(org-agenda-span 14)
(org-agenda-show-all-dates nil)
(org-agenda-time-grid nil)
(org-deadline-warning-days 0)
(org-agenda-block-separator nil)
(org-agenda-entry-types '(:deadline))
(org-agenda-skip-function '(org-agenda-skip-entry-if 'todo 'done))))
(tags "+inbox" ;; or limit to (org-agenda-files '(".../inbox.org"))
((org-agenda-overriding-header (ct/iconified-header "inbox" (ct/inbox-task-header)))
(org-tags-match-list-sublevels nil)
; TODO: customize (org-agenda-sorting-strategy ...) for reversed order?
(wrap-prefix " ")
(org-agenda-max-entries 5)))
(tags-todo "TODO=\"NEXT\"-focus"
((org-agenda-overriding-header (ct/iconified-header "tasks" "Pull: Next Actions"))
(wrap-prefix " ")
(org-tags-match-list-sublevels nil)
(org-agenda-todo-ignore-scheduled 'future)))
(tags-todo "-TODO=\"CANCELLED\"+waiting|+TODO=\"WAITING\"|HOLD/!"
((org-agenda-overriding-header (ct/iconified-header "hourglass-end" "Waiting & Postponed"))
(wrap-prefix " ")
(org-tags-match-list-sublevels nil)
(org-agenda-todo-ignore-scheduled 'future)))
))
("D" "Test Agenda"
((agenda "" nil)
(tags "+inbox" ((org-agenda-overriding-header "Tasks in Inbox")
(org-tags-match-list-sublevels nil)
(org-agenda-max-entries 5)
))
(tags-todo "-CANCELLED+WAITING|HOLD/!"
((org-agenda-overriding-header "Waiting and Postponed Tasks")
(org-tags-match-list-sublevels nil)
(org-agenda-todo-ignore-scheduled 'future)
))
))
)))
(defun ct/display-buffer-org-agenda-managed-p (buffer-name action)
"Determine whether BUFFER-NAME is an org-agenda managed buffer."
(with-current-buffer buffer-name
(and (derived-mode-p 'org-mode)
(member (buffer-file-name) (org-agenda-files)))))
(add-to-list 'display-buffer-alist
`("\\*Org Agenda\\*"
(display-buffer-in-tab display-buffer-reuse-mode-window)
(ignore-current-tab . t)
(tab-name . "Org Files")
(window-width . ,ct/org-agenda-width)
(dedicated . side) ;; Make the Agenda a dedicated side-window
(side . left) ;; to the left so it always stays open.
(inhibit-same-window . nil)))
(add-to-list 'display-buffer-alist
'(ct/display-buffer-org-agenda-managed-p
(display-buffer-reuse-mode-window ;; Prioritize reuse of current window
display-buffer-in-tab) ;; over switching to the Org tab.
;; (ignore-current-tab . t)
(tab-name . "Org Files")))
(defun ct/org-agenda-list-exclude-sascha-advice (orig-fn &rest args)
"Exclude selected tags from `org-agenda-list'.
Intended as :around advice for `org-agenda-list'."
(let ((org-agenda-tag-filter-preset '("-sascha")))
(funcall orig-fn args)))
(advice-add #'org-agenda-list :around #'ct/org-agenda-list-exclude-sascha-advice)
(advice-add #'org-todo-list :around #'ct/org-agenda-list-exclude-sascha-advice)
From https://lists.gnu.org/archive/html/emacs-orgmode/2013-02/msg00644.html and https://gist.github.com/justinhj/eb2d354d06631076566f#file-gistfile1-el
(setq
appt-time-msg-list nil ;; clear existing appt list upon initial launch
appt-message-warning-time '15 ;; send first warning 15 minutes before appointment
appt-display-interval '15
appt-display-mode-line nil ;; don't show in the modeline
appt-display-format 'window ;; pass warnings to the designated window function
appt-disp-window-function (function ct/appt-display-native))
;; activate appointment notification
(appt-activate 1)
;; Wire org-agenda and appointments together
(defun ct/send-notification (title msg)
(let ((notifier-path (executable-find "alerter")))
(start-process
"Appointment Alert"
"*Appointment Alert*" ;; nil to not capture output
notifier-path
"-message" msg
"-title" title
"-sender" "org.gnu.Emacs"
"-activate" "org.gnu.Emacs")))
(defun ct/appt-display-native (min-to-app new-time msg)
(ct/send-notification
(format "Appointment in %s minutes" min-to-app) ;; passed to -title in terminal-notifier call
(format "%s" msg))) ;; passed to -message in terminal-notifier call
;; Agenda-to-appointent hooks
(org-agenda-to-appt)
;; update appt list on agenda view and every hour
(run-at-time "24:01" 3600 'org-agenda-to-appt)
(add-hook 'org-finalize-agenda-hook #'org-agenda-to-appt)
(use-package org-autolist
:load-path "/Users/ctm/.emacs.d/src/org-autolist"
:ensure t
:delight
:config
(setq org-autolist-enable-delete nil)
(add-hook 'org-mode-hook (lambda () (org-autolist-mode))))
https://git.sr.ht/~bzg/org-contrib
The transition is intended to move each module into its own package, and to not rely on the ‘contrib’ folder/repo at all.
(use-package org-trello
:load-path "~/.dotfiles/emacs.d/src/org-trello"
:after org
:config
(custom-set-variables '(org-trello-files
(seq-filter (lambda (f) (string-suffix-p "_trello.org" f))
(org-files-list)))))
(use-package org-menu
:after org
:load-path "~/.dotfiles/emacs.d/src/org-menu"
:config
(define-key org-mode-map (kbd "C-c m") 'org-menu))
(use-package org-view-mode
:ensure)
(define-advice org-flag-region (:around (oldfun from to flag &optional spec) unfold-page-breaks)
"Unfold all form feed characters lines inside folded region."
(funcall oldfun from to flag spec)
(when (and flag (not (eq 'visible spec)))
(org-with-point-at from
(while (re-search-forward "\n\u000c" to t)
(org-flag-region (match-beginning 0) (match-end 0) t 'visible)))))
(setq default-major-mode 'text-mode)
We’re not on old IBM machines anymore, and the 80 character rule breaks down for me with org-mode
when I indent 3 levels. I find 100 columns to work ok. That still gives me some margin in olivetti-mode
, which is on by default when writing.
(use-package emacs
:config
(setq fill-column 100))
- At first, good-scroll-mode looked nice because of the momentum, but it turns out that scrolling large folded org documents or mail histories doesn’t work at all: as if it cannot pass a threshold.
- Built-in
pixel-scroll-mode
looked nice, too, until it broke down time and again in large org documents and emails with attachments. Scrolling down past an image worked ok, but not back up. In my org agenda, it wouls often hang when scrolling using the external mouse. iscroll-mode
, too, didn’t work properly with large images.
The best solution I dound is the Emacs mac fork: https://bitbucket.org/mituharu/emacs-mac/
;; Scrolling is moving the document, not my cursor
(setq scroll-preserve-screen-position nil)
mlscroll
draws a horizontal scroll bar in the modeline that replaces the % and top/bottom indicators, and also shows how much of the document you’re actually seeing right now.
(use-package mlscroll
:hook
(after-init . mlscroll-mode))
mlscroll-in-color
references the region color, but modus-themes v4 uses e.g. a faint yellow selection color, which is not so sexy.
(with-eval-after-load "modus-themes"
(with-eval-after-load "mlscroll"
(defun ct/modus-themes-mlscroll-colors ()
(modus-themes-with-colors
(customize-set-variable 'mlscroll-in-color blue-faint)
(customize-set-variable 'mlscroll-out-color bg-main)))
(add-hook 'modus-themes-after-load-theme-hook #'ct/modus-themes-mlscroll-colors)))
Enable hs-minor-mode
in e.g. source files to fold regions.
Default keybindings:
C-c @ C-M-s show all C-c @ C-M-h hide all C-c @ C-s show block C-c @ C-h hide block C-c @ C-c toggle hide/show
From the extensions at EmacsWiki/HideShow, this does not hide everything up to the root level (e.g. classes in code) but to leaf nodes (e.g. methods in classes):
C-c @ C-M-s
show allC-c @ C-M-h
hide allC-c @ C-s
show blockC-c @ C-h
hide blockC-c @ C-c
toggle hide/show
Cyclical folding by Karthik:
(use-package hideshow
:config
(defun hs-hide-leafs-recursive (minp maxp)
"Hide blocks below point that do not contain further blocks in region (MINP MAXP)."
(when (hs-find-block-beginning)
(setq minp (1+ (point)))
(funcall hs-forward-sexp-func 1)
(setq maxp (1- (point))))
(unless hs-allow-nesting
(hs-discard-overlays minp maxp))
(goto-char minp)
(let ((leaf t))
(while (progn
(forward-comment (buffer-size))
(and (< (point) maxp)
(re-search-forward hs-block-start-regexp maxp t)))
(setq pos (match-beginning hs-block-start-mdata-select))
(if (hs-hide-leafs-recursive minp maxp)
(save-excursion
(goto-char pos)
(hs-hide-block-at-point t)))
(setq leaf nil))
(goto-char maxp)
leaf))
(defun hs-hide-leafs ()
"Hide all blocks in the buffer that do not contain subordinate blocks. The hook `hs-hide-hook' is run; see `run-hooks'."
(interactive)
(hs-life-goes-on
(save-excursion
(message "Hiding blocks ...")
(save-excursion
(goto-char (point-min))
(hs-hide-leafs-recursive (point-min) (point-max)))
(message "Hiding blocks ... done"))
(run-hooks 'hs-hide-hook)))
(defun hs-cycle (&optional level)
(interactive "p")
(let (message-log-max
(inhibit-message t))
(if (= level 1)
(pcase last-command
('hs-cycle
(hs-hide-level 1)
(setq this-command 'hs-cycle-children))
('hs-cycle-children
;; TODO: Fix this case. `hs-show-block' needs to be
;; called twice to open all folds of the parent
;; block.
(save-excursion (hs-show-block))
(hs-show-block)
(setq this-command 'hs-cycle-subtree))
('hs-cycle-subtree
(hs-hide-block))
(_
(if (not (hs-already-hidden-p))
(hs-hide-block)
(hs-hide-level 1)
(setq this-command 'hs-cycle-children))))
(hs-hide-level level)
(setq this-command 'hs-hide-level))))
(defun hs-global-cycle ()
(interactive)
(pcase last-command
('hs-global-cycle
(save-excursion (hs-show-all))
(setq this-command 'hs-global-show))
(_ (hs-hide-all))))
(define-key hs-minor-mode-map (kbd "C-<tab>") #'hs-cycle)
(add-hook 'prog-mode-hook #'hs-minor-mode))
(global-set-key (kbd "C-<tab>") 'hs-toggle-hiding)
(global-set-key (kbd "C-M-<tab>") 'hs-hide-leafs)
The classic linum-mode
is written in Lisp; but display-line-numbers-mode
is available since Emacs 26 and written in C. It’s supposedly a ton faster and doesn’t stutter in large files, at all.
Relative line numbers used to be provided by linum-relative-mode
, but display-line-numbers-mode
provides the same functionality:
(setq-default display-line-numbers nil ;; Disable by default
display-line-numbers-type 'visual) ;; 'relative + counting wrapped lines
When we show line numbers at all, show 4 characters worth of them to prevent re-layouting once we hit 100. That horizontal movement is quite annoying.
(setq-default display-line-numbers-width 4)
Bind the toggle to something convenient, overriding global line number mode:
(with-eval-after-load "xah-fly-keys"
;; Was: #'global-display-line-numbers-mode
(define-key xah-fly-leader-key-map (kbd "l 4") #'display-line-numbers-mode))
Scaling text via C--
and C-+
doesn’t affect/update the line numbers by default:
(defun ct/post-text-scale-callback ()
(let ((new-size (floor (* (face-attribute 'default :height)
(expt text-scale-mode-step text-scale-mode-amount)))))
(set-face-attribute 'line-number nil :height new-size)
(set-face-attribute 'line-number-current-line nil :height new-size)))
(add-hook 'text-scale-mode-hook 'ct/post-text-scale-callback)
See Emacs indentation tutorial for details.
Indent by 4 spaces instead of adhering to tab stops:
(setq-default indent-tabs-mode nil
tab-width 4)
Consider not being smart about TAB-to-indent and always insert 4 spaces by adding this: (setq indent-line-function (quote insert-tab))
Used by some programming mode hooks to share the 2-spaces setting.
(defun ct/two-space-indentation ()
(setq indent-tabs-mode nil)
(setq tab-width 2))
With repeated C-=
presses, expand from point to word, sexp, quote, parens, paragraph, …
https://github.com/magnars/expand-region.el
(use-package expand-region
:demand
:bind
;; `C-- C-=` (negative prefix) shrinks region
("C-=" . er/expand-region))
In xah-fly-keys’s command mode, 8
expands to brackets and quotes, but the rest is a bit unforseeable. Move it to shift-8
and bind 8
to use expand-region
instead:
(with-eval-after-load "xah-fly-keys"
(with-eval-after-load "expand-region"
(define-key xah-fly-command-map "8" #'er/expand-region)
(define-key xah-fly-command-map "*" #'xah-extend-selection)))
(use-package emacs
:delight visual-line-mode
:config
(defun turn-off-visual-line-mode ()
"Disable `visual-line-mode' in current buffer."
(visual-line-mode -1))
:hook (after-init . global-visual-line-mode))
(use-package emacs
:delight
(auto-fill-function " AF")
(global-subword-mode)
(subword-mode))
Discovered through Protesilaos and Xah Lee, which-key
can be used to discover available key chords. So if you don’t know the projectile commands, hit C-c p
and wait, and then a little window will tell you the available keys to press.
It’s basically printing a key map.
(use-package which-key
:delight
:config
(setq which-key-dont-use-unicode t)
(setq which-key-add-column-padding 2)
(setq which-key-show-early-on-C-h nil)
(setq which-key-idle-delay 1.0) ;most-positive-fixnum) ; set this to something like 0.8
(setq which-key-idle-secondary-delay 0.05)
(setq which-key-popup-type 'side-window)
(setq which-key-show-prefix 'echo)
(setq which-key-max-display-columns 3)
(setq which-key-separator " ")
(setq which-key-special-keys nil)
(setq which-key-paging-key "<next>")
(which-key-mode 1))
(use-package discover
:ensure t)
Use M-x command-log-mode
to start/stop the logging, and M-x clm/open-command-log-buffer
to show the log in a side window. Useful for screencasting. Xah Lee seems to use that.
The original repo is dormant since 2016 or so. Am using https://github.com/pierre-rouleau/command-log-mode instead.
(use-package command-log-mode
:load-path "~/.emacs.d/src/command-log-mode"
:ensure t
:config
(setq command-log-mode-window-text-scale -2)
;; Workaround for the bug that you have to show the buffer before enabling the mode.
(setq command-log-mode-auto-show t)
(defun ct/command-log-mode ()
"Toggle command-log-mode and force showing/hiding the buffer, no matter what command-log-mode-auto-show says"
(interactive)
(if command-log-mode
(progn
(clm/close-command-log-buffer)
(command-log-mode -1))
(progn (clm/open-command-log-buffer)
(command-log-mode +1))))
:init
(command-log-mode -1)
(global-command-log-mode -1))
https://github.com/takaxp/org-tree-slide
Used by Protesilaos Stavrou. David Wilson of SystemCrafters uses org-present.
(use-package org-tree-slide
:ensure t
:config
(setq org-tree-slide-breadcrumbs t)
(setq org-tree-slide-header nil)
(setq org-tree-slide-slide-in-effect nil)
(setq org-tree-slide-heading-emphasis nil)
(setq org-tree-slide-never-touch-face t)
(setq org-tree-slide-cursor-init nil) ;;t
(setq org-tree-slide-modeline-display nil)
(setq org-tree-slide-skip-done nil)
(setq org-tree-slide-skip-comments t)
(setq org-tree-slide-fold-subtrees-skipped t)
;; Skipping all subtrees makes it possible to advance to level-1 slides and then unwrap optional content
(setq org-tree-slide-skip-outline-level 2);8
(setq org-tree-slide-activate-message
(format "Presentation %s" (propertize "ON" 'face 'success)))
(setq org-tree-slide-deactivate-message
(format "Presentation %s" (propertize "OFF" 'face 'error)))
(let ((map org-tree-slide-mode-map))
(define-key map (kbd "<C-down>") #'org-tree-slide-display-header-toggle)
(define-key map (kbd "<C-right>") #'org-tree-slide-move-next-tree)
(define-key map (kbd "<C-left>") #'org-tree-slide-move-previous-tree)))
In org files, this would narrow to each heading level, omitting the *
at the beginning of the line.
Enable logos-focus-mode
to show the narrowed region in a special presentation style.
(use-package "logos"
:config
;; If you want to use outlines instead of page breaks (the ^L)
(setq logos-outlines-are-pages t)
(setq logos-outline-regexp-alist
`((emacs-lisp-mode . "^;;;+ ")
(org-mode . "^\\*+ +")
(markdown-mode . "^[\\#]+ +")
(t . ,(or outline-regexp logos--page-delimiter))))
;; These apply when `logos-focus-mode' is enabled. Their value is
;; buffer-local.
(setq-default logos-hide-mode-line t
logos-scroll-lock nil
logos-variable-pitch t
logos-hide-buffer-boundaries t
logos-hide-fringe t
logos-buffer-read-only nil
;; Olivetti might be on already, but better safe than sorry:
logos-olivetti t)
(let ((map global-map))
(define-key map [remap narrow-to-region] #'logos-narrow-dwim)
(define-key map [remap forward-page] #'logos-forward-page-dwim)
(define-key map [remap backward-page] #'logos-backward-page-dwim)))
Like the “BLOKK” font, but in Emacs, with color support https://github.com/bkaestner/redacted.el
(use-package redacted
:defer
:ensure
:config
;; Disable editing of text we can't read
:hook
(redacted-mode . (lambda () (read-only-mode (if redacted-mode 1 -1)))))
I actually like the block cursor type, but the I-beam or “bar” works better with modal inputs like evil-mode
or xah-fly-keys
.
(use-package emacs
:init
(setq-default cursor-type '(bar . 2)))
Abin kindly pointed me towards goggles.el
which does the same and then some:
(use-package "pulsar"
:config
;; Do not use setq but cutomize-set-variable to call update hooks:
(customize-set-variable
'pulsar-pulse-functions
'(recenter-top-bottom
move-to-window-line-top-bottom
reposition-window
bookmark-jump
other-window
delete-window
delete-other-windows
forward-page
backward-page
scroll-up-command
scroll-down-command
windmove-right
windmove-left
windmove-up
windmove-down
windmove-swap-states-right
windmove-swap-states-left
windmove-swap-states-up
windmove-swap-states-down
tab-new
tab-close
tab-next
org-next-visible-heading
org-previous-visible-heading
org-forward-heading-same-level
org-backward-heading-same-level
outline-backward-same-level
outline-forward-same-level
outline-next-visible-heading
outline-previous-visible-heading
outline-up-heading))
(setq pulsar-delay 0.04)
(add-hook 'imenu-after-jump-hook #'pulsar-recenter-top)
(add-hook 'imenu-after-jump-hook #'pulsar-reveal-entry))
(with-eval-after-load "pulsar"
(with-eval-after-load "consult"
(add-hook 'consult-after-jump-hook #'pulsar-recenter-top)
(add-hook 'consult-after-jump-hook #'pulsar-reveal-entry)))
(use-package define-word
:ensure t
:config
;; (setq define-word-default-service 'webster)
(global-set-key (kbd "C-c d") 'define-word-at-point)
(global-set-key (kbd "C-c D") 'define-word))
C-u SPC
can put you back to a previous insertion point, but this puts you back to the point of a previous change.
;; Unset the "minimize window" keys that called (suspend-frame).
(global-unset-key (kbd "C-z"))
(use-package goto-last-change
:ensure
:bind ("C-z" . goto-last-change))
(setq sentence-end-double-space nil)
(defun ct/kill-to-beginning-of-line-dwim ()
"Kill from point to beginning of line.
In `prog-mode', delete up to beginning of actual, not visual
line, stopping at whitespace. Repeat to delete whitespace. In
other modes, e.g. when editing prose, delete to beginning of
visual line only."
(interactive)
(let ((current-point (point)))
(if (not (derived-mode-p 'prog-mode))
;; In prose editing, kill to beginning of (visual) line.
(if visual-line-mode
(kill-visual-line 0)
(kill-line 0))
;; When there's whitespace at the beginning of the line, go to
;; before the first non-whitespace char.
(beginning-of-line)
(when (search-forward-regexp (rx (not space)) (point-at-eol) t)
;; `search-forward' puts point after the find, i.e. first
;; non-whitespace char. Step back to capture it, too.
(backward-char))
(kill-region (point) current-point))))
(global-set-key (kbd "s-<backspace>") #'ct/kill-to-beginning-of-line-dwim)
(defun ct/kill-word-dwim (arg)
"Kill characters forward until end of a word, or the current region."
(interactive "p")
(if (use-region-p)
(delete-active-region 'kill)
(kill-word arg)))
(global-set-key (kbd "M-d") #'ct/kill-word-dwim)
(global-set-key (kbd "M-<delete>") #'ct/kill-word-dwim)
The docs for C-d
which is bound to delete-char
by default say that for interactive use, one should favor delete-forward-char
, because it respects the region. I wonder: why isn’t this the default then?
(global-set-key (kbd "C-d") #'delete-forward-char)
(delete-selection-mode +1)
(setq fill-column 80)
(use-package unfill
:ensure t
:bind (("M-Q" . 'unfill-paragraph)
;; Since ⌘⇧Q is bound to "log out user" on macOS, and rebinding this shortcut is flaky, use a toggle instead to get both
("M-q" . 'unfill-toggle)))
From EmacsWiki:
(defun move-text-internal (arg)
(cond
((and mark-active transient-mark-mode)
(if (> (point) (mark))
(exchange-point-and-mark))
(let ((column (current-column))
(text (delete-and-extract-region (point) (mark))))
(forward-line arg)
(move-to-column column t)
(set-mark (point))
(insert text)
(exchange-point-and-mark)
(setq deactivate-mark nil)))
(t
(let ((column (current-column)))
(beginning-of-line)
(when (or (> arg 0) (not (bobp)))
(forward-line)
(when (or (< arg 0) (not (eobp)))
(transpose-lines arg))
(forward-line -1))
(move-to-column column t)))))
(defun move-line-region-down (arg)
"Move region (transient-mark-mode active) or current line
arg lines down."
(interactive "*p")
(move-text-internal arg))
(defun move-line-region-up (arg)
"Move region (transient-mark-mode active) or current line
arg lines up."
(interactive "*p")
(move-text-internal (- arg)))
(global-set-key (kbd "C-s-<down>") 'move-line-region-down)
(global-set-key (kbd "C-s-<up>") 'move-line-region-up)
M-n
and M-p
are unbound by default. Since C-n
and C-p
operate on a line level, I think these should operate on a paragraph level.
Also applies to code blocks.
(use-package emacs
:config
;; M-n in message-mode does something with "abbrev"
(unbind-key "M-n" message-mode-map)
;; M-n/M-p moves from link to link in markdown-mode
(unbind-key "M-p" markdown-mode-map)
(unbind-key "M-n" markdown-mode-map)
:bind
("M-p" . #'backward-paragraph)
("M-n" . #'forward-paragraph))
(setq require-final-newline t)
CUA (Common User Actions) has a couple of convenience functions pertaining rectangular insert.
C-SPC
to select textC-x SPC
to transform into rectangle (enablesrectangle-mark-mode
)
Then:
C-t
to insert string in rectangle on each line (default)C-return
to activate CUA mode (my binding) whereRET
moves from corner to corner
(use-package emacs
:init
(require 'rect)
:bind
(:map rectangle-mark-mode-map ("<C-return>" . cua-rectangle-mark-mode)))
grep
is built inag
aka thesilversearcher doesn’t have a great Elisp mode (I always get weird highlighting issues);rg
usingdeadgrep
works well as is well-maintained.
Projectile comes with search support for rg/ripgrep and ag/silversearcher already.
(use-package deadgrep
:load-path "/Users/ctm/.emacs.d/src/deadgrep"
:ensure
:bind
(:map deadgrep-mode-map
("w" . deadgrep-edit-mode)))
Open results in other window of my choosing, so I can use deadgrep result buffers like a sidebar and show the result in my main work window:
(with-eval-after-load "deadgrep"
(with-eval-after-load "ace-window"
(defun ct/find-file-ace-window (filename)
(require 'ace-window)
(let ((dir default-directory))
(aw-switch-to-window (aw-select nil))
(let ((default-directory dir))
(apply #'find-file (list filename)))))
(defun ct/deadgrep-visit-result-ace-window ()
(interactive)
(deadgrep--visit-result #'ct/find-file-ace-window))
;; "o" opens in other buffer, but I want to pick which with ace
(define-key deadgrep-mode-map (kbd "o") #'ct/deadgrep-visit-result-ace-window)))
Similar to wdired
, but for search results, wgrep
binds C-c C-p
to toggle editing the search results in-place. That’s like find-and-replace in modern text editors, only you can (or: have to) edit each occurrence on its own, or use a search result buffer replace command.
To make the interface more like wdired
, I bind w
to start the editing mode.
(use-package wgrep
:demand t
:bind
(:map grep-mode-map
("w" . wgrep-change-to-wgrep-mode)))
;; By default bound to `e' but keep things aligned in wdired and editing grep/ag/rg/... results
(define-key occur-mode-map (kbd "w") #'occur-edit-mode)
(use-package wgrep-deadgrep
:after (wgrep deadgrep)
:demand t
:bind (:map deadgrep-mode-map ("w" . wgrep-change-to-wgrep-mode)))
After executing this, you actually then have to call xah-fly-keys
to enable it. I bound the toggle to F6 to turn it on quickly after launching emacs, and having a convenient way to turn it off again.
Switch between modes using f
and home
; on my Moonlander, the Home key is the left caps lock hit once, briefly. If these keys are not available, SPC SPC
and ESC ESC
do the same. Why not single ESC
? Because then I cannot enter Meta-key commands on the Terminal.
SPC C-h
produces a list of leader key bindings via Xah’s website:
http://ergoemacs.org/misc/xah_fly_keys_leader_keys_2021-05-17.txt
;; Do not remove default bindings
(setq xah-fly-use-control-key nil)
(setq xah-fly-use-meta-key nil)
(use-package xah-fly-keys
:load-path "~/.emacs.d/src/xah-fly-keys"
:delight
(xah-fly-keys) ;; Without param, doesn't hide lighter b/c it looks for -mode?
:config
(xah-fly-keys-set-layout "qwerty")
;; SPC SPC gets you into insert mode, M-SPC M-SPC takes me back to command mode.
;; On the Moonlander, ESC ESC is even better.
(define-key xah-fly-insert-map (kbd "M-SPC M-SPC") #'xah-fly-command-mode-activate)
(define-key xah-fly-insert-map (kbd "ESC ESC") #'xah-fly-command-mode-activate)
;; Bound to 'SPC o u' by default for whatever reason
(define-key xah-fly-leader-key-map (kbd "o k") #'kill-rectangle)
(define-key xah-fly-leader-key-map (kbd "1") #'winner-undo)
(define-key xah-fly-leader-key-map (kbd "2") #'winner-redo)
;; `t' is for selecting; `T' is for going back (like C-SPC and C-u C-SPC)
(define-key xah-fly-command-map (kbd "T") #'pop-to-mark-command)
;; tab-bar-mode's key map
(define-key xah-fly-leader-key-map (kbd "t") tab-prefix-map)
;; Use consult buffer switching instead of stock:
(define-key xah-fly-leader-key-map (kbd "f") #'consult-buffer)
;; Switch SPC g and g: By default, `g' deletes a block, but I never know what that is
(define-key xah-fly-command-map (kbd "g") #'kill-line)
(define-key xah-fly-leader-key-map (kbd "g") #'xah-delete-current-text-block)
;; C-x r b → SPC i i
(defun ct/xah-open-file-fast ()
"Fix w/o ido"
(interactive)
(require 'bookmark)
(bookmark-maybe-load-default-file)
(let (($thisBookmark (completing-read "Open bookmark: " (mapcar (lambda ($x) (car $x)) bookmark-alist))))
(find-file (bookmark-get-filename $thisBookmark))))
(define-key xah-fly-leader-key-map (kbd "i i") #'ct/xah-open-file-fast) ;; #'bookmark-jump) ; but without setting cursor position
(define-key xah-fly-leader-key-map (kbd "i b") #'bookmark-bmenu-list)
(define-key xah-fly-leader-key-map (kbd ";") #'recenter-top-bottom) ;; was save file
(define-key xah-fly-leader-key-map (kbd "p") project-prefix-map) ;; was 'recenter-top-bottom
(defun ct/mark-paragraph ()
"Mark the paragraph or structural element, depending on the mode."
(interactive)
(cond
((derived-mode-p 'org-mode)
(org-mark-element))
(t
(mark-paragraph))))
(define-key xah-fly-command-map (kbd "9") 'ct/mark-paragraph) ;; was 'xah-select-text-in-quote
(define-key xah-fly-command-map (kbd "'") 'xah-select-text-in-quote) ;; was 'xah-cycle-hyphen-lowline-space
;; M-o → SPC O
(define-key xah-fly-command-map (kbd "O") #'consult-outline)
(define-key xah-fly-leader-key-map (kbd "o o") #'consult-outline)
(define-key xah-fly-leader-key-map (kbd "o y") #'yank-rectangle) ;; was on `SPC o o' before
;; Page navigation, use after `narrow-to-page' of `narrow-to-region' (SPC l l)
(define-key xah-fly-leader-key-map (kbd "l [") #'backward-page)
(define-key xah-fly-leader-key-map (kbd "l ]") #'forward-page)
;; Bind these AGAIN because Xah unbinds all
(global-set-key (kbd "s-<delete>") #'ct/kill-word)
(global-set-key (kbd "s-DEL") #'backward-kill-word)
;; Was: xah-forward-right-bracket
(define-key xah-fly-command-map (kbd ".") #'xah-goto-matching-bracket)
(defun ct/xah-show-kill-ring ()
"Insert all `kill-ring' content in a new buffer named *copy history*.
URL `http://ergoemacs.org/emacs/emacs_show_kill_ring.html'"
(interactive)
(let (($buf (generate-new-buffer "*copy history*"))
(inhibit-read-only t))
(progn
(require 'shortdoc)
(switch-to-buffer $buf)
(shortdoc-mode)
(hl-line-mode) ;; pronounced highlight of current position, esp. for `n`/`p` movement
(dolist ($killed-line kill-ring)
(insert (propertize $killed-line
'shortdoc-section t
'shortdoc-function t)
"\n")
(insert (propertize "\n" 'face 'shortdoc-separator)))
(goto-char (point-min)))))
(global-set-key [remap xah-show-kill-ring] #'ct/xah-show-kill-ring)
(defun ct/xah-fly-keys-toggled ()
"While XFK is on, use a block mode cursor for command mode"
(if xah-fly-keys
(setq-default cursor-type t) ;; t = use frame value, the value XFK is setting
(setq-default cursor-type '(bar . 2))))
(add-hook 'xah-fly-keys-hook #'ct/xah-fly-keys-toggled)
(defvar ct/xfk-auto-insert-mode-fns '()
"List of functions to automatically call xah-fly-insert-mode-activate on.")
(setq ct/xfk-auto-insert-mode-fns
'(org-meta-return
org-insert-todo-heading-respect-content
org-insert-heading-respect-content
org-insert-link
ct/markdown-insert-comment
;; More function names here
))
(defun ct/xfk-auto-insert-mode-activate ()
"Wires xah-fly-insert-mode-activate to all functions from ct/xfk-auto-insert-mode-fns."
(dolist (element ct/xfk-auto-insert-mode-fns)
(advice-add element :after #'xah-fly-insert-mode-activate)))
(ct/xfk-auto-insert-mode-activate)
(defun ct/turn-on-shift-select-mode (&rest r)
"Enables `shift-select-mode'."
(interactive)
(setq shift-select-mode t))
(add-hook 'xah-fly-keys-hook #'ct/turn-on-shift-select-mode)
(add-hook 'after-init-hook #'xah-fly-keys))
(with-eval-after-load "xah-fly-keys"
(with-eval-after-load "modus-themes"
(defun ct/set-xah-fly-mode-indicators ()
"Sets XFK indicators based on current (!) modus theme."
(modus-themes-with-colors
(setq xah-fly-command-mode-indicator
(propertize " " 'face `(:foreground ,fg-dim)))
(setq xah-fly-insert-mode-indicator
(propertize " " 'face `(:foreground ,blue-intense)))))
(add-hook 'modus-themes-after-load-theme-hook #'ct/set-xah-fly-mode-indicators)))
;; Imported default Control re-bindings from XFK 2021-10-16
(with-eval-after-load "xah-fly-keys"
;; (global-set-key (kbd "<C-S-prior>") 'xah-previous-emacs-buffer)
;; (global-set-key (kbd "<C-S-next>") 'xah-next-emacs-buffer)
;; (global-set-key (kbd "<C-tab>") 'xah-next-user-buffer)
;; (global-set-key (kbd "<C-S-tab>") 'xah-previous-user-buffer)
;; (global-set-key (kbd "<C-S-iso-lefttab>") 'xah-previous-user-buffer)
;; (global-set-key (kbd "<C-prior>") 'xah-previous-user-buffer)
;; (global-set-key (kbd "<C-next>") 'xah-next-user-buffer)
(define-key global-map (kbd "C-<tab>") #'tab-next)
(define-key global-map (kbd "C-S-<tab>") #'tab-previous)
(global-set-key (kbd "<f7>") 'xah-fly-leader-key-map)
(global-set-key (kbd "C-1") 'nil)
(global-set-key (kbd "C-2") 'pop-global-mark)
(global-set-key (kbd "C-3") 'previous-error)
(global-set-key (kbd "C-4") 'next-error)
;; (global-set-key (kbd "C-5") 'xah-previous-emacs-buffer)
;; (global-set-key (kbd "C-6") 'xah-next-emacs-buffer)
(global-set-key (kbd "C-7") 'xah-previous-user-buffer)
(global-set-key (kbd "C-8") 'xah-next-user-buffer)
;; (global-set-key (kbd "C-9") 'scroll-down-command)
;; (global-set-key (kbd "C-0") 'scroll-up-command)
(global-set-key (kbd "C--") 'text-scale-decrease)
(global-set-key (kbd "C-=") 'text-scale-increase)
;; (global-set-key (kbd "C-S-n") 'make-frame-command)
;; (global-set-key (kbd "C-S-s") 'write-file)
;; (global-set-key (kbd "C-S-t") 'xah-open-last-closed)
(if (memq window-system '(mac ns))
(progn
(global-set-key (kbd "s-a") 'mark-whole-buffer)
(global-set-key (kbd "s-n") 'xah-new-empty-buffer)
(global-set-key (kbd "s-o") 'find-file)
(global-set-key (kbd "s-s") 'save-buffer)
(global-set-key (kbd "s-S") 'write-file)
(global-set-key (kbd "s-x") 'xah-cut-all-or-region)
(global-set-key (kbd "s-c") 'xah-copy-all-or-region) ;'kill-ring-save in vanilla
(global-set-key (kbd "s-v") 'yank)
;; (global-set-key (kbd "s-T") 'xah-open-last-closed)
(define-key isearch-mode-map (kbd "M-v") 'isearch-yank-kill)
;; (global-set-key (kbd "s-z") 'undo) 'undo-fu-only-redo
;; (global-set-key (kbd "s-w") 'xah-close-current-buffer)
(global-set-key (kbd "s-<right>") 'forward-word)
(global-set-key (kbd "s-<left>") 'backward-word)
(global-set-key (kbd "s-<backspace>") #'ct/kill-to-beginning-of-line-dwim)
(global-set-key (kbd "s-<delete>") #'kill-line)
)
(progn
(global-set-key (kbd "C-a") 'mark-whole-buffer)
(global-set-key (kbd "C-n") 'xah-new-empty-buffer)
(global-set-key (kbd "C-o") 'find-file)
(global-set-key (kbd "C-s") 'save-buffer)
(global-set-key (kbd "C-v") 'yank)
(define-key isearch-mode-map (kbd "C-v") 'isearch-yank-kill)
(global-set-key (kbd "C-z") 'undo)
(global-set-key (kbd "C-w") 'xah-close-current-buffer)))
)
(use-package undo-fu
:config
(global-set-key [remap undo] 'undo-fu-only-undo)
(if (memq window-system '(mac ns x))
(global-set-key (kbd "s-Z") 'undo-fu-only-redo)
(global-set-key (kbd "C-Z") 'undo-fu-only-redo))
;; Global default undo key cannot be shiftef, because S-/ is ?, so keep it as-is
;; (global-unset-key (kbd "C-/"))
;; (global-set-key (kbd "C-/") 'undo-fu-only-undo)
;; (global-set-key (kbd "C-Z") 'undo-fu-only-redo)
)
;; XFK bindings in command and insert mode
(with-eval-after-load "undo-fu"
(with-eval-after-load "xah-fly-keys"
;; XFK shared bindings in both modes
(if (memq window-system '(mac ns x))
(progn
(define-key xah-fly-shared-map (kbd "s-z") #'undo-fu-only-undo)
(define-key xah-fly-shared-map (kbd "s-S-Z") #'undo-fu-only-redo))
(progn
(define-key xah-fly-shared-map (kbd "C-z") #'undo-fu-only-undo)
(define-key xah-fly-shared-map (kbd "C-Z") #'undo-fu-only-redo)))
;; XFK command mode bindings
(define-key xah-fly-command-map "y" #'undo-fu-only-undo)
(define-key xah-fly-command-map "Y" #'undo-fu-only-redo)
))
YASnippets comes with auto-completion for language snippets in a format similar to TextMate’s snippet syntax (which can be imported, mostly). Actual snippets come from https://github.com/AndreaCrotti/yasnippet-snippets and are installed by default.
(use-package yasnippet
:ensure t
:delight yas-minor-mode
:config
;; Yas messages stretches the status buffer when it starts up.
(setq yas-verbosity 0)
(add-hook 'prog-mode-hook #'yas-minor-mode)
(add-hook 'message-mode-hook #'yas-minor-mode)
;; Once after launch reload all snippets. (Done automatically by yas-global-mode.)
(yas-reload-all)
:init
(yas-global-mode))
(use-package yasnippet-snippets
:ensure t
:after yasnippet)
The built-in functions change the case of the word at point starting at point. I want the whole word to change when I use these shortcuts at all.
M-d
is for word deletion, so the mnemonic of using “d” for “downcasing” doesn’t work. My default key binding is M-l
for “lowercase”.
Published here: https://christiantietze.de/posts/2021/03/change-case-of-word-at-point/
(defun ct/word-boundary-at-point-or-region ()
"Return the boundary (beginning and end) of the word at point, or region, if any.
URL: https://christiantietze.de/posts/2021/03/change-case-of-word-at-point/"
(let ((deactivate-mark nil)
$p1 $p2)
(if (use-region-p)
(setq $p1 (region-beginning)
$p2 (region-end))
(save-excursion
(skip-chars-backward "[:alpha:]")
(setq $p1 (point))
(skip-chars-forward "[:alpha:]")
(setq $p2 (point))))
(list $p1 $p2)))
(defun ct/capitalize-region (p1 p2)
"Capitalize whole words, downcasing all but the first letter."
(downcase-region p1 p2)
(upcase-initials-region p1 p2))
(defun ct/capitalize-word-at-point (&optional arg)
"Capitalize, i.e. upcase the initial character of region of the word at point.
Lowercases other characters from region or word. Moves point to end of word.
When called with ARG (a C-u or numerical prefix), passes on to the
default `capitalize-word'."
(interactive "*P")
(if arg
(call-interactively #'capitalize-word)
(let* (($bounds (ct/word-boundary-at-point-or-region))
($p1 (car $bounds))
($p2 (cadr $bounds)))
(ct/capitalize-region $p1 $p2)
(goto-char $p2))))
(defun ct/downcase-word-at-point (&optional arg)
"Lowercase characters in region, or whole word at point.
Moves point to end of word.
When called with ARG (a C-u or numerical prefix), passes on to the
default `downcase-word'."
(interactive "*P")
(if arg
(call-interactively #'downcase-word)
(let* (($bounds (ct/word-boundary-at-point-or-region))
($p1 (car $bounds))
($p2 (cadr $bounds)))
(downcase-region $p1 $p2)
(goto-char $p2))))
(defun ct/upcase-word-at-point (&optional arg)
"Upcase (all-caps) characters in region, or whole word at point.
Moves point to end of word.
When called with ARG (a C-u or numerical prefix), passes on to the
default `upcase-word'."
(interactive "*P")
(if arg
(call-interactively #'upcase-word)
(let* (($bounds (ct/word-boundary-at-point-or-region))
($p1 (car $bounds))
($p2 (cadr $bounds)))
(upcase-region $p1 $p2)
(goto-char $p2))))
;; Set global shortcuts
(global-set-key [remap capitalize-word] #'ct/capitalize-word-at-point)
(global-set-key [remap upcase-word] #'ct/upcase-word-at-point)
(global-set-key [remap downcase-word] #'ct/downcase-word-at-point)
- =’rfc1345=: Instead of pressing the
Option key + u
on the Mac to enter umlaut composition mode, use&
as the prefix key to compose accented characters with help in the minibuffer. - =’german-postfix=: typing
ae
becomes “ä”.
(custom-set-variables '(default-input-method "german-postfix"))
Notable keys:
- The
C-\
key toggles this input method on or off. That’s useful to not have “does” combine into “dös”. C-x RET C-\
configures what this key does, i.e. selects the buffer-local input method.
The same idea as ace-window
: character based movement. Issue a “go to link” command, then select a link on screen. Excellent overview of interactions: https://karthinks.com/software/avy-can-do-anything/
(use-package avy
:ensure
:demand
:config
;; Karthik Chikmagalur @karthink's init:
;; https://github.com/karthink/.emacs.d/blob/c717161d7653a4615c1f0a6b22cbc2994b145cd2/init.el
(defun kt/avy-isearch (&optional arg)
"Goto isearch candidate in this window with hints."
(interactive "P")
(let ((avy-all-windows)
(current-prefix-arg (if arg 4)))
(call-interactively 'avy-isearch)))
:bind
("C-'" . 'avy-goto-char-timer)
("M-'" . 'avy-goto-line)
(:map isearch-mode-map
("C-'" . kt/avy-isearch)))
(with-eval-after-load "xah-fly-keys"
(with-eval-after-load "avy"
(define-key 'xah-fly-leader-key-map (kbd "'") #'avy-goto-char-timer)))
(setq system-time-locale "en_US.utf8")
(prefer-coding-system 'utf-8)
(set-default-coding-systems 'utf-8)
(set-terminal-coding-system 'utf-8)
(set-keyboard-coding-system 'utf-8)
(set-language-environment 'utf-8)
Wiki link usage:
C-c C-s w
: add/modify wiki link at pointC-c C-o
: open thing at point, also wiki link
(use-package markdown-mode
:ensure t
:mode (("README\\.md\\'" . gfm-mode)
("\\.txt\\'" . markdown-mode)
("\\.md\\'" . markdown-mode)
("\\.markdown\\'" . markdown-mode))
:config
(defun ct/markdown-insert-comment ()
"Insert Markdown comments for <!--ct-->!"
(interactive)
(if (use-region-p)
(progn ; active region
(let (($p1 (region-beginning))
($p2 (region-end)))
(goto-char $p2)
(insert "-->")
(goto-char $p1)
(insert "<!--ct: ")
(goto-char (+ $p2 8))))
(progn ; no selection
(let (($p1 (point)))
(insert "-->")
(goto-char $p1)
(insert "<!--ct: ")
(goto-char (+ $p1 8))))
) ; if
) ; defun
(defun ct/markdown-insert-comment--folie ()
"Insert Markdown comments for <!--FF-->!"
(interactive)
(if (use-region-p)
(progn ; active region
(let (($p1 (region-beginning))
($p2 (region-end)))
(goto-char $p2)
(insert "-->")
(goto-char $p1)
(insert "<!--FF: ")
(goto-char (+ $p2 8))))
(progn ; no selection
(let (($p1 (point)))
(insert "-->")
(goto-char $p1)
(insert "<!--FF: ")
(goto-char (+ $p1 8))))))
(defun ct/markdown-insert-comment--maybe-comma ()
"Insert Markdown comments for <!--FF-->!"
(interactive)
(insert "<!--ct: \",\"?-->"))
(define-key markdown-mode-map (kbd "C-c C-t C-t") #'ct/markdown-insert-comment)
(define-key markdown-mode-map (kbd "C-c C-t C-f") #'ct/markdown-insert-comment--folie)
(define-key markdown-mode-map (kbd "C-c C-t C-,") #'ct/markdown-insert-comment--maybe-comma)
(setq markdown-hide-markup t)
(setq markdown-asymmetric-header t)
(setq markdown-italic-underscore t)
(setq markdown-enable-wiki-links t)
(setq markdown-wiki-link-alias-first t) ;; [[alias first|pagename_second]]
)
(add-hook 'markdown-mode-hook #'display-line-numbers-mode)
(add-hook 'markdown-mode-hook #'visual-line-mode)
Overscroll in both directions to keep the cursor centered at all times.x
(use-package centered-cursor-mode
:config
;; ccm-scroll-down conflicts with my paste key
(unbind-key (kbd "M-v") 'ccm-map)
:init
(setq ccm-recenter-at-end-of-file t))
(use-package topspace
:ensure t)
C-c { { { {
and C-c } } } }
allow setting the width on the fly. <3
(use-package olivetti
:ensure t
:defer t
:after org
:delight ;; Don't show in modeline; it's obvious when the mode is on.
:interpreter ("olivetti" . olivetti-mode)
:config
(setq olivetti-body-width 0.65
olivetti-minimum-body-width 80
olivetti-recall-visual-line-mode-entry-state t)
(defun ct/olivetti-set-width (w)
"Increase the column width to accommodate the 24px frame border (inner padding)"
(olivetti-set-width (+ 2 w)))
(defun ct/olivetti-org-mode ()
(olivetti-mode 1)
(ct/olivetti-set-width ct/org-width))
(defun ct/olivetti-org-agenda-mode ()
(olivetti-mode 1)
(ct/olivetti-set-width ct/org-agenda-width))
:hook ((message-mode . olivetti-mode)
(org-agenda-finalize . ct/olivetti-org-agenda-mode)
(org-mode . ct/olivetti-org-mode))
)
(use-package focus
:config
(set-face-attribute 'focus-unfocused nil
:foreground "azure3"))
(use-package emacs
:demand
:after (:all olivetti centered-cursor-mode topspace)
:config
(defun ct/writeroom-mode (&optional enable)
"Toggles olivetti and centered-cursor-mode on/off."
(interactive)
(display-line-numbers-mode -1) ;; no need to re-enable, just toggle off
(let* ((is-in-writeroom-mode (or olivetti-mode centered-cursor-mode))
(enable (or enable (not is-in-writeroom-mode)))
($value (if enable +1 -1)))
(message (if enable "yes" "no"))
(olivetti-mode $value)
(topspace-mode $value)
(centered-cursor-mode $value))))
(use-package adaptive-wrap
:defer t
:ensure t
:hook (markdown-mode . adaptive-wrap-prefix-mode))
(use-package hl-todo
:ensure t
:config
(setq hl-todo-keyword-faces
'(("TODO" . modus-themes-intense-yellow)
("FIXME" . modus-themes-intense-red)
("DEBUG" . modus-themes-intense-neutral)))
:bind
(:map hl-todo-mode-map
;; The prefix `C-c C-t` is used for comment insertion in texts, too
("C-c C-t p" . hl-todo-previous)
("C-c C-t n" . hl-todo-next)))
The default compilation command began to prepend \\nonstop\\input
some time in late 2020, and that breaks file names with spaces in my shell somehow.
(use-package tex-mode
:defer t
:config
(setq tex-start-commands nil))
The interactive shell of tex-mode
is annoying, especially when I compile mostly invoices where I change numbers and don’t produce errors I would want to react to. AucTeX gets out of my way, prints the result in the minibuffer, and it compiles filenames with spaces in the default settings.
Enabling AucTeX automatically associates .tex
and other file extensions with this mode, so tex-mode
is obsolete by now.
(use-package auctex
:defer t
:ensure t)
Use subword-mode
to make e.g. M-f
move forward up to the next camel-cased part of a variable instead of the next word boundary. camelCase
(use-package emacs
:delight subword-mode
:hook (after-init . global-subword-mode))
(use-package flymake-languagetool
:ensure t
:config
(setq flymake-languagetool-server-jar nil; "/opt/homebrew/Cellar/languagetool/6.1/libexec/languagetool-server.jar"
;; homebrew server connects, but also dies
flymake-languagetool-url "http://localhost:8081")
:hook ((markdown-mode . flymake-languagetool-load))
:bind
(:map flymake-mode-map
("s-." . flymake-languagetool-correct-dwim)))
(use-package highlight-indent-guides
:config
(setq highlight-indent-guides-method 'bitmap))
Enable visual line mode and showing parentheses in all programming modes.
(use-package emacs
:after highlight-indent-guides
:hook ((prog-mode . visual-line-mode)
(prog-mode . show-paren-mode)
(prog-mode . display-line-numbers-mode)
(prog-mode . highlight-indent-guides-mode)
;; Treat camel-case as words
(prog-mode . subword-mode)))
(defun ct/delete-trailing-whitespace-when-writing-file ()
(add-to-list 'write-file-functions 'delete-trailing-whitespace))
(add-hook 'prog-mode-hook #'ct/delete-trailing-whitespace-when-writing-file)
When something appears to be wrong, check the current flycheck configuration:
C-c ! v
M-x flycheck-verify-setup
(use-package flycheck
:ensure t
:hook (prog-mode . flycheck-mode))
;; Scroll to the bottom of the compilation buffer:
; (setq compilation-scroll-output t)
;; Scroll to the bottom OR the first error in the compilation buffer
(setq compilation-scroll-output 'first-error)
- Keep compilation window small,
- Dedicate compilation window to its buffer to not accidentally open other buffers in it.
This was annoying because it opens the compilation buffer in the first place on screen, even though it wouldn’t need to. Affected tangling the org init file and also ag
searches.
(setq ct/compilation-window-height 10)
(defun ct/create-proper-compilation-window ()
"Setup the *compilation* window with custom settings."
(when (not (get-buffer-window "*compilation*"))
(save-selected-window
(save-excursion
(let* ((w (split-window-vertically))
(h (window-height w)))
(select-window w)
(switch-to-buffer "*compilation*")
;; Reduce window height
(shrink-window (- h ct/compilation-window-height))
;; Prevent other buffers from displaying inside
(set-window-dedicated-p w t)
)))))
;; (add-hook 'compilation-mode-hook #'ct/create-proper-compilation-window)
(defun ct/quit-and-kill-auxiliary-windows ()
"Kill compilation buffer and its window on quitting"
(local-set-key (kbd "q") #'kill-buffer-and-window))
(add-hook 'special-mode-hook #'ct/quit-and-kill-auxiliary-windows)
(add-hook 'compilation-mode-hook #'ct/quit-and-kill-auxiliary-windows)
Used in ancient outputs as a page break character: https://www.gnu.org/software/emacs/manual/html_node/emacs/Pages.html
Can be used as section separators – but I want these to display as lines in GUI emacs, then.
- page-break-lines inserts unicode characters; gets line width wrong in collapsed org buffer, though
- form-feed-mode draws a line that also works for collapsed, truncated org buffers
(use-package form-feed
:ensure
:delight
:config
(custom-set-variables
'(form-feed-include-modes '(prog-mode text-mode help-mode compilation-mode org-mode)))
:init
(global-form-feed-mode +1))
(require 'ansi-color)
(defun colorize-compilation-buffer ()
(let ((inhibit-read-only t))
(ansi-color-apply-on-region (point-min) (point-max))))
(add-hook 'compilation-filter-hook #'colorize-compilation-buffer)
The default project-compile
command always prompts for input interactively. For some modes, or even projects, I want to bind this to a default command.
(defun ct/project-compile (command)
"Run `compile' non-interactively in the project root with `command'."
(let ((default-directory (project-root (project-current t)))
(compilation-buffer-name-function
(or project-compilation-buffer-name-function
compilation-buffer-name-function)))
(compile command)))
In principle, saving silently can work by using (save-some-buffers t)
to save all modified buffers without asking, or by temporarily overriding compilation-ask-about-save
:
(let ((compilation-ask-about-save nil)) (compile command here))
But the following function only auto-saved project-related, file-visiting buffers. It will still ask for buffers modified elsewhere, which I prefer:
(defun ct/project-save-all-buffers (&optional arg proj)
"Save all file-visiting buffers in PROJ, without asking if ARG is non-nil.
Falls back to `project-current' if PROJ is not specified."
(let* ((proj (or proj (project-current)))
(buffers (project-buffers (project-current))))
(dolist (buf buffers)
;; Act on base buffer of indirect buffers, if needed.
(with-current-buffer (or (buffer-base-buffer buf) buf)
(when (and (buffer-file-name buf) ;; Ignore all non-file-visiting buffers.
(file-writable-p ;;
(buffer-file-name buf)) ;; Ignore readonly buffers.
(buffer-modified-p buf)) ;; Ignore all unchanged buffers.
(let ((buffer-save-without-query t)) ; Save silently.
(save-buffer arg)))))))
Can also use save-some-buffers
, supplying a predicate to decide. See how magit-save-repository-buffers
limits the scope to files withing the repository:
(save-some-buffers arg (lambda () (and buffer-file-name ;; - Check whether refreshing is disabled. (not magit-inhibit-refresh-save) ;; - Check whether the visited file is either on the ;; same remote as the repository, or both are on ;; the local system. (equal (file-remote-p buffer-file-name) remote) ;; Delayed checks that are more expensive for remote ;; repositories, due to the required network access. ;; - Check whether the file is inside the repository. (equal (magit-rev-parse-safe "--show-toplevel") topdir) ;; - Check whether the file is actually writable. (file-writable-p buffer-file-name))))
(use-package ruby-mode
:hook
(ruby-mode . tree-sitter)
(ruby-mode . tree-sitter-hl-mode))
(use-package rvm
:ensure t)
Recognize ERB template files: http://web-mode.org/
(add-to-list 'auto-mode-alist '("\\.erb\\'" . web-mode))
(add-to-list 'auto-mode-alist '("\\.rhtml\\'" . web-mode))
(use-package ruby-electric
:delight ruby-electric-mode
:config
(add-hook 'ruby-mode-hook #'ruby-electric-mode))
(use-package robe
:ensure t
:config
(defun ct/company-robe-backend ()
(set (make-local-variable 'company-backends)
'((company-dabbrev-code company-robe company-capf))))
(add-hook 'ruby-mode-hook #'ct/company-robe-backend)
(add-hook 'ruby-mode-hook #'robe-mode))
(setq python-indent-offset 4)
(use-package company-anaconda ;; Python
:after company
:config
(defun ct/company-anaconda-backend ()
(set (make-local-variable 'company-backends)
'((company-dabbrev-code company-anaconda company-capf))))
(add-hook 'python-mode-hook #'ct/company-anaconda-backend))
(use-package web-mode
:ensure t
:commands web-mode
:mode
(("\\.html\\'" . web-mode)
("\\.phtml\\'" . web-mode)
("\\.tpl\\.php\\'" . web-mode)
("\\.html\\.twig\\'" . web-mode))
:config
(setq web-mode-attr-indent-offset 2
web-mode-code-indent-offset 2
web-mode-css-indent-offset 2
web-mode-indent-style 2
web-mode-markup-indent-offset 2 ;; markup is HTML
web-mode-sql-indent-offset 2
;; Draw vertical column highlighter between opening/closing block
web-mode-enable-current-element-highlight t
web-mode-enable-current-column-highlight t)
;; Set PHP as the embedded language for phtml/tpl.php files.
(add-to-list 'web-mode-engines-alist
'(("php" . "\\.phtml\\'")
("php" . "\\.tpl\\.php\\'")))
(defun ct/web-mode-init ()
(setq web-mode-style-padding 2)
(emmet-mode)
;; Manually override the supported company backenda
(set (make-local-variable 'company-backends)
'(company-css company-web-html company-yasnippet company-files company-php company-dabbrev-code company-capf company-files))
;; Use emacs browser to browse local documentation:
(setq-local browse-url-browser-function #'eww-browse-url))
(defun ct/web-mode-before-autocomplete-hook ()
(let ((web-mode-cur-language (web-mode-language-at-pos)))
(if (string= web-mode-cur-language "php")
(yas-activate-extra-mode 'php-mode)
(yas-deactivate-extra-mode 'php-mode))
(if (string= web-mode-cur-language "css")
(setq emmet-use-css-transform t)
(setq emmet-use-css-transform nil))))
:hook
;; (web-mode . lsp)
(web-mode . ct/web-mode-init)
(web-mode-before-auto-complete . ct/web-move-before-autocomplete-hook)
:bind
(:map web-mode-map
;; HTML element selection comes closest to paragraph selection
("M-h" . web-mode-element-select))
)
Based on the code by @emacs_gifs from https://emacsgifs.github.io/html-entities-helper, but the scraped entities and the helper function to select an entity are put into the private namespace of the ct/html-insert-entity
function.
I saved this in an other file because the many entities slowed down editing this org file a lot.
See my blog post: https://christiantietze.de/posts/2020/07/emacs-insert-html-entity
(load-file "~/.emacs.d/src/html-insert-entity.el")
Emmet’s expansion is triggered via <C-J>
by default.
In TextMate, you could tab between edit points, e.g. from list item to list item. Here, it’s:
<C-M-left>
is “Previous Edit Point”(M-x emmet-prev-edit-point)
<C-M-right>
is “Next Edit Point”(M-x emmet-next-edit-point)
<C-c C-c w>
is used to apply the expansion from the minibuffer around the current selection.
See the website for more details. Emmet can do a lot more stuff.
(use-package emmet-mode
:delight emmet-mode
:ensure t
:config
(add-hook 'web-mode-hook #'emmet-mode)
(add-hook 'sgml-mode-hook #'emmet-mode) ;; Auto-start on any markup modes
(add-hook 'css-mode-hook #'emmet-mode) ;; enable Emmet's css abbreviation.
)
Always just two spaces for CSS/SCSS, thank you.
(add-hook 'css-mode-hook #'ct/two-space-indentation)
(add-hook 'scss-mode-hook #'ct/two-space-indentation)
(setq css-indent-offset 2)
;; js-mode does not use tab-width
(setq js-indent-level 2)
(use-package js2-mode)
Used in JS or node-based projects to specify the version of node we’re using. This is not intended to just use the “default” of nvm. You don’t need nvm for that, just the shell environment that resolved which node
.
(use-package nvm
:defer)
(use-package js-comint
:config
(define-key js-mode-map [remap eval-last-sexp] #'js-comint-send-last-sexp))
Use NVM when active.
(with-eval-after-load 'nvm
(with-eval-after-load 'js-comint
(js-do-use-nvm)))
- Plain
php-mode
is fine, butphps-mode
implements the PHP parser in Elisp, so it works without any reference to the PHP runtime/CLI tools. You can usephps-mode
over TRAMP as well because of this. phps-mode
is faster but has the annoying habit of opening error buffers in my neotree buffer or a window split.<C-c C-f>
displays documentation for symbol at point; changebrowse-url-browser-function
to display it inside emacs.
Consider adding this file extension lookup to enable any of the PHP modes. Use =web-mode= for PHP files instead because that’s my main use case. Enable PHP modes for Laravel projects and the like where needed. This way, I neither spawn an LSP session nor run into ac-php
errors for completions.
:mode ("\\.php\\'" "\\.phtml\\'")
(use-package phps-mode
:after flycheck
:ensure t
:hook
(phps-mode . ct/phps-mode-hook)
(phps-mode . tree-sitter-mode)
(phps-mode . tree-sitter-hl-mode)
:config
(defun ct/phps-mode-hook ()
(setq-local tab-width 2))
(phps-mode-flycheck-setup)
(setq phps-mode-async-process t)
(setq phps-mode-async-process-using-async-el t))
(with-eval-after-load 'phps-mode
(with-eval-after-load 'tree-sitter-langs
(add-to-list 'tree-sitter-major-mode-language-alist '(phps-mode . php))))
(with-eval-after-load 'phps-mode
(with-eval-after-load 'eglot
(add-to-list 'eglot-server-programs '(phps-mode . ("intelephense" "--stdio")))))
(use-package php-mode :ensure t :config (defun ct/use-eww-as-local-browser () "Use eww as browser to browse links, e.g. local documentation" (setq-local browse-url-browser-function #'eww-browse-url)) (add-hook 'php-mode-hook #'ct/use-eww-as-local-browser) ;; We should use web-mode for mixed PHP/HTML; php-mode is for pure PHP (setq php-mode-template-compatibility nil))
company-php
, which confusingly is part of the ac-php project, requires a TAGS database file in the project root, unless you have a .projectile
file or vendor/autoload.php
.
cd /project/to/poject/root echo ".ac-php-conf.json" >> .gitignore touch .ac-php-conf.json
Then you have to run M-x ac-php-remake-tags
to remake the TAGS database.
(use-package company-php
:after (company php-mode)
:config
(defun ct/company-php-mode-hook ()
(ac-php-core-eldoc-setup)
(set (make-local-variable 'company-backends)
'((;;company-ac-php-backend ;; can't get this to work
company-dabbrev-code)
company-capf company-files))
(local-set-key (kbd "M-.") #'ac-php-find-symbol-at-point))
:hook
(php-mode . ct/company-php-mode-hook)
:bind
(:map php-mode-map
;; Jump to definition (optional)
("M-]" . ac-php-find-symbol-at-point)
;; Return back (optional)
("M-[" . ac-php-location-stack-back))
)
company-sourcekit
requires sourcekittendaemon
in PATH and that tool hasn’t been updated sicne Swift 4.2 in late 2018. So we’re stuck with LSP.
(with-eval-after-load 'swift-mode
(defun ct/swift-build-command (&optional arg)
"Builds the Swift package in the current project directory."
(interactive "p")
(ct/project-save-all-buffers t)
(ct/project-compile "swift build"))
(defun ct/swift-test-command (&optional arg)
(interactive "p")
(ct/project-save-all-buffers t)
(ct/project-compile "swift test"))
(define-key swift-mode-map (kbd "s-u") #'ct/swift-test-command)
(define-key swift-mode-map (kbd "s-b") #'ct/swift-build-command))
(use-package magit
:defer t
:ensure
:config
(setq magit-display-buffer-function #'magit-display-buffer-same-window-except-diff-v1)
(setq magit-diff-refine-hunk t)
;; Sort branches by modification date (creation date of commits)
(setq magit-list-refs-sortby "-creatordate")
;; Display (rather wide) fringes in Magit buffers to show the collapsible sections
(defun ct/magit-window-config ()
"Used in `window-configuration-change-hook' to configure fringes for Magit."
(set-window-fringes nil 20 0))
(defun ct/magit-mode-hook ()
"Custom `magit-mode' behaviours."
;; Install the fringe setter using buffer-local (!) mode hooks.
(add-hook 'window-configuration-change-hook
'ct/magit-window-config
nil :local)
;; Show magit stuff in smaller font; since there's no magit-base-face, cannot set :height uniformly
(text-scale-decrease 1)
(subword-mode)
(ct/disable-local-line-spacing))
(add-hook 'magit-mode-hook #'ct/magit-mode-hook)
;; I'm using `hl-line-mode' and `lin' to highlight the current line in Magit.
;; UI lines (i.e. non-code) should also be variable pitch. Use :family, not :font,
;; to allow the height to be inherited.
(let ((sans-serif-family (face-attribute 'variable-pitch :family)))
(set-face-attribute 'magit-diff-file-heading nil :family sans-serif-family :weight 'normal :bold nil)
(set-face-attribute 'magit-diff-file-heading-highlight nil :family sans-serif-family :weight 'normal :bold nil)
(set-face-attribute 'magit-section-child-count nil :family sans-serif-family :weight 'normal :bold nil)
(set-face-attribute 'magit-section-heading nil :family sans-serif-family :bold t)
(set-face-attribute 'magit-section-highlight nil :family sans-serif-family :bold t))
;; To not override nested faces when highlighting, disable section highlight completely:
;; https://github.com/magit/magit/issues/4599#issuecomment-1050204282
(setq magit-section-highlight-hook nil)
;; Show absolute dates in the log margin.
(setq magit-log-margin '(t "%Y-%m-%d %H:%M" magit-log-margin-width t 18))
;; Do not add <C-x g>, <C-x M-g> and <C-c M-g> by default; I'll add these.
(setq magit-define-global-key-bindings nil)
;; Was 'magit-copy-buffer-revision but 99% of the time I want to close a buffer.
(define-key magit-mode-map (kbd "M-w") #'kill-buffer-and-window)
;; Dired-like key binding to hide log details (author and date)
(define-key magit-log-mode-map (kbd "(") #'magit-toggle-margin)
(global-set-key (kbd "C-x g") #'magit-status)
(global-set-key (kbd "C-x M-g") #'magit-dispatch)
(global-set-key (kbd "C-c g") #'magit-file-dispatch))
(use-package magit-todos
:after magit
:ensure
:defer
:custom
(magit-todos-exclude-globs '(".git/" "Carthage/"))
:hook (magit-status-mode . magit-todos-mode))
C-u RET
visits the file of the TODO in the system default editor thanks to consult.el’s helper that I’m also using via Embark:
(with-eval-after-load 'magit-todos
(with-eval-after-load 'consult
(defun ct/magit-todos-open-item-default-editor (item)
(let* ((filename (string-trim-right (magit-todos-item-filename item)))
(path (expand-file-name filename)))
(message (concat "Opening externally: '" filename "'"))
(consult-file-externally path)))
(cl-defun ct/magit-todos-jump-to-item (arg &key peek externally (item (oref (magit-current-section) value)))
(interactive "P")
(if (or externally arg)
(ct/magit-todos-open-item-default-editor item)
(magit-todos-jump-to-item :peek peek :item item)))
(define-key magit-todos-item-section-map [remap magit-visit-thing] #'ct/magit-todos-jump-to-item)))
(use-package forge
:after magit
:config
;; Was 'magit-copy-buffer-revision but 99% of the time I want to close a buffer.
(unbind-key "M-w" forge-topic-mode-map))
(use-package lua-mode)
(use-package pico8-mode
:after lua-mode
:load-path "~/.emacs.d/src/pico8-mode"
:config
(add-to-list 'auto-mode-alist '("\\.p8\\'" . pico8-mode)))
Resolving merge conflicts from magit starts in smerge-mode
. The prefix is C-x ^
; I’d rather use n
and p
directly to go from one conflict to another.
(with-eval-after-load 'smerge-mode
(define-key smerge-mode-map (kbd "C-p") #'smerge-prev)
(define-key smerge-mode-map (kbd "C-n") #'smerge-next)
(define-key smerge-basic-map (kbd "<down>") #'smerge-keep-lower)
(define-key smerge-basic-map (kbd "<up>") #'smerge-keep-upper))
(use-package tree-sitter
:defer t)
(use-package tree-sitter-langs
:after tree-sitter
:defer t)
repeat-mode
helps to stay in the context of a complex keymap so you only repeat the last letters of the map
(use-package emacs
:hook (after-init . repeat-mode)
:config
;; https://karthinks.com/software/it-bears-repeating/
(defun ct/repeatize (keymap)
"Add `repeat-mode' support to a KEYMAP."
(map-keymap
(lambda (_key cmd)
(when (symbolp cmd)
(put cmd 'repeat-map keymap)))
(symbol-value keymap)))
(with-eval-after-load 'smerge-mode
(ct/repeatize 'smerge-basic-map)))
via https://karthinks.com/software/it-bears-repeating/
(with-eval-after-load 'repeat-mode
(with-eval-after-load 'which-key
;; Disable the built-in repeat-mode hinting
(custom-set-variables '(repeat-echo-function #'ignore))
;; Spawn or hide a which-key popup
(advice-add 'repeat-post-hook :after
(defun repeat-help--which-key-popup ()
(if-let ((cmd (or this-command real-this-command))
(keymap (or repeat-map
(repeat--command-property 'repeat-map))))
(run-at-time
0 nil
(lambda ()
(which-key--create-buffer-and-show
nil (symbol-value keymap))))
(which-key--hide-popup))))))
Try:
(use-package emacs
:if (not (window-system))
:init
(require 'mouse)
(xterm-mouse-mode t)
(defun track-mouse (e))
(setq mouse-sel-mode t))
Customizations by Philip K used to highlight symbol at point of the mouse click.
(use-package emacs
:config
(defun highlight-symbol-at-mouse (e)
"Highlight symbol at mouse click E."
(interactive "e")
(save-excursion
(mouse-set-point e)
(highlight-symbol-at-point)))
(defun context-menu-highlight-symbol (menu click)
"Populate MENU with command to search online."
(save-excursion
(mouse-set-point click)
(when (symbol-at-point)
(define-key-after menu [highlight-search-separator] menu-bar-separator)
(define-key-after menu [highlight-search-mouse]
'(menu-item "Highlight Symbol" highlight-symbol-at-mouse
:help "Highlight symbol at point"))))
menu)
(defun magit-current-section-menu-label ()
"Menu item label for section at point."
(pcase (magit-diff-scope)
('hunk "Hunk")
('hunks "Hunks")
('file "File")
('files "Files")
('module "Module")
('region "Region")))
(defun magit-context-menu (menu click)
"Populate MENU with commands that perform actions on magit sections at point."
(save-excursion
(mouse-set-point click)
;; Ignore log and commit buffers, only apply to status.
;; `magit-section-match' without 2nd parameter uses section at point automatically.
(when (magit-section-match 'status magit-root-section)
(when-let ((section-label (magit-current-section-menu-label)))
(define-key-after menu [magit-separator] menu-bar-separator)
(when (eq 'staged (magit-diff-type))
(define-key-after menu [magit-unstage-thing-at-mouse]
`(menu-item (concat "Unstage " ,section-label)
magit-unstage
:help "Unstage thing at point")))
(when (member (magit-diff-type) '(unstaged untracked))
(define-key-after menu [magit-stage-thing-at-mouse]
`(menu-item (concat "Stage " ,section-label)
magit-stage
:help "Stage thing at point")))
(when (magit-section-match '(magit-module-section
magit-file-section
magit-hunk-section))
(define-key-after menu [magit-discard-thing-at-mouse]
`(menu-item (concat "Discard " ,section-label)
magit-discard
:help "Discard thing at point"))))))
menu)
(setq-default context-menu-functions
'(context-menu-ffap
magit-context-menu
context-menu-highlight-symbol
occur-context-menu
context-menu-region
context-menu-undo))
:hook
(after-init . context-menu-mode))
Unbind shift-click opening the appearance menu, of all things.
(global-set-key (kbd "S-<down-mouse-1>") #'mouse-set-mark) ;; was: mouse-appearance-menu
(global-set-key (kbd "A-<down-mouse-1>") #'mouse-drag-region-rectangle)
(use-package edit-server
:if window-system
:ensure t
:init
(add-hook 'after-init-hook 'server-start t)
(add-hook 'after-init-hook 'edit-server-start t))
Some commands are destructive and warrant a longer “yes” reply by the user. But some, like killing all buffers in a project, don’t. Shorten these to “y”.
(defun yes-or-no-p->-y-or-n-p (orig-fun &rest r)
(cl-letf (((symbol-function 'yes-or-no-p) #'y-or-n-p))
(apply orig-fun r)))
(advice-add 'project-kill-buffers :around #'yes-or-no-p->-y-or-n-p)
My setup relies on OfflineIMAP to sync the Maildir-formatted mail into ~/.Mail
, which is then indexed by notmuch
using a cron job.
*/5 * * * * /usr/local/bin/notmuch new >/dev/null 2>&1
I want to see signatures by default, because GitHub issue links are hidden away in them:
(setq notmuch-wash-signature-lines-max 0)
(setq message-default-mail-headers "Cc: \nBcc: \n"
message-kill-buffer-on-exit t
user-mail-address "me@christiantietze.de"
user-full-name "Christian Tietze"
ct/user-sender (concat user-full-name " <" user-mail-address ">"))
I don’t want to auto-fill (insert hard line breaks) when writing email. But I do want to see a visual guide.
;; Turn of autofill when composing.
(add-hook 'message-mode-hook #'turn-off-auto-fill)
(add-hook 'message-mode-hook #'display-fill-column-indicator-mode)
(with-eval-after-load 'corfu
(defun ct/turn-off-corfu-mode ()
(corfu-mode -1))
(add-hook 'message-mode-hook #'ct/turn-off-corfu-mode))
Warn before sending email with empty subjects.
(defun ct/confirm-empty-subject ()
"Allow to quit when current message subject is empty."
(or (message-field-value "Subject")
(yes-or-no-p "Really send without Subject? ")
(keyboard-quit)))
(add-hook 'message-send-hook #'ct/confirm-empty-subject)
When viewing/reading email in a notmuch message buffer, the dot .
is a prefix key to a key map full of attachment handling functions, like . s
to save attachment and . v
to view.
(defun ct/set-downloads-dir ()
(setq-local default-directory "~/Downloads"))
(add-hook 'notmuch-show-hook #'ct/set-downloads-dir)
Use C-c RET C-a
to attach marked files in dired to the current composition window. Despite the name, also works without Gnus.
Confer https://emacs.stackexchange.com/a/33463/18986 for other approaches.
(add-hook 'dired-mode-hook #'turn-on-gnus-dired-mode)
Make sure to have this set in custom.el
because that’s the only place the setting will work:
'(epg-gpg-program "/opt/homebrew/bin/gpg")
;;(epa-file-enable)
;;;;
;;;; Disabled since homebrew install of v27.x -- apparently, the package is not included
;;;;
;; (use-package starttls
;; :ensure t
;; :config
;; (setq starttls-use-gnutls t
;; starttls-gnutls-program "gnutls-cli"
;; starttls-extra-arguments '("--insecure")
;; auth-sources '("~/.authinfo.gpg")))
;; SMTP outgoing email
(setq message-send-mail-function 'smtpmail-send-it
smtpmail-stream-type 'starttls
smtpmail-default-smtp-server "smtp.fastmail.com"
smtpmail-smtp-server "smtp.fastmail.com"
smtpmail-smtp-service 587)
(require 'epa-file)
;; Let Emacs handle passphrase queries through the minibuffer:
(setf epa-pinentry-mode 'loopback)
;;(custom-set-variables '(epg-gpg-program "/opt/homebrew/bin/gpg"))
$ mu mkdir ~/.Mail/queue $ touch ~/.Mail/queue/.noindex
(setq smtpmail-queue-mail nil ;t ;; start in queuing mode
smtpmail-queue-dir "~/.Mail/queue/cur")
(setq message-directory "~/.Mail/")
Notmuch maintains a database of indexed mail only. The maildir has to be fetched by e.g. offlineimap
first; my notmuch new
’s pre-new hook executes an offlineimap fetch.
The notmuch configuration files are:
- .notmuch-config
- pre-new hook
- post-new hook
- feed.db which contains lines of
notmuch search
queries to identify newsletters
- feed.db which contains lines of
(add-to-list 'load-path "/opt/homebrew/share/emacs/site-lisp/notmuch")
(require 'notmuch)
(global-set-key (kbd "<f10>") #'notmuch)
(global-set-key (kbd "C-x m") #'notmuch-mua-new-mail)
;; After refreshing a buffer with `g', the selection is not where it seems. hl-line is off
(defun ct/notmuch--reset-selection ()
"Refresh line highlight when refreshing search buffers"
(hl-line-mode -1)
(forward-char +1)
(hl-line-mode +1)
(backward-char -1))
(advice-add #'notmuch-search-refresh-view :after #'ct/notmuch--reset-selection)
Eagerly load to further separate configurations into more org babel code blocks:
(require 'notmuch)
;; Buffer local nowadays
(setq-default notmuch-search-oldest-first nil)
(setq notmuch-draft-folder "Drafts"
notmuch-fcc-dirs '(("me@christiantietze.de" . "Sent"))
notmuch-show-all-tags-list t)
;; Always reply from my main address
(defadvice notmuch-mua-reply (around notmuch-fix-sender)
(let ((sender ct/user-sender))
ad-do-it))
(ad-activate 'notmuch-mua-reply)
;; Directories to access the sender databases:
(setq
notmuch-mail-dir "~/.Mail/me@christiantietze.de-fastmail/"
notmuch-hooks-dir (expand-file-name ".notmuch/hooks" notmuch-mail-dir))
(defun ct/notmuch-set-wrap-prefix ()
(setq-local wrap-prefix " "))
(add-hook 'notmuch-search-hook #'ct/notmuch-set-wrap-prefix)
(add-hook 'notmuch-tree-hook #'ct/notmuch-set-wrap-prefix)
- In message view, hit
<e>
fornotmuch-show-resume-message
to continue a draft.
(defvar ct/notmuch-trashing-tags '("+trashing" "-inbox" "-new" "-feed")
"List of tag changes to apply to a amessage or a thread when it is trashed.")
(setq notmuch-archive-tags '("-inbox" "-unread" "+archived")
notmuch-message-replied-tags '("+replied" "-inbox")
notmuch-show-mark-read-tags '("-inbox" "-unread" "+touched"))
(setq notmuch-tagging-keys
'(("a"
notmuch-archive-tags
"Archive")
("u"
notmuch-show-mark-read-tags
"Mark read")
("f"
("+flagged")
"Flag")
("s"
("+spam" "-inbox")
"Mark as spam")
("d"
ct/notmuch-trashing-tags
"Trash")
("i"
("+killing")
"Kill (delete)")))
;; Bindings in `notmuch-show-mode' (single message)
(define-key notmuch-show-mode-map (kbd "r")
'notmuch-show-reply)
(define-key notmuch-show-mode-map (kbd "R")
'notmuch-show-reply-sender)
(define-key notmuch-show-mode-map (kbd "TAB")
'notmuch-show-toggle-message)
(define-key notmuch-show-mode-map (kbd "d")
(lambda ()
"Toggle single message to be trashed"
(interactive)
(if (member "trashing" (notmuch-show-get-tags))
(notmuch-show-tag (list "-trashing"))
(notmuch-show-tag ct/notmuch-trashing-tags))))
;; Bindings in `notmuch-search-mode' (search results/tagged messages list)
(define-key notmuch-search-mode-map (kbd "r")
'notmuch-search-reply-to-thread)
(define-key notmuch-search-mode-map (kbd "F")
'vedang/notmuch-move-sender-to-feed)
(define-key notmuch-search-mode-map (kbd "R")
'notmuch-search-reply-to-thread-sender)
(define-key notmuch-search-mode-map (kbd "d")
(lambda ()
"Mark single message to be trashed"
(interactive)
(if (member "trashing" (notmuch-tree-get-tags))
(notmuch-search-tag '("-trashing"))
(notmuch-search-tag ct/notmuch-trashing-tags))))
;; Bindings in `notmuch-tree-mode' (tree view of the current results)
(define-key notmuch-tree-mode-map (kbd "d")
(lambda ()
"Toggle whole thread to be trashed (or untrash thread if current message is to be trashed)"
(interactive)
(if (member "trashing" (notmuch-tree-get-tags))
(notmuch-tree-tag-thread '("-trashing"))
(notmuch-tree-tag-thread ct/notmuch-trashing-tags))))
Need a macro for this to execute the body block lazily. Copied from save-window-excursion
.
(defmacro ct/with-notmuch-as-compose-mail (&rest body)
"Overrides `compose-mail' with `notmuch-mua-mail' for the duration of BODY."
`(progn
(require 'notmuch-mua)
(advice-add 'compose-mail :override #'notmuch-mua-mail)
(unwind-protect (progn ,@body)
(advice-remove 'compose-mail #'notmuch-mua-mail))))
(setq notmuch-hello-thousands-separator ".")
(setq notmuch-saved-searches
'((:key "i"
:name "Unscreened"
:query "tag:inbox")
(:name "Timing inbox"
:query "tag:timing not (tag:trashed or tag:trashing or tag:sent or tag:archived)")
(:name "Mind Tap inbox"
:query "tag:mindtap not (tag:trashed or tag:trashing or tag:sent or tag:archived)")
(:name "Starface inbox"
:query "tag:starface not (tag:trashed or tag;trashing or tag:sent or tag:archived)")
(:name "The Archive inbox"
:query "tag:thearchiveapp not (tag:trashed or tag:trashing or tag:sent or tag:archived)")
(:name "TableFlip inbox"
:query "tag:tableflipapp not (tag:trashed or tag:trashing or tag:sent or tag:archived)")
(:name "WordCounter inbox"
:query "tag:wordcounterapp not (tag:trashed or tag:trashing or tag:sent or tag:archived)")
(:name "Open Source inbox"
:query "tag:opensource not (tag:trashed or tag:trashing or tag:sent or tag:archived)")
(:key "F"
:name "flagged"
:query "tag:flagged and not (tag:trashed or tag:trashing or tag:sent)"
:search-type 'tree)
(:key "f"
:name "feed"
:query "tag:feed and not (tag:trashed or tag:trashing)"
:search-type 'tree)
(:key "p"
:name "papertrail"
:query "tag:papertrail and not (tag:trashed or tag:trashing)")
(:key "S"
:name "@Sascha"
:query "from:/sascha.?fast@/ and not (tag:trashed or tag:trashing)"
:search-type tree)
(:key "u"
:name "unread"
:query "tag:unread" )
(:key "d"
:name "drafts"
:query "tag:draft")
(:key "t"
:name "sent"
:query "tag:sent")
(:key "a"
:name "all mail"
:query "*")))
The “Hey!” workflow implementation comes from this Gist by @vedang: https://gist.github.com/vedang/26a94c459c46e45bc3a9ec935457c80f
;; Helper functions
(defun vedang/notmuch-search-get-from ()
"A helper function to find the email address for the given email. Assumes `notmuch-search-mode.`"
(let ((notmuch-addr-sexp (car ; was `first` in the original
(notmuch-call-notmuch-sexp "address"
"--format=sexp"
"--format-version=1"
"--output=sender"
(notmuch-search-find-thread-id)))))
(plist-get notmuch-addr-sexp :name-addr)))
(defun vedang/notmuch-tree-get-from ()
"A helper function to find the email address for the given email. Assumes `notmuch-tree-mode'."
(plist-get (notmuch-tree-get-prop :headers) :From))
(defun vedang/notmuch-get-from ()
"Find the From email address for the email at point."
(car (notmuch-clean-address (cond
((eq major-mode 'notmuch-show-mode)
(notmuch-show-get-from))
((eq major-mode 'notmuch-tree-mode)
(vedang/notmuch-tree-get-from))
((eq major-mode 'notmuch-search-mode)
(vedang/notmuch-search-get-from))))))
(defun vedang/notmuch-search-by-from (&optional no-display)
"Show all emails sent from the sender of the current thread.
NO-DISPLAY is sent forward to `notmuch-search'."
(interactive)
(notmuch-search (concat "from:" (vedang/notmuch-get-from))
notmuch-search-oldest-first
nil
nil
no-display))
(defun vedang/notmuch-tag-by-from (tag-changes &optional beg end refresh)
"Apply TAG-CHANGES to all emails from the sender of the current thread.
BEG and END provide the region, but are ignored. They are defined
since `notmuch-search-interactive-tag-changes' returns them. If
REFRESH is true, refresh the buffer from which we started the
search."
(interactive (notmuch-search-interactive-tag-changes))
(let ((this-buf (current-buffer)))
(vedang/notmuch-search-by-from t)
;; This is a dirty hack since I can't find a way to run a
;; temporary hook on `notmuch-search' completion. So instead of
;; waiting on the search to complete in the background and then
;; making tag-changes on it, I will just sleep for a short amount
;; of time. This is generally good enough and works, but is not
;; guaranteed to work every time. I'm fine with this.
(sleep-for 0.5)
(notmuch-search-tag-all tag-changes)
(when refresh
(set-buffer this-buf)
(notmuch-refresh-this-buffer))))
(defun vedang/notmuch-add-addr-to-db (nmaddr nmdbfile)
"Add the email address NMADDR to the db-file NMDBFILE with a `from:` prefix."
(append-to-file (format "+feed -- from:%s\n" nmaddr) nil nmdbfile))
(defun vedang/notmuch-move-sender-to-feed ()
"For the email at point, move the sender of that email to the feed.
This means:
1. All new email should go to the feed and skip the inbox altogether.
2. All existing email should be updated with the tag =feed=.
3. All existing email should be removed from the inbox."
(interactive)
(let ((sender (vedang/notmuch-get-from)))
(vedang/notmuch-add-addr-to-db sender
(format "%s/feed.db" notmuch-hooks-dir))
(vedang/notmuch-tag-by-from '("+feed" "+archived" "-inbox"))
(message (concat "Added " sender " to feed.db"))))
Add a capture template:
(with-eval-after-load 'notmuch
(with-eval-after-load 'org-capture
(push '("r" "Respond to email"
entry (file (ct/file-join ct/org-gtd-directory "tickler.org"))
"* TODO Respond to %:from on %:subject :email: \nSCHEDULED: %t\n%U\n%a\n"
:clock-in t
:clock-resume t
:immediate-finish t)
org-capture-templates)))
Programmatically invoke the capture template. Use the same key mnemonic!
(defun ct/notmuch-reply-later ()
"Capture this email for replying later."
(interactive)
(org-capture nil "r")
;; The rest of this function is just a nice message in the modeline.
(let* ((email-subject (format "%s..."
(substring (notmuch-show-get-subject) 0 15)))
(email-from (format "%s..."
(substring (notmuch-show-get-from) 0 15)))
(email-string (format "%s (From: %s)" email-subject email-from)))
(message "Noted! Reply Later: %s" email-string)))
(define-key notmuch-show-mode-map (kbd "C") #'ct/notmuch-reply-later)
(use-package erc
:config
(setq erc-user-full-name "Christian Tietze"
erc-nick "ctietze"
erc-kill-buffer-on-part t
erc-track-shorten-start 8)
(defun ct/erc-connect-to-sourcehut ()
(interactive)
(erc-tls :server "chat.sr.ht"
:port 6697
:nick "ctietze"
:password (ct/get-keychain-password "emacs-circe-sourcehut"))))
(use-package circe
:config
(setq circe-network-options
`(("chat.sr.ht"
:tls t
:port 6697
:nick "ctietze"
:sasl-username "ctietze/irc.libera.chat"
:sasl-password ,(ct/get-keychain-password "emacs-circe-sourcehut")
:channels ("#soju")))))
Limit IRC buffer width to readable center column:
(with-eval-after-load 'circe
(with-eval-after-load 'olivetti
(defun ct/circe-olivetti-mode ()
"Set up olivetti for IRC buffers."
(setq-local olivetti-body-width 82)
(olivetti-mode))
(add-hook 'circe-channel-mode-hook #'ct/circe-olivetti-mode)
(add-hook 'circe-server-mode-hook #'ct/circe-olivetti-mode)))
Slow down Company completions a bit so that I can type in the chat without every other word being completed into a user name or IRC command:
(with-eval-after-load 'circe
(with-eval-after-load 'company
(defun ct/slow-down-company ()
(setq-local company-idle-delay 0.8))
(add-hook 'crce-channel-mode-hook #'ct/slow-down-company)))
(use-package mastodon
:ensure
:defer
:bind
(:map mastodon-mode-map
("g" . mastodon-tl--update)
;; see org-capture-templates addition
;; ("o" . (lambda () (interactive) (org-capture nil "m")))
("@" . my-mastodon-mentions))
:commands (mastodon-http--api mastodon-http--post mastodon-mode mastodon-http--get-search-json)
:config
(setq mastodon-instance-url "https://mastodon.social"
mastodon-active-user "ctietze"))
(with-eval-after-load 'mastodon
(with-eval-after-load 'discover
(require 'mastodon-discover)
(mastodon-discover)))
(use-package elpher
:defer t
:config
(defun elpher:eww-browse-url (original url &optional new-window)
"Handle gemini links."
(cond ((string-match-p "\\`\\(gemini\\|gopher\\)://" url)
(require 'elpher)
(elpher-go url))
(t (funcall original url new-window))))
(advice-add 'eww-browse-url :around 'elpher:eww-browse-url))
(use-package xwidget
:defer t
:if (memq window-system '(mac ns x))
:bind
(:map xwidget-webkit-mode-map
("<swipe-right>" . xwidget-webkit-forward)
("<swipe-left>" . xwidget-webkit-back)))
(defun display-current-time ()
(interactive)
(message (format-time-string "%Y-%m-%d %H:%M:%S")))
(use-package ledger-mode)
(use-package company-ledger
:after (ledger-mode company))
(use-package emacs
:after (modus-themes org)
:init
(delete-other-windows) ;; Ensure only 1 window is used
(add-hook 'after-init-hook (lambda () (org-agenda nil "d"))))
(setq confirm-kill-emacs 'y-or-n-p)
For my website projects at ~/Sites/project.com
I sometimes need to copuy the project-relative path to assemble relative links.
(defun ct/next-subdir (dir)
"Returns what's left after trying to trim until the next \"/\".
Will work with leading \"/\", too:
(ct/next-subdir (ct/next-subdir \"/var/tmp/foo\")) will return \"foo\"."
(let ((pos (string-match "/" dir))
(next (string-match "/" dir 1)))
(if (eq 0 pos)
(if next
(substring dir next)
dir)
(if pos
(substring dir next)
dir))))
(defun ct/sites-relative-path ()
(let* ((sites-dir (expand-file-name "~/Sites/"))
(site (ct/next-subdir (substring buffer-file-name (string-match "/" buffer-file-name (length sites-dir)))))
(path (concat (file-name-sans-extension site) "/")))
path))
(defun ct/copy-site-path ()
(interactive)
(let ((path (ct/sites-relative-path)))
(kill-new path)
(message "Added \"%s\" to kill ring" path)))
(with-eval-after-load 'project
(defun ct/project-create-post (title filename)
"Add a new post called TITLE in the current project's root directory,
or `default-directory' if no project is recognized."
(interactive
(if-let* ((title (read-string "New post's title: "))
(filename-suggestion (concat (ct/slug title) ".md"))
(filename (read-string "Filename: " filename-suggestion)))
(list title filename)))
(require 'project)
(ct/create-post title filename
(project-root (project-current t)))))
(defun ct/create-post (title filename &optional directory)
"Visit post called TITLE at FILENAME inside DIRECTORY.
A blog-appropriate subdirectory inside DIRECTORY is used -- if DIRECTORY is nil,
falls back to `default-directory'."
(unless directory
(setq directory default-directory))
(let* ((subdir (concat "content/posts/" (format-time-string "%Y/%m") "/"))
(default-directory (concat directory subdir))
(path (concat default-directory filename)))
(find-file path)
(kill-new title)
(message "Added %s to kill ring; TODO add to title template!")))
Adapted from org-roam, changing the _
to -
in slugs:
(defun ct/slug (title)
"Return the slug of TITLE."
(let ((slug-trim-chars '(;; Combining Diacritical Marks https://www.unicode.org/charts/PDF/U0300.pdf
768 ; U+0300 COMBINING GRAVE ACCENT
769 ; U+0301 COMBINING ACUTE ACCENT
770 ; U+0302 COMBINING CIRCUMFLEX ACCENT
771 ; U+0303 COMBINING TILDE
772 ; U+0304 COMBINING MACRON
774 ; U+0306 COMBINING BREVE
775 ; U+0307 COMBINING DOT ABOVE
776 ; U+0308 COMBINING DIAERESIS
777 ; U+0309 COMBINING HOOK ABOVE
778 ; U+030A COMBINING RING ABOVE
779 ; U+030B COMBINING DOUBLE ACUTE ACCENT
780 ; U+030C COMBINING CARON
795 ; U+031B COMBINING HORN
803 ; U+0323 COMBINING DOT BELOW
804 ; U+0324 COMBINING DIAERESIS BELOW
805 ; U+0325 COMBINING RING BELOW
807 ; U+0327 COMBINING CEDILLA
813 ; U+032D COMBINING CIRCUMFLEX ACCENT BELOW
814 ; U+032E COMBINING BREVE BELOW
816 ; U+0330 COMBINING TILDE BELOW
817 ; U+0331 COMBINING MACRON BELOW
)))
(cl-flet* ((nonspacing-mark-p (char) (memq char slug-trim-chars))
(strip-nonspacing-marks (s) (ucs-normalize-NFC-string
(apply #'string
(seq-remove #'nonspacing-mark-p
(ucs-normalize-NFD-string s)))))
(cl-replace (title pair) (replace-regexp-in-string (car pair) (cdr pair) title)))
(let* ((pairs `(("[^[:alnum:][:digit:]]" . "-") ;; convert anything not alphanumeric
("__*" . "_") ;; remove sequential underscores
("--*" . "-") ;; remove sequential dashes
("^-+" . "") ;; remove starting dashes
("-+$" . ""))) ;; remove ending dashes
(slug (-reduce-from #'cl-replace (strip-nonspacing-marks title) pairs)))
(downcase slug)))))
(defun ct/ct-insert-header (title)
(interactive "sTitle: ")
(insert
(format template
title
(format-time-string "%Y-%m-%d %H:%M:%S +0100"))))
(defun ct/zk-insert-header (title author)
(interactive
(list (read-string "Title: ")
(completing-read "Author: " '("christian" "sascha") nil t)))
(insert
(format template
title
(format-time-string "%Y-%m-%d %H:%M:%S +0100")
author)))
- Consider using a proper YAML frontmatter parser with libyaml bindings: https://github.com/syohex/emacs-libyaml
- Consider using Elisp YAML parser: https://github.com/zkry/yaml.el – still need to extract the frontmatter, though
(defun ct/yaml/update-post ()
"Insert or update an `updated_at' in the YAML header with current ISO time."
(interactive)
(save-excursion
(goto-char (point-min))
;; Find the YAML frontmatter block boundaries, ignoring (optional) beginning of the block.
(when (looking-at-p "^---") (forward-line +1))
(let ((target-pos (point))
(timestamp (format-time-string "%Y-%m-%d %H:%M:%S +0100"))
(yaml-end-pos (search-forward-regexp "^---" nil t)))
;; Return after search-forward moved point.
(goto-char target-pos)
(if (not yaml-end-pos)
;; No YAML block, yet; insert a new one
(insert (format "updated_at: %s\n---\n" timestamp))
;; If YAML block is found, update or insert the date entry
(progn
(if (search-forward-regexp "^updated_at:" yaml-end-pos t)
(delete-region (line-beginning-position) (line-end-position))
(progn
(goto-char yaml-end-pos)
(beginning-of-line)
(newline)
(backward-char)))
(insert (format "updated_at: %s" timestamp)))))))
(use-package emacs
:config
(defun ct/all-tag-lines ()
"Extract the array contents of YAML lines for `tags: [...]'."
(let ((project-dir (cdr (project-current)))
(regex "^tags:\\s\\[\\s*([a-zA-Z0-9 -_]+)\\s*\\]"))
(shell-command-to-string
;; (concat "ag -t -C0 --nobreak --nomultiline --nonumbers --noheading --nofilename -- " (shell-quote-argument regex) " " project-dir)
(concat "rg -C0 --no-filename --no-heading --replace \"\\$1\" -- " (shell-quote-argument regex) " " project-dir))))
(defun ct/all-tags ()
"Return a list of unique tags across all articles."
(delete-dups
(flatten-tree
(mapcar (lambda (s) (split-string s "[, ]+" t " "))
(split-string (ct/all-tag-lines) "\n" nil " ")))))
(defun ct/insert-project-tag ()
"Select and insert a tag from YAML frontmatter tags in the project."
(interactive)
(insert (completing-read "Tag: " (ct/all-tags))))
(define-key project-prefix-map (kbd "t") #'ct/insert-project-tag))
(use-package emacs
:config
(fset 'ct/setup-tabs
(kmacro-lambda-form [home ? ?t ?r ?M ?a ?i ?l return f10 ? ?t ?n ?\C-c ?a ?d ? ?t ?n ? ?f ?t ?i ?m ?i ?n ?g return ? ?t ?n] 0 "%d")))
(dir-locals-set-class-variables 'schreibprojekt-dir-settings
'((markdown-mode . ((eval . (ct/writeroom-mode 1))
(eval . (display-line-numbers-mode -1))
(eval . (hl-todo-mode 1))
;; Languagetool integration
(eval . (flymake-mode))))))
(dir-locals-set-directory-class "/Users/ctm/Dropbox/Shared/Sascha/Online Course" 'schreibprojekt-dir-settings)
(dir-locals-set-directory-class "/Users/ctm/Dropbox/Shared/Sascha/Gegenlesen" 'schreibprojekt-dir-settings)
(dir-locals-set-directory-class "/Users/ctm/Sites/christiantietze.de/content" 'schreibprojekt-dir-settings)