This is (will be) my Emacs literate configuration file. A self contained file with all my configuration is useful for documentation purposes. It will be modeled using the technique described by Protesilaos for his own Emacs config file: . This method consists in generating all files /a priori/, after modifying this file, and *not* at load time, as that would be too slow. #+begin_src emacs-lisp :tangle no :results none (org-babel-tangle) #+end_src * Overview of files and directories - =early-init.el=: quoting the [[https://www.gnu.org/software/emacs/manual/html_node/emacs/Early-Init-File.html][Emacs documentation]], this file is "loaded before the package system and GUI is initialized, so in it you can customize variables that affect the package initialization process" - =init.el=: the skeleton of my configuration framework. It will load the rest of the modules. - =rul-emacs-modules/=: a directory with Emacs modules specific to my configuration. Modules group code related to a topic or theme of configuration. For example, =rul-prog.el= contains code related to programming, and =rul-org.el= contains code related to org-mode. If a module gets too big, I can create a smaller module under the same topic; for example, =rul-org-agenda.el=. - =rul-post-init.el=: this file will be loaded after =init.el=, and will normally live in other git repository. Here I normally add overrides needed in my work computer. - =rul-emacs.org=: this file. It (will) generate the rest of the structure. * Early configuration file (=early-init.el=) ** Graphical aspects Customization of graphical aspects of Emacs, such as size, panels, etc. #+begin_src emacs-lisp :tangle "early-init.el" ;; I don't use any of these (menu-bar-mode -1) (tool-bar-mode -1) (scroll-bar-mode -1) ;; Avoid initial flash of light. ;; Inspired on prot-emacs-avoid-initial-flash-of-light. (setq mode-line-format nil) (set-face-attribute 'default nil :background "#000000" :foreground "#ffffff") (set-face-attribute 'mode-line nil :background "#000000" :foreground "#ffffff" :box 'unspecified) #+end_src ** Frame configuration I like to keep a few frames open all the time. A main frame, where I open my org files, code, etc. A frame for communication and reading, such as email and feeds, and a frame for terminals. Currently, the frames are all the same, but I will add configuration to distinguish them so I can automate their placement in my desktop environment. #+begin_src emacs-lisp :tangle "early-init.el" ;; Do not resize when font size changes (setq frame-resize-pixelwise t) ;; By default, start maximized (add-to-list 'default-frame-alist '(fullscreen . maximized)) ;; No need for titlebar (modify-frame-parameters nil '((undecorated . t))) ;; Name frames to ease switching between them (add-hook 'after-init-hook (lambda () (set-frame-name "main"))) #+end_src ** Miscellany #+begin_src emacs-lisp :tangle "early-init.el" ;; Initialise installed packages, otherwise, basic functions are not ;; available during the initialization stage. (setq package-enable-at-startup t) ;; Do not report warnings. It's too noisy. (setq native-comp-async-report-warnings-errors 'silent) ;; Keep things minimal (setq inhibit-startup-screen t) (setq inhibit-startup-echo-area-message user-login-name) #+end_src * Main configuration file (=init.el=) ** Package matters I use package from both stable and bleeding-edge Melpa. #+begin_src emacs-lisp :tangle "init.el" ;; package.el (require 'package) (add-to-list 'package-archives '("melpa-stable" . "https://stable.melpa.org/packages/") t) (add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t) #+end_src ** Backups Emacs tends to clutter the filesystem with backup files. A backup file is normally the filename with a =~= suffix. I rather have my filesystem clean, and centralize all backups in a single directory. #+begin_src emacs-lisp :tangle "init.el" (if (file-directory-p "~/.backup") (setq backup-directory-alist '(("." . "~/.backup"))) (message "Directory does not exist: ~/.backup")) (setq backup-by-copying t ; Don't delink hardlinks delete-old-versions t ; Clean up the backups kept-new-versions 3 ; keep some new versions kept-old-versions 2 ; and some old ones, too version-control t) ; Use version numbers on backups #+end_src ** Customizations Customizations don't place nicely with version control, so I do them in a random file that won't get persisted. Configurations that need persisting will be added to =custom-set-variables= and =custom-set-faces=. #+begin_src emacs-lisp :tangle "init.el" ;; Do not persist customizations (setq custom-file (make-temp-file "emacs-custom-")) #+end_src ** Editor interface General configurations related to text editing across all modes. #+begin_src emacs-lisp :tangle "init.el" (setq fill-column 79) ; Wrap lines (setq mouse-yank-at-point t) ; Do not follow mouse curors when mouse-yanking (setq-default indent-tabs-mode nil) ; No tabs when indenting (setq-default tab-width 4) ; How many spaces a tab represents (setq initial-scratch-message "") (defalias 'yes-or-no-p 'y-or-n-p) ;; Only flash the mode line (setq ring-bell-function (lambda () (let ((orig-fg (face-foreground 'mode-line))) (set-face-foreground 'mode-line "#F2804F") (run-with-idle-timer 0.1 nil (lambda (fg) (set-face-foreground 'mode-line fg)) orig-fg)))) ;; Highlight parens (setq show-paren-delay 0) (show-paren-mode 1) (savehist-mode 1) ; Save histories, including minibuffer (save-place-mode 1) ; Remember and restore cursor information (setq auto-save-no-message t) ; Do not print a message when auto-saving (pixel-scroll-precision-mode 1) ; Precision scrolling #+end_src ** Emacs server I used to run Emacs as a systemd daemon, but it was not too deterministic as sometimes it would break. https://rbenencia.name/blog/emacs-daemon-as-a-systemd-service/ Now, I simply start it from Emacs itself. This approach works well for me. #+begin_src emacs-lisp :tangle "init.el" ;; Server (require 'server) (setq server-client-instructions nil) ; Keep it quiet when opening an ec (unless (server-running-p) (server-start)) #+end_src ** Modules machinery #+begin_src emacs-lisp :tangle "init.el" (dolist (path '("~/.emacs.d/rul-lisp/packages")) (add-to-list 'load-path path)) (when-let* ((file (locate-user-emacs-file "rul-pre-init.el")) ((file-exists-p file))) (load-file file)) (require 'rul-themes) (require 'rul-bindings) (require 'rul-completion) (require 'rul-fm) (require 'rul-fonts) (require 'rul-io) (require 'rul-mail) (require 'rul-modeline) (require 'rul-org) (require 'rul-prog) (require 'rul-terminals) (require 'rul-vc) (require 'rul-wm) (require 'rul-write) (when-let* ((file (locate-user-emacs-file "rul-post-init.el")) ((file-exists-p file))) (load-file file)) ;; init.el ends here #+end_src * Modules I group my configuration in logical modules. In general, a module contains configuration for more than one package. ** The =themes= module The =themes= module contains code pertaining to Emacs themes. #+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-themes.el" (use-package ef-themes :ensure t) (use-package modus-themes :ensure t :config (setq modus-themes-mode-line '(accented borderless padded) modus-themes-region '(bg-only) modus-themes-bold-constructs t modus-themes-italic-constructs t modus-themes-paren-match '(bold intense) modus-themes-headings (quote ((1 . (rainbow variable-pitch 1.3)) (2 . (rainbow 1.1)) (t . (rainbow)))) modus-themes-org-blocks 'tinted)) #+end_src Additionally, this module subscribes to =org.freedesktop.appearance color-theme= to detect what color theme is preferred, and set our Emacs theme accordingly. #+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-themes.el" (use-package dbus) (defun mf/set-theme-from-dbus-value (value) "Set the appropiate theme according to the color-scheme setting value." (message "value is %s" value) (if (equal value '1) (progn (message "Switch to dark theme") (modus-themes-select 'modus-vivendi)) (progn (message "Switch to light theme") (modus-themes-select 'modus-operandi)))) (defun mf/color-scheme-changed (path var value) "DBus handler to detect when the color-scheme has changed." (when (and (string-equal path "org.freedesktop.appearance") (string-equal var "color-scheme")) (mf/set-theme-from-dbus-value (car value)) )) ;; Register for future changes (dbus-register-signal :session "org.freedesktop.portal.Desktop" "/org/freedesktop/portal/desktop" "org.freedesktop.portal.Settings" "SettingChanged" #'mf/color-scheme-changed) ;; Request the current color-scheme (dbus-call-method-asynchronously :session "org.freedesktop.portal.Desktop" "/org/freedesktop/portal/desktop" "org.freedesktop.portal.Settings" "Read" (lambda (value) (mf/set-theme-from-dbus-value (caar value))) "org.freedesktop.appearance" "color-scheme" ) (provide 'rul-themes) #+end_src ** The =bindings= module This module contains code pertaining to keybindings. It starts by defining a set global keys. #+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-bindings.el" ;; Global keybindings (global-set-key (kbd "C-c R") 'revert-buffer) (global-set-key (kbd "C-c w") 'whitespace-cleanup) (defun help/insert-em-dash () "Inserts an EM-DASH (not a HYPEN, not an N-DASH)" (interactive) (insert "—")) (global-set-key (kbd "C--") #'help/insert-em-dash) #+end_src Next, we define a few /hydras/. /Hydras/ are a way of grouping keybindings together, offering a menu on the way. #+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-bindings.el" (use-package hydra :ensure t :defer 1) ;; tab-bar (defhydra hydra-tab-bar (:color amaranth) "Tab Bar Operations" ("t" tab-new "Create a new tab" :column "Creation" :exit t) ("d" dired-other-tab "Open Dired in another tab") ("f" find-file-other-tab "Find file in another tab") ("x" tab-close "Close current tab") ("m" tab-move "Move current tab" :column "Management") ("r" tab-rename "Rename Tab") ("" tab-bar-select-tab-by-name "Select tab by name" :column "Navigation") ("l" tab-next "Next Tab") ("j" tab-previous "Previous Tab") ("q" nil "Exit" :exit t)) (global-set-key (kbd "C-x t") 'hydra-tab-bar/body) ;; Zoom (defhydra hydra-zoom () "zoom" ("g" text-scale-increase "in") ("l" text-scale-decrease "out")) (global-set-key (kbd "C-c z") 'hydra-zoom/body) ;; Go (defhydra hydra-go () "zoom" ("=" gofmt :exit t) ("c" go-coverage :exit t)) ;; vterm (defhydra hydra-vterm () "zoom" ("t" multi-vterm "Open a terminal" :exit t) ("d" multi-vterm-dedicated-open "Dedicated" :exit t) ("p" multi-vterm-prev "Previous terminal") ("n" multi-vterm-next "Next terminal") ("r" multi-vterm-rename-buffer "Rename buffer" :exit t) ) (global-set-key (kbd "C-c t") 'hydra-vterm/body) (global-set-key (kbd "C-c m") 'hydra-go/body) #+end_src Finally, we make use of =which-key=, which will show a menu with all keybinding options after a prefix is pressed. I think this package has the potential to obsolete =hydra=, so I'll have to revisit that code. #+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-bindings.el" (use-package which-key :ensure t :config (which-key-mode)) (provide 'rul-bindings) #+end_src ** The =completions= module This module contains code pertaining to completion and the minibuffer. #+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-completion.el" (use-package orderless :ensure t) (setq completion-styles '(basic substring initials orderless)) (setq completion-category-overrides '( (file (styles . (basic partial-completion orderless))) (project-file (styles . (flex basic substring partial-completion orderless))) )) (setq completion-ignore-case t) #+end_src The =vertico= package provides a vertical completion UI based on the default completion system. #+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-completion.el" ;; Enable vertico (use-package vertico :ensure t :init (vertico-mode) :config (add-hook 'rfn-eshadow-update-overlay-hook #'vertico-directory-tidy)) #+end_src The =marginalia= package annotates the completion candidates with useful contextual information. #+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-completion.el" ;; Enable rich annotations using the Marginalia package (use-package marginalia :ensure t :bind (:map minibuffer-local-map ("M-A" . marginalia-cycle)) :init (marginalia-mode)) #+end_src The =consult= package replaces most of Emacs core functions with completion-friendly alternatives that integrates well with =vertico= and =marginalia=. #+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-completion.el" (use-package consult :ensure t :bind (;; C-c bindings in `mode-specific-map' ("C-c M-x" . consult-mode-command) ("C-c h" . consult-history) ("C-c k" . consult-kmacro) ("C-c m" . consult-man) ("C-c i" . consult-info) ([remap Info-search] . consult-info) ;; C-x bindings in `ctl-x-map' ("C-x M-:" . consult-complex-command) ;; orig. repeat-complex-command ("C-x b" . consult-buffer) ;; orig. switch-to-buffer ("C-x 4 b" . consult-buffer-other-window) ;; orig. switch-to-buffer-other-window ("C-x 5 b" . consult-buffer-other-frame) ;; orig. switch-to-buffer-other-frame ("C-x r b" . consult-bookmark) ;; orig. bookmark-jump ("C-x p b" . consult-project-buffer) ;; orig. project-switch-to-buffer ;; Custom M-# bindings for fast register access ("M-#" . consult-register-load) ("M-'" . consult-register-store) ;; orig. abbrev-prefix-mark (unrelated) ("C-M-#" . consult-register) ;; Other custom bindings ("M-y" . consult-yank-pop) ;; orig. yank-pop ;; M-g bindings in `goto-map' ("M-g e" . consult-compile-error) ("M-g f" . consult-flymake) ;; Alternative: consult-flycheck ("M-g g" . consult-goto-line) ;; orig. goto-line ("M-g M-g" . consult-goto-line) ;; orig. goto-line ("M-g o" . consult-outline) ;; Alternative: consult-org-heading ("M-g m" . consult-mark) ("M-g k" . consult-global-mark) ("M-g i" . consult-imenu) ("M-g I" . consult-imenu-multi) ;; M-s bindings in `search-map' ("M-s d" . consult-find) ("M-s D" . consult-locate) ("M-s g" . consult-grep) ("M-s G" . consult-git-grep) ("M-s r" . consult-ripgrep) ("M-s l" . consult-line) ("M-s L" . consult-line-multi) ("M-s k" . consult-keep-lines) ("M-s u" . consult-focus-lines) ;; Isearch integration ("M-s e" . consult-isearch-history) :map isearch-mode-map ("M-e" . consult-isearch-history) ;; orig. isearch-edit-string ("M-s e" . consult-isearch-history) ;; orig. isearch-edit-string ("M-s l" . consult-line) ;; needed by consult-line to detect isearch ("M-s L" . consult-line-multi) ;; needed by consult-line to detect isearch ;; Minibuffer history :map minibuffer-local-map ("M-s" . consult-history) ;; orig. next-matching-history-element ("M-r" . consult-history)) ;; orig. previous-matching-history-element :init (setq xref-show-xrefs-function #'consult-xref) (setq xref-show-definitions-function #'consult-xref) (add-hook 'completion-list-mode-hook #'consult-preview-at-point-mode) :config (setq consult-preview-key 'any) (setq consult-narrow-key "<") ) #+end_src The next piece of code corresponds to =embark=, a package that enables context-specific actions in the minibuffer, or common buffers. #+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-completion.el" (use-package embark :ensure t :bind (("C-." . embark-act) ;; pick some comfortable binding ("M-." . embark-dwim) ;; good alternative: M-. ("C-h B" . embark-bindings)) ;; alternative for `describe-bindings' :init (setq prefix-help-command #'embark-prefix-help-command) :config ;; Hide the mode line of the Embark live/completions buffers (add-to-list 'display-buffer-alist '("\\`\\*Embark Collect \\(Live\\|Completions\\)\\*" nil (window-parameters (mode-line-format . none))))) (use-package embark-consult :ensure t :hook (embark-collect-mode . consult-preview-at-point-mode)) (provide 'rul-completion) #+end_src ** The =fm= module The =fm= module contains code pertaining to file management. In particular, it's the module that configures =dired= and adds a few extra packages. #+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-fm.el" ;;; rul-fm.el --- File management ;; dired (add-hook 'dired-mode-hook #'dired-hide-details-mode) (setq dired-guess-shell-alist-user '(("\\.\\(png\\|jpe?g\\|tiff\\)" "feh" "xdg-open") ("\\.\\(mp[34]\\|m4a\\|ogg\\|flac\\|webm\\|mkv\\)" "mpv" "xdg-open") (".*" "xdg-open"))) (setq dired-kill-when-opening-new-dired-buffer t) (put 'dired-find-alternate-file 'disabled nil) ;;; Icons (use-package nerd-icons :ensure t ) (use-package nerd-icons-dired :ensure t :config (add-hook 'dired-mode-hook #'nerd-icons-dired-mode)) (provide 'rul-fm) #+end_src ** The =fonts= module The =fonts= module contains code pertaining to fonts. In particular, it installs =fontaine=, a software that allows defining font presets. #+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-fonts.el" ;;; rul-fonts.el --- Fonts configuration (use-package fontaine :ensure t :config (setq fontaine-presets '((tiny :default-height 100) (small :default-height 120) (medium :default-height 140) (large :default-weight semilight :default-height 180 :bold-weight extrabold) (presentation :default-weight semilight :default-height 200 :bold-weight extrabold) (jumbo :default-weight semilight :default-height 230 :bold-weight extrabold) (t :default-family "Iosevka" :default-weight regular :default-height 140 :variable-pitch-family "Iosevka Aile"))) ;; Set desired style from `fontaine-presets' (fontaine-set-preset 'medium)) (provide 'rul-fonts) #+end_src ** The =io= module The =io= module contains configurations for packages related to Internet services and media. I don't have excessive costumizations in these packages, so they're somewhat unrelated fragments of code grouped in the same file. We install =elfeed= to browse RSS and Atom feeds. #+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-io.el" ;;; rul-io.el --- Configuration for Internet and media packages (use-package elfeed :ensure t) (provide 'rul-feeds) #+end_src The =empv= package allow us to use the =mpv= player from within Emacs. Here we're simply installing it and configuring it with some Internet radio channels. It requires =mpv= to be installed. #+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-io.el" (use-package empv :ensure t :config (bind-key "C-x m" empv-map) (setq empv-radio-channels '( ("SomaFM - Groove Salad" . "http://www.somafm.com/groovesalad.pls") ("SomaFM - DEFCON" . "https://somafm.com/defcon256.pls") ("SomaFM - Metal" . "https://somafm.com/metal.pls") ("SomaFM - Lush" . "https://somafm.com/lush130.pls") ("KCSM Jazz 91" . "http://ice5.securenetsystems.net/KCSM") ))) (provide 'rul-io) #+end_src ** The =mail= module Emacs can act as Mail User Agent. My preferred package for this is =notmuch=. #+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-mail.el" ;;; rul-mail.el --- Email configuration ;; mml-sec.el ;; Use sender to find GPG key. (setq mml-secure-openpgp-sign-with-sender t) (use-package notmuch :ensure t :config ;; UI (setq notmuch-show-logo nil notmuch-column-control 1.0 notmuch-hello-auto-refresh t notmuch-hello-recent-searches-max 20 notmuch-hello-thousands-separator "" notmuch-show-all-tags-list t) ;; Keymaps (defun rul/capture-mail() "Capture mail to org mode." (interactive) (org-store-link nil) (org-capture nil "m") ) (bind-key "c" 'rul/capture-mail notmuch-show-mode-map) (define-key notmuch-show-mode-map "R" 'notmuch-show-reply) (define-key notmuch-search-mode-map "R" 'notmuch-search-reply-to-thread) ;; Spam (define-key notmuch-show-mode-map "S" (lambda () "mark message as spam" (interactive) (notmuch-show-tag (list "+spam" "-inbox" "-unread")))) (define-key notmuch-search-mode-map "S" (lambda (&optional beg end) "mark thread as spam" (interactive (notmuch-search-interactive-region)) (notmuch-search-tag (list "+spam" "-inbox" "-unread") beg end))) ;; Archive (setq notmuch-archive-tags (list "-inbox" "+archive")) (define-key notmuch-show-mode-map "A" (lambda () "archive" (interactive) (notmuch-show-tag (list "+archive" "-inbox" "-unread")) (notmuch-refresh-this-buffer))) (define-key notmuch-search-mode-map "A" (lambda (&optional beg end) "archive thread" (interactive (notmuch-search-interactive-region)) (notmuch-search-tag (list "+archive" "-inbox" "-unread") beg end) (notmuch-refresh-this-buffer))) ;; Mark as read (define-key notmuch-search-mode-map "r" (lambda (&optional beg end) "mark thread as read" (interactive (notmuch-search-interactive-region)) (notmuch-search-tag (list "-unread") beg end) (notmuch-search-next-thread))) (define-key notmuch-search-mode-map (kbd "RET") (lambda () "Show the selected thread with notmuch-tree if it has more than one email. Use notmuch-show otherwise." (interactive) (if (= (plist-get (notmuch-search-get-result) :total) 1) (notmuch-search-show-thread) (notmuch-tree (notmuch-search-find-thread-id) notmuch-search-query-string nil (notmuch-prettify-subject (notmuch-search-find-subject)))))) (defun color-inbox-if-unread () (interactive) (save-excursion (goto-char (point-min)) (let ((cnt (car (process-lines "notmuch" "count" "tag:inbox and tag:unread")))) (when (> (string-to-number cnt) 0) (save-excursion (when (search-forward "inbox" (point-max) t) (let* ((overlays (overlays-in (match-beginning 0) (match-end 0))) (overlay (car overlays))) (when overlay (overlay-put overlay 'face '((:inherit bold) (:foreground "green"))))))))))) (defvar notmuch-hello-refresh-count 0) (defun notmuch-hello-refresh-status-message () (let* ((new-count (string-to-number (car (process-lines notmuch-command "count")))) (diff-count (- new-count notmuch-hello-refresh-count))) (cond ((= notmuch-hello-refresh-count 0) (message "You have %s messages." (notmuch-hello-nice-number new-count))) ((> diff-count 0) (message "You have %s more messages since last refresh." (notmuch-hello-nice-number diff-count))) ((< diff-count 0) (message "You have %s fewer messages since last refresh." (notmuch-hello-nice-number (- diff-count))))) (setq notmuch-hello-refresh-count new-count))) (add-hook 'notmuch-hello-refresh-hook 'color-inbox-if-unread) (add-hook 'notmuch-hello-refresh-hook 'notmuch-hello-refresh-status-message) (setq notmuch-hello-sections '(notmuch-hello-insert-saved-searches notmuch-hello-insert-search notmuch-hello-insert-recent-searches notmuch-hello-insert-alltags )) ;; https://git.sr.ht/~tslil/dotfiles/tree/4e51afbb/emacs/notmuch-config.el#L76-82 (defmacro make-binds (mode-map binds argfunc &rest body) "Create keybindings in `mode-map' using a list of (keystr . arg) pairs in `binds' of the form ( ... (argfunc arg) body)." `(progn ,@(mapcar (lambda (pair) `(define-key ,mode-map (kbd ,(car pair)) (lambda () (interactive) (,argfunc ,(cdr pair)) ,@body))) (eval binds)))) (defvar notmuch-hello-tree-searches '(("u" . "tag:unread") ("i" . "tag:inbox") ("*" . "*")) "List of (key . query) pairs to bind in notmuch-hello.") (make-binds notmuch-hello-mode-map notmuch-hello-tree-searches notmuch-search) ) ;; ends use-package notmuch (use-package notmuch-indicator :ensure t) (provide 'rul-mail) #+end_src ** The =modeline= module The =modeline= module contains code pertaining to Emacs modeline. #+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-modeline.el" ;;; rul-modeline.el --- Modeline configuration ;; Most of the code in this file is based on: ;; https://git.sr.ht/~protesilaos/dotfiles/tree/cf26bc34/item/emacs/.emacs.d/prot-lisp/prot-modeline.el ;; ;; All Kudos to Prot. ;;;; Faces (defface rul-modeline-indicator-red '((default :inherit bold) (((class color) (min-colors 88) (background light)) :foreground "#880000") (((class color) (min-colors 88) (background dark)) :foreground "#ff9f9f") (t :foreground "red")) "Face for modeline indicators.") ;;;; Common helper functions (defcustom rul-modeline-string-truncate-length 9 "String length after which truncation should be done in small windows." :type 'natnum) (defun rul-modeline--string-truncate-p (str) "Return non-nil if STR should be truncated." (and (< (window-total-width) split-width-threshold) (> (length str) rul-modeline-string-truncate-length) (not (one-window-p :no-minibuffer)))) (defun rul-modeline-string-truncate (str) "Return truncated STR, if appropriate, else return STR. Truncation is done up to `rul-modeline-string-truncate-length'." (if (rul-modeline--string-truncate-p str) (concat (substring str 0 rul-modeline-string-truncate-length) "...") str)) ;;;; Major mode (defun rul-modeline-major-mode-indicator () "Return appropriate propertized mode line indicator for the major mode." (let ((indicator (cond ((derived-mode-p 'text-mode) "§") ((derived-mode-p 'prog-mode) "λ") ((derived-mode-p 'comint-mode) ">_") (t "◦")))) (propertize indicator 'face 'shadow))) (defun rul-modeline-major-mode-name () "Return capitalized `major-mode' without the -mode suffix." (capitalize (string-replace "-mode" "" (symbol-name major-mode)))) (defun rul-modeline-major-mode-help-echo () "Return `help-echo' value for `rul-modeline-major-mode'." (if-let ((parent (get major-mode 'derived-mode-parent))) (format "Symbol: `%s'. Derived from: `%s'" major-mode parent) (format "Symbol: `%s'." major-mode))) (defvar-local rul-modeline-major-mode (list (propertize "%[" 'face 'rul-modeline-indicator-red) '(:eval (concat (rul-modeline-major-mode-indicator) " " (propertize (rul-modeline-string-truncate (rul-modeline-major-mode-name)) 'mouse-face 'mode-line-highlight 'help-echo (rul-modeline-major-mode-help-echo)))) (propertize "%]" 'face 'rul-modeline-indicator-red)) "Mode line construct for displaying major modes.") (with-eval-after-load 'eglot (setq mode-line-misc-info (delete '(eglot--managed-mode (" [" eglot--mode-line-format "] ")) mode-line-misc-info))) (defvar-local prot-modeline-eglot `(:eval (when (and (featurep 'eglot) (mode-line-window-selected-p)) '(eglot--managed-mode eglot--mode-line-format))) "Mode line construct displaying Eglot information. Specific to the current window's mode line.") ;;;; Miscellaneous (defvar-local rul-modeline-misc-info '(:eval (when (mode-line-window-selected-p) mode-line-misc-info)) "Mode line construct displaying `mode-line-misc-info'. Specific to the current window's mode line.") ;;;; Display current time (setq display-time-format " %a %e %b, %H:%M ") (setq display-time-default-load-average nil) (setq display-time-mail-string "") ;;;; Variables used in the modeline need to be in `risky-local-variable'. (dolist (construct '( rul-modeline-major-mode rul-modeline-misc-info )) (put construct 'risky-local-variable t)) ;;;; Finally, define the modeline format (setq-default mode-line-format '("%e" mode-line-front-space mode-line-buffer-identification mode-line-front-space rul-modeline-major-mode prot-modeline-eglot mode-line-format-right-align rul-modeline-misc-info )) (provide 'rul-modeline) #+end_src ** The =org= module My org mode configuration is quite big, so I split it across multiple files. #+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-org.el" ;;; rul-org.el --- Org configuration (require 'org) (require 'org-capture) (require 'org-protocol) (require 'org-habit) (require 'rul-org-agenda) (setq org-attach-use-inheritance t) (setq org-cycle-separator-lines 0) (setq org-hide-leading-stars nil) (setq org-startup-indented t) (setq org-edit-src-content-indentation 0) (use-package org-modern :ensure t) (use-package org-pomodoro :ensure t) ;; (add-hook 'org-mode-hook 'turn-off-auto-fill) ;; (add-hook 'auto-save-hook 'org-save-all-org-buffers) (add-hook 'org-mode-hook 'visual-line-mode) (use-package org-download :ensure t :config (add-hook 'dired-mode-hook 'org-download-enable)) (setq org-startup-indented t org-pretty-entities nil org-hide-emphasis-markers t ;; show actually italicized text instead of /italicized text/ org-fontify-whole-heading-line t org-fontify-done-headline t org-fontify-quote-and-verse-blocks t) ;; ORG BINDINGS ;; (global-set-key (kbd "C-c l") #'org-store-link) (global-set-key (kbd "C-c c") #'org-capture) (global-set-key (kbd "C-c s") #'org-schedule) ;; ORG STATES ;; (setq org-todo-keywords (quote ((sequence "TODO(t)" "MAYBE(m)" "NEXT(n)" "|" "DONE(d)") (sequence "WAITING(w@/!)" "HOLD(h@/!)" "|" "CANCELLED(c@/!)" "MEETING")))) (setq org-use-fast-todo-selection t) (setq org-todo-state-tags-triggers (quote (("CANCELLED" ("CANCELLED" . t)) ("WAITING" ("WAITING" . t)) ("HOLD" ("WAITING") ("HOLD" . t)) (done ("WAITING") ("HOLD")) ("TODO" ("WAITING") ("CANCELLED") ("HOLD")) ("NEXT" ("WAITING") ("CANCELLED") ("HOLD")) ("DONE" ("WAITING") ("CANCELLED") ("HOLD"))))) (setq org-enforce-todo-dependencies t) (setq org-log-done (quote time)) (setq org-log-redeadline (quote time)) (setq org-log-reschedule (quote time)) ;; CAPTURE ;; (setq org-capture-templates (quote ( ("w" "Todo" entry (file+headline org-refile-path "Tasks") "* TODO %?" :empty-lines 1) ("m" "Capture incoming email" entry (file+headline org-refile-path "Incoming") "* TODO Re: %:description\n\n Source: %u, %a\n" :empty-lines 1) ("e" "Elfeed entry" entry (file+headline org-refile-path "Read later") "* %? [[%:link][%:description]]\n %U\n %:description\n") ("L" "Web Link" entry (file+headline org-refile-path "Read later") "* %?[[%:link][%:description]] \"\")\n %:initial\n \nCaptured On: %U" ) ("l" "Web Link with Selection" entry (file+headline org-refile-path "Read later") "* [[%:link][%:description]] \n %:initial\n \nCaptured On: %U") ))) ;; REFILE ;; ; Targets include this file and any file contributing to the agenda - up to 3 levels deep (setq org-refile-targets '((nil :maxlevel . 3) (org-agenda-files :maxlevel . 3))) ; Targets complete directly with IDO (setq org-outline-path-complete-in-steps nil) ; Allow refile to create parent tasks with confirmation (setq org-refile-allow-creating-parent-nodes (quote confirm)) ;; ORG REPORTS ;; ; Set default column view headings: Task Effort Clock_Summary (setq org-columns-default-format "%80ITEM(Task) %10Effort(Effort){:} %10CLOCKSUM") (defun my-org-clocktable-indent-string (level) (if (= level 1) "" (let ((str "^")) (while (> level 2) (setq level (1- level) str (concat str "--"))) (concat str "-> ")))) (advice-add 'org-clocktable-indent-string :override #'my-org-clocktable-indent-string) (setq org-clock-clocktable-default-properties '(:maxlevel 4 :scope file :formula %)) ; global Effort estimate values ; global STYLE property values for completion (setq org-global-properties (quote (("Effort_ALL" . "0:15 0:30 0:45 1:00 2:00 3:00 4:00 5:00 6:00 0:00") ("STYLE_ALL" . "habit")))) ;; TAGS ;; ; Tags with fast selection keys (setq org-tag-alist (quote ((:startgroup) ("@errand" . ?e) ("@office" . ?o) ("@home" . ?H) (:endgroup) ("WAITING" . ?w) ("HOLD" . ?h) ("CANCELLED" . ?c) ("FLAGGED" . ??)))) (setq org-stuck-projects '("+LEVEL=2+PROJECT/-MAYBE-DONE" ("NEXT") ("@shop") "\\")) ; Allow setting single tags without the menu (setq org-fast-tag-selection-single-key (quote expert)) ;; org-modern (add-hook 'org-mode-hook 'org-modern-mode) (add-hook 'org-agenda-finalize-hook #'org-modern-agenda) ;; Honor ATTR_ORG attribute. Defaults to image's width if not set. (setq org-image-actual-width nil) (provide 'rul-org) #+end_src #+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-org-agenda.el" ;;; rul-org-agenda.el --- Org agenda configuration (require 'org) (global-set-key (kbd "") #'org-agenda) (global-set-key (kbd "C-c a") #'org-agenda) (defun bh/is-project-p () "Any task with a todo keyword subtask" (save-restriction (widen) (let ((has-subtask) (subtree-end (save-excursion (org-end-of-subtree t))) (is-a-task (member (nth 2 (org-heading-components)) org-todo-keywords-1))) (save-excursion (forward-line 1) (while (and (not has-subtask) (< (point) subtree-end) (re-search-forward "^\*+ " subtree-end t)) (when (member (org-get-todo-state) org-todo-keywords-1) (setq has-subtask t)))) (and is-a-task has-subtask)))) (defun bh/is-project-subtree-p () "Any task with a todo keyword that is in a project subtree. Callers of this function already widen the buffer view." (let ((task (save-excursion (org-back-to-heading 'invisible-ok) (point)))) (save-excursion (bh/find-project-task) (if (equal (point) task) nil t)))) (defun bh/is-task-p () "Any task with a todo keyword and no subtask" (save-restriction (widen) (let ((has-subtask) (subtree-end (save-excursion (org-end-of-subtree t))) (is-a-task (member (nth 2 (org-heading-components)) org-todo-keywords-1))) (save-excursion (forward-line 1) (while (and (not has-subtask) (< (point) subtree-end) (re-search-forward "^\*+ " subtree-end t)) (when (member (org-get-todo-state) org-todo-keywords-1) (setq has-subtask t)))) (and is-a-task (not has-subtask))))) (defun bh/is-subproject-p () "Any task which is a subtask of another project" (let ((is-subproject) (is-a-task (member (nth 2 (org-heading-components)) org-todo-keywords-1))) (save-excursion (while (and (not is-subproject) (org-up-heading-safe)) (when (member (nth 2 (org-heading-components)) org-todo-keywords-1) (setq is-subproject t)))) (and is-a-task is-subproject))) (defun bh/list-sublevels-for-projects-indented () "Set org-tags-match-list-sublevels so when restricted to a subtree we list all subtasks. This is normally used by skipping functions where this variable is already local to the agenda." (if (marker-buffer org-agenda-restrict-begin) (setq org-tags-match-list-sublevels 'indented) (setq org-tags-match-list-sublevels nil)) nil) (defun bh/list-sublevels-for-projects () "Set org-tags-match-list-sublevels so when restricted to a subtree we list all subtasks. This is normally used by skipping functions where this variable is already local to the agenda." (if (marker-buffer org-agenda-restrict-begin) (setq org-tags-match-list-sublevels t) (setq org-tags-match-list-sublevels nil)) nil) (defvar bh/hide-scheduled-and-waiting-next-tasks t) (defun bh/toggle-next-task-display () (interactive) (setq bh/hide-scheduled-and-waiting-next-tasks (not bh/hide-scheduled-and-waiting-next-tasks)) (when (equal major-mode 'org-agenda-mode) (org-agenda-redo)) (message "%s WAITING and SCHEDULED NEXT Tasks" (if bh/hide-scheduled-and-waiting-next-tasks "Hide" "Show"))) (defun bh/skip-stuck-projects () "Skip trees that are not stuck projects" (save-restriction (widen) (let ((next-headline (save-excursion (or (outline-next-heading) (point-max))))) (if (bh/is-project-p) (let* ((subtree-end (save-excursion (org-end-of-subtree t))) (has-next )) (save-excursion (forward-line 1) (while (and (not has-next) (< (point) subtree-end) (re-search-forward "^\\*+ NEXT " subtree-end t)) (unless (member "WAITING" (org-get-tags-at)) (setq has-next t)))) (if has-next nil next-headline)) ; a stuck project, has subtasks but no next task nil)))) (defun bh/skip-non-stuck-projects () "Skip trees that are not stuck projects" ;; (bh/list-sublevels-for-projects-indented) (save-restriction (widen) (let ((next-headline (save-excursion (or (outline-next-heading) (point-max))))) (if (bh/is-project-p) (let* ((subtree-end (save-excursion (org-end-of-subtree t))) (has-next )) (save-excursion (forward-line 1) (while (and (not has-next) (< (point) subtree-end) (re-search-forward "^\\*+ NEXT " subtree-end t)) (unless (member "WAITING" (org-get-tags-at)) (setq has-next t)))) (if has-next next-headline nil)) ; a stuck project, has subtasks but no next task next-headline)))) (defun bh/skip-non-projects () "Skip trees that are not projects" ;; (bh/list-sublevels-for-projects-indented) (if (save-excursion (bh/skip-non-stuck-projects)) (save-restriction (widen) (let ((subtree-end (save-excursion (org-end-of-subtree t)))) (cond ((bh/is-project-p) nil) ((and (bh/is-project-subtree-p) (not (bh/is-task-p))) nil) (t subtree-end)))) (save-excursion (org-end-of-subtree t)))) (defun bh/skip-non-tasks () "Show non-project tasks. Skip project and sub-project tasks, habits, and project related tasks." (save-restriction (widen) (let ((next-headline (save-excursion (or (outline-next-heading) (point-max))))) (cond ((bh/is-task-p) nil) (t next-headline))))) (defun bh/skip-project-trees-and-habits () "Skip trees that are projects" (save-restriction (widen) (let ((subtree-end (save-excursion (org-end-of-subtree t)))) (cond ((bh/is-project-p) subtree-end) ((org-is-habit-p) subtree-end) (t nil))))) (defun bh/skip-projects-and-habits-and-single-tasks () "Skip trees that are projects, tasks that are habits, single non-project tasks" (save-restriction (widen) (let ((next-headline (save-excursion (or (outline-next-heading) (point-max))))) (cond ((org-is-habit-p) next-headline) ((and bh/hide-scheduled-and-waiting-next-tasks (member "WAITING" (org-get-tags-at))) next-headline) ((bh/is-project-p) next-headline) ((and (bh/is-task-p) (not (bh/is-project-subtree-p))) next-headline) (t nil))))) (defun bh/skip-project-tasks-maybe () "Show tasks related to the current restriction. When restricted to a project, skip project and sub project tasks, habits, NEXT tasks, and loose tasks. When not restricted, skip project and sub-project tasks, habits, and project related tasks." (save-restriction (widen) (let* ((subtree-end (save-excursion (org-end-of-subtree t))) (next-headline (save-excursion (or (outline-next-heading) (point-max)))) (limit-to-project (marker-buffer org-agenda-restrict-begin))) (cond ((bh/is-project-p) next-headline) ((org-is-habit-p) subtree-end) ((and (not limit-to-project) (bh/is-project-subtree-p)) subtree-end) ((and limit-to-project (bh/is-project-subtree-p) (member (org-get-todo-state) (list "NEXT"))) subtree-end) (t nil))))) (defun bh/skip-project-tasks () "Show non-project tasks. Skip project and sub-project tasks, habits, and project related tasks." (save-restriction (widen) (let* ((subtree-end (save-excursion (org-end-of-subtree t)))) (cond ((bh/is-project-p) subtree-end) ((org-is-habit-p) subtree-end) ((bh/is-project-subtree-p) subtree-end) ((not (org-entry-is-todo-p)) subtree-end) (t nil))))) (defun bh/skip-non-project-tasks () "Show project tasks. Skip project and sub-project tasks, habits, and loose non-project tasks." (save-restriction (widen) (let* ((subtree-end (save-excursion (org-end-of-subtree t))) (next-headline (save-excursion (or (outline-next-heading) (point-max))))) (cond ((bh/is-project-p) next-headline) ((org-is-habit-p) subtree-end) ((and (bh/is-project-subtree-p) (member (org-get-todo-state) (list "NEXT"))) subtree-end) ((not (bh/is-project-subtree-p)) subtree-end) (t nil))))) (defun bh/skip-projects-and-habits () "Skip trees that are projects and tasks that are habits" (save-restriction (widen) (let ((subtree-end (save-excursion (org-end-of-subtree t)))) (cond ((bh/is-project-p) subtree-end) ((org-is-habit-p) subtree-end) (t nil))))) (defun bh/skip-non-subprojects () "Skip trees that are not projects" (let ((next-headline (save-excursion (outline-next-heading)))) (if (bh/is-subproject-p) nil next-headline))) ;; CLOCKING ;; ;; Resume clocking task when emacs is restarted (org-clock-persistence-insinuate) ;; ;; Show lot of clocking history so it's easy to pick items off the C-F11 list (setq org-clock-history-length 23) ;; Resume clocking task on clock-in if the clock is open (setq org-clock-in-resume t) ;; Change tasks to NEXT when clocking in (setq org-clock-in-switch-to-state 'bh/clock-in-to-next) ;; Separate drawers for clocking and logs (setq org-drawers (quote ("PROPERTIES" "LOGBOOK"))) ;; Save clock data and state changes and notes in the LOGBOOK drawer (setq org-clock-into-drawer t) ;; Sometimes I change tasks I'm clocking quickly - this removes clocked tasks with 0:00 duration (setq org-clock-out-remove-zero-time-clocks t) ;; Clock out when moving task to a done state (setq org-clock-out-when-done t) ;; Save the running clock and all clock history when exiting Emacs, load it on startup (setq org-clock-persist t) ;; Do not prompt to resume an active clock (setq org-clock-persist-query-resume nil) ;; Enable auto clock resolution for finding open clocks (setq org-clock-auto-clock-resolution (quote when-no-clock-is-running)) ;; Include current clocking task in clock reports (setq org-clock-report-include-clocking-task t) (setq bh/keep-clock-running nil) (defun bh/clock-in-to-next (kw) "Switch a task from TODO to NEXT when clocking in. Skips capture tasks, projects, and subprojects. Switch projects and subprojects from NEXT back to TODO" (when (not (and (boundp 'org-capture-mode) org-capture-mode)) (cond ((and (member (org-get-todo-state) (list "TODO")) (bh/is-task-p)) "NEXT") ((and (member (org-get-todo-state) (list "NEXT")) (bh/is-project-p)) "TODO")))) (defun bh/find-project-task () "Move point to the parent (project) task if any" (save-restriction (widen) (let ((parent-task (save-excursion (org-back-to-heading 'invisible-ok) (point)))) (while (org-up-heading-safe) (when (member (nth 2 (org-heading-components)) org-todo-keywords-1) (setq parent-task (point)))) (goto-char parent-task) parent-task))) (defun bh/punch-in (arg) "Start continuous clocking and set the default task to the selected task. If no task is selected set the Organization task as the default task." (interactive "p") (setq bh/keep-clock-running t) (if (equal major-mode 'org-agenda-mode) ;; ;; We're in the agenda ;; (let* ((marker (org-get-at-bol 'org-hd-marker)) (tags (org-with-point-at marker (org-get-tags-at)))) (if (and (eq arg 4) tags) (org-agenda-clock-in '(16)) (bh/clock-in-organization-task-as-default))) ;; ;; We are not in the agenda ;; (save-restriction (widen) ; Find the tags on the current task (if (and (equal major-mode 'org-mode) (not (org-before-first-heading-p)) (eq arg 4)) (org-clock-in '(16)) (bh/clock-in-organization-task-as-default))))) (defun bh/punch-out () (interactive) (setq bh/keep-clock-running nil) (when (org-clock-is-active) (org-clock-out)) (org-agenda-remove-restriction-lock)) (defun bh/clock-in-default-task () (save-excursion (org-with-point-at org-clock-default-task (org-clock-in)))) (defun bh/clock-in-parent-task () "Move point to the parent (project) task if any and clock in" (let ((parent-task)) (save-excursion (save-restriction (widen) (while (and (not parent-task) (org-up-heading-safe)) (when (member (nth 2 (org-heading-components)) org-todo-keywords-1) (setq parent-task (point)))) (if parent-task (org-with-point-at parent-task (org-clock-in)) (when bh/keep-clock-running (bh/clock-in-default-task))))))) (defvar bh/organization-task-id "eb155a82-92b2-4f25-a3c6-0304591af2f9") ;; https://stackoverflow.com/a/10091330 (defun zin/org-agenda-skip-tag (tag &optional others) "Skip all entries that correspond to TAG. If OTHERS is true, skip all entries that do not correspond to TAG." (let ((next-headline (save-excursion (or (outline-next-heading) (point-max)))) (current-headline (or (and (org-at-heading-p) (point)) (save-excursion (org-back-to-heading))))) (if others (if (not (member tag (org-get-tags-at current-headline))) next-headline nil) (if (member tag (org-get-tags-at current-headline)) next-headline nil)))) (defun bh/clock-in-organization-task-as-default () (interactive) (org-with-point-at (org-id-find bh/organization-task-id 'marker) (org-clock-in '(16)))) (defun bh/clock-out-maybe () (when (and bh/keep-clock-running (not org-clock-clocking-in) (marker-buffer org-clock-default-task) (not org-clock-resolving-clocks-due-to-idleness)) (bh/clock-in-parent-task))) (add-hook 'org-clock-out-hook 'bh/clock-out-maybe 'append) ;; AGENDA VIEW ;; ;; Do not dim blocked tasks (setq org-agenda-compact-blocks nil) (setq org-agenda-dim-blocked-tasks nil) (setq org-agenda-block-separator 61) ;; Agenda log mode items to display (closed and state changes by default) (setq org-agenda-log-mode-items (quote (closed state))) ; For tag searches ignore tasks with scheduled and deadline dates (setq org-agenda-tags-todo-honor-ignore-options t) (setq org-icalendar-include-body nil) (setq org-icalendar-include-bbdb-anniversaries t) (setq org-icalendar-include-todo t) (setq org-icalendar-use-scheduled '(todo-start event-if-not-todo event-if-todo-not-done)) (provide 'rul-org-agenda) #+end_src ** The =prog= module This package contains code related to programming or markup languages modes. As my configurations are generally small, I prefer to have them on a single file. #+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-prog.el" ;;; rul-prog.el --- Configuration related to programming and markup ;;; languages (use-package eglot :ensure t) ;; Go (use-package go-mode :ensure t :init (progn (bind-key [remap find-tag] #'godef-jump)) :config (add-hook 'go-mode-hook #'yas-minor-mode) (add-hook 'go-mode-hook 'electric-pair-mode) (add-hook 'go-mode-hook 'my-go-mode-hook) (add-hook 'before-save-hook 'gofmt-before-save)) (use-package go-eldoc :ensure t :init (add-hook 'go-mode-hook 'go-eldoc-setup)) ;; Latex (add-hook 'latex-mode-hook 'flyspell-mode) (setq TeX-PDF-mode t) (defun pdfevince () (add-to-list 'TeX-output-view-style '("^pdf$" "." "evince %o %(outpage)"))) (add-hook 'LaTeX-mode-hook 'pdfevince t) ; AUCTeX LaTeX mode ;; Markdown (use-package markdown-mode :ensure t :config (setq auto-mode-alist (cons '("\\.mdwn" . markdown-mode) auto-mode-alist))) ;; Python (use-package blacken :ensure t :defer t) (add-hook 'python-mode-hook 'py-autopep8-enable-on-save) ;; Terraform (use-package terraform-mode :ensure t :defer t) ;; YAML (use-package yaml-mode :ensure t :defer t) ;; Rust (use-package rust-mode :defer t :init (setq rust-mode-treesitter-derive t) :config (add-hook 'rust-mode-hook 'eglot-ensure)) (provide 'rul-prog) #+end_src