#+title: Luj's literate Emacs configuration #+author: Luj (Julien Malka) #+TOC: headlines #+macro: latest-export-date (eval (format-time-string "%F %T %z")) #+macro: word-count (eval (count-words (point-min) (point-max))) *Last revised and exported on {{{latest-export-date}}} with a word count of {{{word-count}}}.* This is my literate Emacs configuration. It is written with several objectives in mind: - Minimalism: I am trying to use as little external code as possible, in order to keep the experience fast; - Functionality: I want all the features of modern editor, and more. * Early initialisation The ~early-init.el~ file is the first file loaded during ~emacs~ initialisation. It can be used to setup frames before ~emacs~ actually start loading external code. ** Setting up frame parameters I want to configure the frame size if I am in a non-tiling window manager, for this I define the ~with-desktop-session~ macro, inspired by [[https://protesilaos.com/emacs/dotemacs][Protesilaos Stavrou's emacs configuration]]. #+begin_src emacs-lisp :tangle "early-init.el" (defvar luj/tiling-window-manager-regexp "hyprland\\|none\\+exwm" "Regular expression to tiling window managers. See definition of the `with-desktop-session' macro.") (defmacro with-desktop-session (&rest body) "Expand BODY if desktop session is not a tiling window manager." (declare (indent 0)) `(when-let* ((session (getenv "DESKTOP_SESSION")) ((not (string-match-p luj/tiling-window-manager-regexp session)))) ,@body)) #+end_src We can then apply the macro to make frame adjustments only in desktop environments. #+begin_src emacs-lisp :tangle "early-init.el" (with-desktop-session (mapc (lambda (var) (add-to-list var '(width . (text-pixels . 2200))) (add-to-list var '(height . (text-pixels . 1200)))) '(default-frame-alist initial-frame-alist))) #+end_src I also adjust the frame color to use the same background as Catpuccin mocha, which is my theme of choice. #+begin_src emacs-lisp :tangle "early-init.el" (set-face-attribute 'default nil :background "#1e1e2e" :foreground "#ffffff") #+end_src Let's setup also additional frame settings that are nice to have. #+begin_src emacs-lisp :tangle "early-init.el" (setq frame-resize-pixelwise t frame-inhibit-implied-resize t ring-bell-function 'ignore use-dialog-box t use-file-dialog nil use-short-answers t inhibit-splash-screen t inhibit-startup-screen t inhibit-x-resources t inhibit-startup-echo-area-message user-login-name inhibit-startup-buffer-menu t) (menu-bar-mode -1) (scroll-bar-mode -1) (tool-bar-mode -1) #+end_src ** Package manager bootstrap I am using the ~elpaca~ package manager, that needs to be bootstrapped because it is not included in ~emacs~. #+begin_src emacs-lisp :tangle "early-init.el" (setq elpaca-core-date '(20250301)) (defvar elpaca-installer-version 0.10) (defvar elpaca-directory (expand-file-name "elpaca/" user-emacs-directory)) (defvar elpaca-builds-directory (expand-file-name "builds/" elpaca-directory)) (defvar elpaca-repos-directory (expand-file-name "repos/" elpaca-directory)) (defvar elpaca-order '(elpaca :repo "https://github.com/progfolio/elpaca.git" :ref nil :depth 1 :inherit ignore :files (:defaults "elpaca-test.el" (:exclude "extensions")) :build (:not elpaca--activate-package))) (let* ((repo (expand-file-name "elpaca/" elpaca-repos-directory)) (build (expand-file-name "elpaca/" elpaca-builds-directory)) (order (cdr elpaca-order)) (default-directory repo)) (add-to-list 'load-path (if (file-exists-p build) build repo)) (unless (file-exists-p repo) (make-directory repo t) (when (<= emacs-major-version 28) (require 'subr-x)) (condition-case-unless-debug err (if-let* ((buffer (pop-to-buffer-same-window "*elpaca-bootstrap*")) ((zerop (apply #'call-process `("git" nil ,buffer t "clone" ,@(when-let* ((depth (plist-get order :depth))) (list (format "--depth=%d" depth) "--no-single-branch")) ,(plist-get order :repo) ,repo)))) ((zerop (call-process "git" nil buffer t "checkout" (or (plist-get order :ref) "--")))) (emacs (concat invocation-directory invocation-name)) ((zerop (call-process emacs nil buffer nil "-Q" "-L" "." "--batch" "--eval" "(byte-recompile-directory \".\" 0 'force)"))) ((require 'elpaca)) ((elpaca-generate-autoloads "elpaca" repo))) (progn (message "%s" (buffer-string)) (kill-buffer buffer)) (error "%s" (with-current-buffer buffer (buffer-string)))) ((error) (warn "%s" err) (delete-directory repo 'recursive)))) (unless (require 'elpaca-autoloads nil t) (require 'elpaca) (elpaca-generate-autoloads "elpaca" repo) (load "./elpaca-autoloads"))) (add-hook 'after-init-hook #'elpaca-process-queues) (elpaca `(,@elpaca-order)) (setq package-enable-at-startup nil) #+end_src We now integrate ~elpaca~ with ~use-package~: #+begin_src emacs-lisp :tangle "early-init.el" (elpaca elpaca-use-package (elpaca-use-package-mode)) #+end_src * General quality of life improvements ** Remove littering Prevent ~emacs~ from storing garbage everywhere in my repos. #+begin_src emacs-lisp :tangle "init.el" (use-package no-littering :ensure t :demand t :config (no-littering-theme-backups)) #+end_src ** Save history and editing position #+begin_src emacs-lisp :tangle "init.el" (use-package savehist :init (savehist-mode)) (save-place-mode 1) #+end_src ** Accept tangling this file as a hook in a local variable #+begin_src emacs-lisp :tangle "init.el" (add-to-list 'safe-local-eval-forms '(add-hook 'after-save-hook 'org-babel-tangle nil t)) #+end_src ** No impure customizations #+begin_src emacs-lisp :tangle "init.el" (setq custom-file (make-temp-file "emacs-custom-")) #+end_src ** Auto-reverting buffers #+begin_src emacs-lisp :tangle init.el (use-package autorevert :ensure nil :hook (after-init . global-auto-revert-mode) :config (setq auto-revert-verbose t)) #+end_src ** Which key We sometimes need a little help to figure out keybindings, using ~which-key~ for this. #+begin_src emacs-lisp :tangle "init.el" (use-package which-key :ensure t :config (which-key-mode)) #+end_src ** Misc changes #+begin_src emacs-lisp :tangle "init.el" (use-package emacs :ensure nil :demand t :config (setq help-window-select t) (setq dired-kill-when-opening-new-dired-buffer t) (setq eval-expression-print-length nil) (setq kill-do-not-save-duplicates t)) #+end_src * Theming and user interface ** Main theme I am a pretty big fan of the Catpuccin color palette, I use the ~mocha~ variation as my main theme. #+begin_src emacs-lisp :tangle "init.el" (use-package catppuccin-theme :ensure t :config (load-theme 'catppuccin :no-confirm)) #+end_src I also configure ~modus-operandi~ as my backup theme when I need a light theme. #+begin_src emacs-lisp :tangle "init.el" (use-package modus-themes :ensure t) #+end_src ** Solaire mode Allows to have darker colors for non editing buffers, a bit like an IDE. #+begin_src emacs-lisp :tangle "init.el" (use-package solaire-mode :ensure t :config (solaire-global-mode +1)) #+end_src ** Mode line I use the ~doom-modeline~, it's nice and clean. #+begin_src emacs-lisp :tangle "init.el" (use-package doom-modeline :ensure t :init (doom-modeline-mode 1)) #+end_src ** Nerd icons Nerd icons are a nice addition to the UI, I try to enable them wherever I can. #+begin_src emacs-lisp :tangle "init.el" (use-package nerd-icons-dired :ensure t :hook (dired-mode . nerd-icons-dired-mode)) (use-package nerd-icons-completion :ensure t :after marginalia :config (nerd-icons-completion-mode) (add-hook 'marginalia-mode-hook #'nerd-icons-completion-marginalia-setup)) (use-package nerd-icons-corfu :ensure t :after corfu :config (add-to-list 'corfu-margin-formatters #'nerd-icons-corfu-formatter)) #+end_src ** Line highlighting #+begin_src emacs-lisp :tangle "init.el" (use-package lin :ensure (:host github :repo "protesilaos/lin") :config (setq global-hl-line-sticky-flag t) (lin-global-mode) (global-hl-line-mode)) #+end_src ** Fonts #+begin_src emacs-lisp :tangle "init.el" (use-package fontaine :ensure t :hook ((after-init . fontaine-mode) (after-init . (lambda () ;; Set last preset or fall back to desired style from `fontaine-presets'. (fontaine-set-preset (or (fontaine-restore-latest-preset) 'regular))))) :bind (("C-c f" . fontaine-set-preset) ("C-c F" . fontaine-toggle-preset)) :config (setq x-underline-at-descent-line nil) (setq-default text-scale-remap-header-line t) (setq fontaine-latest-state-file (locate-user-emacs-file "fontaine-latest-state.eld")) (setq fontaine-presets '((small :default-height 80) (regular) ; like this it uses all the fallback values and is named `regular' (medium :default-family "FiraCode Nerd Font" :default-height 115 :fixed-pitch-family "FiraCode Nerd Font" :variable-pitch-family "FiraCode Nerd Font") (large :inherit medium :default-height 150) (presentation :inherit medium :default-height 180) (jumbo :inherit medium :default-height 260) (t :default-weight regular :default-slant normal :default-width normal :default-height 100 :fixed-pitch-weight nil :fixed-pitch-slant nil :fixed-pitch-width nil :fixed-pitch-height 1.0 :fixed-pitch-serif-family nil :fixed-pitch-serif-weight nil :fixed-pitch-serif-slant nil :fixed-pitch-serif-width nil :fixed-pitch-serif-height 1.0 :variable-pitch-weight nil :variable-pitch-slant nil :variable-pitch-width nil :variable-pitch-height 1.0 :mode-line-active-family nil :mode-line-active-weight nil :mode-line-active-slant nil :mode-line-active-width nil :mode-line-active-height 1.0 :mode-line-inactive-family nil :mode-line-inactive-weight nil :mode-line-inactive-slant nil :mode-line-inactive-width nil :mode-line-inactive-height 1.0 :header-line-family nil :header-line-weight nil :header-line-slant nil :header-line-width nil :header-line-height 1.0 :line-number-family nil :line-number-weight nil :line-number-slant nil :line-number-width nil :line-number-height 1.0 :tab-bar-family nil :tab-bar-weight nil :tab-bar-slant nil :tab-bar-width nil :tab-bar-height 1.0 :tab-line-family nil :tab-line-weight nil :tab-line-slant nil :tab-line-width nil :tab-line-height 1.0 :bold-family nil :bold-slant nil :bold-weight bold :bold-width nil :bold-height 1.0 :italic-family nil :italic-weight nil :italic-slant italic :italic-width nil :italic-height 1.0 :line-spacing nil)))) #+end_src * Editing facilities ** Meow I use ~meow~ as a modal editing framework. It is less invasive than ~evil-mode~ for example. #+begin_src emacs-lisp :tangle "init.el" (defun meow-setup () (meow-motion-overwrite-define-key '("s-SPC" . meow-keypad) '("j" . meow-next) '("k" . meow-prev) '("" . ignore)) (meow-leader-define-key '("j" . "H-j") '("k" . "H-k") '("1" . meow-digit-argument) '("2" . meow-digit-argument) '("3" . meow-digit-argument) '("4" . meow-digit-argument) '("5" . meow-digit-argument) '("6" . meow-digit-argument) '("7" . meow-digit-argument) '("8" . meow-digit-argument) '("9" . meow-digit-argument) '("0" . meow-digit-argument) '("/" . meow-keypad-describe-key) '("?" . meow-cheatsheet)) (meow-normal-define-key '("s-SPC" . meow-keypad) '("0" . meow-expand-0) '("9" . meow-expand-9) '("8" . meow-expand-8) '("7" . meow-expand-7) '("6" . meow-expand-6) '("5" . meow-expand-5) '("4" . meow-expand-4) '("3" . meow-expand-3) '("2" . meow-expand-2) '("1" . meow-expand-1) '("-" . negative-argument) '(";" . meow-reverse) '("," . meow-inner-of-thing) '("." . meow-bounds-of-thing) '("[" . meow-beginning-of-thing) '("]" . meow-end-of-thing) '("a" . meow-append) '("A" . meow-open-below) '("b" . meow-back-word) '("B" . meow-back-symbol) '("c" . meow-change) '("d" . meow-delete) '("D" . meow-backward-delete) '("e" . meow-next-word) '("E" . meow-next-symbol) '("f" . meow-find) '("g" . meow-cancel-selection) '("G" . meow-grab) '("h" . meow-left) '("H" . meow-left-expand) '("i" . meow-insert) '("I" . meow-open-above) '("j" . meow-next) '("J" . meow-next-expand) '("k" . meow-prev) '("K" . meow-prev-expand) '("l" . meow-right) '("L" . meow-right-expand) '("m" . meow-join) '("n" . meow-search) '("o" . meow-block) '("O" . meow-to-block) '("p" . meow-yank) '("q" . meow-quit) '("Q" . meow-goto-line) '("r" . meow-replace) '("R" . meow-swap-grab) '("s" . meow-kill) '("t" . meow-till) '("u" . meow-undo) '("U" . meow-undo-in-selection) '("v" . meow-visit) '("w" . meow-mark-word) '("W" . meow-mark-symbol) '("x" . meow-visual-line) '("X" . meow-goto-line) '("y" . meow-save) '("Y" . meow-sync-grab) '("z" . meow-pop-selection) '("'" . repeat) '("" . ignore))) (use-package meow :ensure (:wait t) :demand t :custom (meow-keypad-leader-dispatch "C-c") :config (meow-global-mode 1) (meow-setup)) #+end_src Numbers for region extension sometimes slow down my editing workflow, so I disable it. #+begin_src emacs-lisp :tangle "init.el" (use-package meow :ensure nil :config (setq meow-expand-hint-remove-delay 0) (setq meow-expand-hint-counts nil)) #+end_src ** Avy ~Avy~ is a nice addition to ~meow~: it allows to jump to specific visible places in the frame using key chords. #+begin_src emacs-lisp :tangle "init.el" (use-package avy :ensure t :bind ("C-c j" . avy-goto-char)) #+end_src * Completions Completions are at the core of the editing experience, I try to use a stack that is both feature-full and yet minimal and well integrated with ~emacs~. ** Minibuffer completions *** Vertico ~Vertico~ enables VERTIcal COmpletions in the minibuffer. #+begin_src emacs-lisp :tangle "init.el" (use-package vertico :ensure (:files (:defaults "extensions/vertico-directory.el")) :demand t :bind (:map vertico-map ("DEL" . vertico-directory-delete-char) ("M-DEL" . vertico-directory-delete-word)) :init (vertico-mode)) #+end_src *** Orderless With ~orderless~ we can get fuzzy searches in the minibuffer. #+begin_src emacs-lisp :tangle "init.el" (use-package orderless :ensure t :custom (completion-styles '(orderless basic)) (completion-category-overrides '((file (styles basic partial-completion))))) #+end_src *** Marginalia Setup contextual information on minibuffer options. #+begin_src emacs-lisp :tangle "init.el" (use-package marginalia :ensure t :init (marginalia-mode)) #+end_src *** Consult ~Consult~ is another nice improvement to minibuffers, where it allows you to live-preview the buffers that would be opened by selecting a given option. #+begin_src emacs-lisp :tangle "init.el" (use-package consult :ensure t :bind (("C-c b" . consult-buffer) ("C-x C-b" . consult-buffer))) #+end_src ** In buffer completions *** Corfu & Cape ~Corfu~ is the equivalent of ~vertico~ for in buffer completions, for example LSP suggestions, but not limited to that. #+begin_src emacs-lisp :tangle "init.el" (use-package corfu :ensure t :config (setq corfu-auto t) :init (global-corfu-mode) (corfu-popupinfo-mode) (corfu-history-mode) :config (setq corfu-popupinfo-delay '(0.2 . 0.1)) (setq corfu-popupinfo-hide nil)) #+end_src ~Cape~ is a completion provider for ~corfu~. I mostly use it to complete filenames, mostly. #+begin_src emacs-lisp :tangle "init.el" (use-package cape :ensure t :bind ("M-p" . cape-prefix-map) :init (add-hook 'completion-at-point-functions #'cape-file)) #+end_src * Buffer and window management First I define some keybindings that are helpful in order to make window management more straightforward. #+begin_src emacs-lisp :tangle "init.el" (use-package luj-windows :ensure nil :after ace-window :bind (:map mode-specific-map ("w r" . split-window-right) ("w d" . split-window-below) ("w " . windmove-right) ("w " . windmove-left) ("w " . windmove-up) ("w " . windmove-down) ("w x" . delete-window) ("w w" . other-window) ("w o" . ace-window))) #+end_src ** Embark ~Embark~ is of course much more versatile than just window/buffer management, but I particularly use it to manage buffer placement when opening them, so... it counts. #+begin_src emacs-lisp :tangle "init.el" (use-package embark :ensure t :bind (("C-." . embark-act) ;; pick some comfortable binding ("C-;" . embark-dwim) ;; good alternative: M-. ("C-h B" . embark-bindings)) ;; alternative for `describe-bindings' :init ;; Optionally replace the key help with a completing-read interface (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))))) ;; Consult users will also want the embark-consult package. (use-package embark-consult :ensure t ; only need to install it, embark loads it after consult if found :hook (embark-collect-mode . consult-preview-at-point-mode)) #+end_src ** Ace window In the same spirit as ~avy~, ~ace~ is super useful to select/act on windows when you have a lot of them open. #+begin_src emacs-lisp :tangle "init.el" (use-package ace-window :ensure t :config (ace-window-display-mode)) #+end_src * My writing tools ** Org mode Obviously, the bulk of my writing activity in ~emacs~ is done in the amazing ~org-mode~. To stay on top of new releases, I use the upstream version. #+begin_src emacs-lisp :tangle "init.el" (use-package org :ensure (:wait t) :config ;; Exclude the daily tag from inheritance so that archived tasks don't appear with this tag in my agenda (add-to-list 'org-tags-exclude-from-inheritance "daily") (setq org-hide-leading-stars t) (setq org-hide-emphasis-markers t)) #+end_src ** Org modern To make ~org-mode~ a bit cleaner. #+begin_src emacs-lisp :tangle "init.el" (use-package org-modern :ensure t :init (global-org-modern-mode) :custom (org-modern-star nil) (org-modern-block-fringe nil) (add-hook 'org-agenda-finalize-hook #'org-modern-agenda) :hook (org-mode . org-indent-mode) (org-mode . visual-line-mode)) #+end_src ** Taking notes with Denote I used to rely on ~org-roam~ for my notetaking, but I like the simpler philosophy of ~denote~, relying on simpler basis and allowing to link to non-org resources. #+begin_src emacs-lisp :tangle "init.el" (use-package denote :ensure (:host github :repo "protesilaos/denote") :hook (dired-mode . denote-dired-mode) :bind (("C-c n n" . denote) ("C-c n r" . denote-rename-file) ("C-c n l" . denote-link) ("C-c n f" . denote-open-or-create) ("C-c n b" . denote-backlinks) ("C-c n d" . denote-sort-dired)) :config (setq denote-directory (expand-file-name "~/dev/notes/")) (setq denote-save-buffers t) (setq denote-kill-buffers t) (denote-rename-buffer-mode 1)) #+end_src I also frequently take "daily" notes, and ~denote-journal~ is a great integration for that. #+begin_src emacs-lisp :tangle "init.el" (use-package denote-journal :ensure (:wait t) (:host github :repo "protesilaos/denote-journal") :config (setq denote-journal-title-format 'day-date-month-year) (setq denote-journal-directory (expand-file-name "dailies" denote-directory)) (setq denote-journal-keyword "daily")) #+end_src We also load ~denote-org~, a package containing a handful of useful functions for ~denote~ notes that happen to be ~org~ files. #+begin_src emacs-lisp :tangle "init.el" (use-package denote-org :ensure (:wait (:host github :repo "protesilaos/denote-org"))) #+end_src I have this helper function to bring any new file into my notes directory. #+begin_src emacs-lisp :tangle "init.el" (defun denote-add-to-vault () (interactive) (let* ((current-buffer (current-buffer)) (original-content (buffer-string)) (original-filename (buffer-file-name)) (vault-directory (expand-file-name "vault" denote-directory))) (save-buffer) (call-interactively #'denote-rename-file) (let ((new-filename (buffer-file-name))) (copy-file new-filename (expand-file-name (file-name-nondirectory new-filename) vault-directory) t) (with-current-buffer current-buffer (erase-buffer) (insert original-content) (save-buffer))))) #+end_src ** Spell checking I use ~jinx~ for spell checking. #+begin_src emacs-lisp :tangle "init.el" (use-package jinx :bind (("M-$" . jinx-correct) ("C-M-$" . jinx-languages)) :hook (text-mode . jinx-mode) :init ;; Fix warnings with corfu, see (customize-set-variable 'text-mode-ispell-word-completion nil) (setq text-mode-ispell-word-completion nil) :config (add-to-list 'jinx-exclude-faces '(tex-mode font-lock-constant-face)) (setq jinx-languages "en_US fr_FR")) #+end_src ** Finding synonyms #+begin_src emacs-lisp :tangle "init.el" (use-package powerthesaurus :ensure t) #+end_src * Productivity and agenda ** Org-ql First of all, I use ~org-ql~ to make my agenda faster, and also express custom agenda views more easily. #+begin_src emacs-lisp :tangle "init.el" (use-package org-ql :ensure t) #+end_src For some reason, ~org-ql~ is freaking out when there is no org header in a file, so I silence the warnings. #+begin_src emacs-lisp :tangle "init.el" (use-package org-ql :after org-ql :ensure nil :config (defun my-org-ql--select-no-logging-advice (orig-fun &rest args) "Advice to prevent org-ql--select from logging." (let ((inhibit-message t) (message-log-max nil)) (apply orig-fun args))) (advice-add 'org-ql--select :around #'my-org-ql--select-no-logging-advice)) #+end_src ** Agenda configuration To add new tasks to my agenda, I first use a capture template to generally write the task to ~todo.org~. #+begin_src emacs-lisp :tangle "init.el" (use-package emacs :ensure nil :demand t :after denote-org :config (global-set-key "\C-ca" 'org-agenda) (setq org-agenda-files (append '("~/dev/todos/inbox.org" "~/dev/todos/these.org" "~/dev/todos/freelance.org" "~/dev/todos/perso.org" "~/dev/todos/oss.org") (list (denote-journal-path-to-new-or-existing-entry)))) (setq org-todo-keywords (quote ((sequence "TODO(t)" "|" "DONE(d)") (sequence "WAITING(w@/!)" "HOLD(h@/!)" "|" "CANCELLED(c@/!)" "PHONE" "MEETING")))) (setq org-use-fast-todo-selection t) (setq org-treat-S-cursor-todo-selection-as-state-change nil) (global-set-key (kbd "C-c c") 'org-capture) (setq org-capture-templates (quote (("t" "todo" entry (file "~/dev/todos/inbox.org") "* TODO %?\n\n%a\n" :clock-in t :clock-resume t) ("w" "org-protocol" entry (file "~/dev/todos/inbox.org") "* TODO Review %c\n%U\n" :immediate-finish t) ("m" "Meeting" entry (file "~/dev/todos/inbox.org") "* MEETING with %? :MEETING:\n%U" :clock-in t :clock-resume t) ("p" "Phone call" entry (file "~/dev/todos/inbox.org") "* PHONE %? :PHONE:\n%U" :clock-in t :clock-resume t) ("h" "Habit" entry (file "~/dev/todos/inbox.org") "* NEXT %?\n%U\n%a\nSCHEDULED: %(format-time-string \"%<<%Y-%m-%d %a .+1d/3d>>\")\n:PROPERTIES:\n:STYLE: habit\n:REPEAT_TO_STATE: NEXT\n:END:\n"))))) #+end_src I will then refile the tasks in inbox to any of my agenda files sections. #+begin_src emacs-lisp :tangle "init.el" (use-package emacs :ensure nil :config (setq org-refile-targets (quote ((nil :maxlevel . 9) (org-agenda-files :maxlevel . 9)))) (setq org-refile-use-outline-path t) (setq org-outline-path-complete-in-steps nil) (setq org-refile-allow-creating-parent-nodes (quote confirm)) (defun luj/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 'luj/verify-refile-target)) #+end_src I have two main agenda views: - One that shows all the tasks in my agenda in a TODO state, that I use to schedule tasks that I should do on a given day; - One that is a specialized version that shows only what is scheduled and has been done on the given day. #+begin_src emacs-lisp :tangle "init.el" (use-package emacs :ensure nil :config (setq org-agenda-custom-commands '(("p" "Planner" ((org-ql-block '(and (todo "TODO") (tags "inbox")) ((org-ql-block-header "Inbox"))) (org-ql-block '(and (not (tags "inbox")) (not (todo "DONE")) (scheduled :to today)) ((org-ql-block-header "Already scheduled today"))) (org-ql-block '(or (and (not (tags "inbox")) (descendants (and (not (scheduled :to today))(todo "TODO"))) (not (scheduled :to today))) (and (not (tags "inbox")) (ancestors) (todo "TODO") (not (scheduled :to today)))) ((org-ql-block-header "Tasks to pick"))))) ("t" "Today tasks" ((org-ql-block '(or (and (not (tags "inbox")) (descendants (and (scheduled :to today)(todo "TODO")))) (and (not (tags "inbox")) (descendants (and (closed :to today)(todo "DONE")))) (and (not (tags "inbox")) (ancestors) (todo "TODO") (scheduled :to today)) (and (not (tags "inbox")) (ancestors) (todo "DONE") (closed :to today))) ((org-ql-block-header "Today tasks")))))))) #+end_src I have a little integration to remove empty lookbooks from my tasks. #+begin_src emacs-lisp :tangle "init.el" (use-package emacs :ensure nil :demand t :config ;; Remove empty clocks (setq org-clock-out-remove-zero-time-clocks t) ;; And remove empty logbooks (defun luj/remove-empty-drawer-on-clock-out () (interactive) (save-excursion (beginning-of-line 0) (org-remove-empty-drawer-at (point)))) (add-hook 'org-clock-out-hook 'luj/remove-empty-drawer-on-clock-out 'append)) #+end_src Whenever I mark a tasks as done, it gets archived in my daily note. #+begin_src emacs-lisp :tangle "init.el" (use-package org-archive :ensure nil :after denote-org :after org :config (setq org-archive-file-header-format nil) (setq org-archive-default-command #'org-archive-subtree-hierarchically) (defun org-archive-subtree-hierarchically (&optional prefix) (interactive "P") (let* ((fix-archive-p (and (not prefix) (not (use-region-p)))) (afile (car (org-archive--compute-location (or (org-entry-get nil "ARCHIVE" 'inherit) org-archive-location)))) (buffer (or (find-buffer-visiting afile) (find-file-noselect afile)))) (org-archive-subtree prefix) (when fix-archive-p (with-current-buffer buffer (goto-char (point-max)) (while (org-up-heading-safe)) (let* ((olpath (org-entry-get (point) "ARCHIVE_OLPATH")) (path (and olpath (split-string olpath "/"))) (level 1) tree-text) (when olpath (org-mark-subtree) (setq tree-text (buffer-substring (region-beginning) (region-end))) ;; we don’t want to see "Cut subtree" messages (let (this-command (inhibit-message t)) (org-cut-subtree)) (goto-char (point-min)) (save-restriction (widen) (-each path (lambda (heading) (if (re-search-forward (rx-to-string `(: bol (repeat ,level "*") (1+ " ") ,heading)) nil t) (org-narrow-to-subtree) (goto-char (point-max)) (unless (looking-at "^") (insert "\n")) (insert (make-string level ?*) " " heading "\n")) (cl-incf level))) (widen) (org-end-of-subtree t t) (org-paste-subtree level tree-text)))))))) (defun luj/archive-to-location (location) (interactive) (setq org-archive-location location) (setq org-archive-subtree-add-inherited-tags t) (org-archive-subtree-hierarchically)) (defun luj/org-roam-archive-todo-to-today () (interactive) (let ((org-archive-hook #'save-buffer) today-file pos) (save-window-excursion (denote-journal-new-or-existing-entry) (setq today-file (buffer-file-name)) (setq pos (point))) ;; Only archive if the target file is different than the current file (unless (equal (file-truename today-file) (file-truename (buffer-file-name))) (luj/archive-to-location (concat today-file "::* Tasks done"))))) (add-hook 'org-after-todo-state-change-hook (lambda () (when (member org-state '("DONE" "CANCELLED" "READ")) (luj/org-roam-archive-todo-to-today)))) (setq org-log-done 'time)) #+end_src ** Weekly reports I often want to generate weekly reports from the tasks I did since the last report. For this I parse all the archived notes in the dailies, extract all the tasks and filter them if they have been done for my PhD. #+begin_src emacs-lisp :tangle "init.el" (defun luj/file-greater-than-weekly-p (weekly-file file) (let* ((date-weekly (denote-retrieve-filename-identifier weekly-file)) (date-file (denote-retrieve-filename-identifier file))) (apply '<= (mapcar (lambda (x) (time-to-seconds (date-to-time x))) (list date-weekly date-file))))) (defun luj/dailies-greater-than-weekly (weekly-file files) (let ((partial (apply-partially 'luj/file-greater-than-weekly-p weekly-file))) (seq-filter partial files))) (defun luj/get-latest-weekly (&optional directory) (let* ((weekly-dir (expand-file-name "weeklies" denote-directory)) (files (directory-files (or directory weekly-dir)))) (car (last files)))) (defun luj/remove-drawers-task (task) (org-element-map task '(drawer property-drawer planning) (lambda (d) (org-element-extract-element d))) task) (defun luj/extract-tasks-file (file-path) (let* ((tree (with-temp-buffer (insert-file-contents file-path) (org-mode) (org-element-parse-buffer))) (tasks (org-element-map tree 'headline (lambda (elem) (when (and (string= "DONE" (org-element-property :todo-keyword elem)) (string= "these" (org-element-property :ARCHIVE_CATEGORY elem))) elem))))) (mapcar 'luj/remove-drawers-task tasks))) (defun luj/next-tuesday () (let ((human-time "next Tuesday")) (parse-time-string (with-temp-buffer (call-process "env" nil t nil "LC_ALL=C" "LANGUAGE=" "date" "-d" human-time) (or (bobp) (delete-backward-char 1)) (buffer-string))))) (defun generate-weekly-report () (interactive) (let* ((latest-weekly (luj/get-latest-weekly)) (daily-dir (expand-file-name "dailies" denote-directory)) (daily-files (directory-files daily-dir :match ".org")) (dailies-since-weekly (luj/dailies-greater-than-weekly latest-weekly daily-files)) (all-tasks (apply 'append (mapcar 'luj/extract-tasks-file dailies-since-weekly))) (next-weekly-date (format-time-string "%Y-%m-%d" (encode-time (luj/next-tuesday)))) (title (concat next-weekly-date " - Weekly report")) (weekly-dir (expand-file-name "weeklies" denote-directory)) (weekly-file (denote title '("weekly") 'org weekly-dir next-weekly-date)) ) (find-file weekly-file) (org-mode) (let* ((tree (org-element-parse-buffer)) (top-heading (org-element-create 'headline (list :title "Report" :level 1)))) (dolist (task all-tasks) (org-element-put-property task :level 2) (org-element-adopt-elements top-heading task)) (org-element-map tree 'org-data (lambda (document) (org-element-adopt-elements document top-heading))) (erase-buffer) (insert (org-element-interpret-data tree)) (write-file weekly-file) (message "Weekly report created")))) #+end_src * Mails I use ~notmuch~ to index and search through my large mail inboxes. #+begin_src emacs-lisp :tangle "init.el" (use-package notmuch :ensure t :init (setq notmuch-search-oldest-first nil) (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-hello-sections '(notmuch-hello-insert-saved-searches) notmuch-show-all-tags-list t) (setq notmuch-show-empty-saved-searches t) (setq notmuch-saved-searches `(( :name "telecom/inbox" :query "tag:inbox and tag:telecom and folder:telecom/INBOX" :sort-order newest-first :key ,(kbd "i")) ( :name "telecom/CGT" :query "tag:telecom and tag:inbox and tag:cgt" :sort-order newest-first :key ,(kbd "u")) ( :name "telecom/all" :query "tag:telecom and tag:inbox" :sort-order newest-first :key ,(kbd "o")) ( :name "work/inbox" :query "tag:work and folder:work/INBOX" :sort-order newest-first :key ,(kbd "p")) ( :name "work/RB" :query "tag:work and tag:inbox and tag:reproducible-builds" :sort-order newest-first :key ,(kbd "c")) ( :name "work/bluehats" :query "tag:work and tag:inbox and tag:bluehats" :sort-order newest-first :key ,(kbd "p")) ( :name "work/all" :query "tag:work and tag:inbox" :sort-order newest-first :key ,(kbd "p")))) (setq notmuch-search-result-format '(("date" . "%12s ") ("count" . "%-7s ") ("authors" . "%-20s ") ("subject" . "%-80s ") ("tags" . "(%s)"))) (setq notmuch-tree-result-format '(("date" . "%12s ") ("authors" . "%-20s ") ((("tree" . "%s") ("subject" . "%s")) . " %-80s ") ("tags" . "(%s)"))) (setq notmuch-search-oldest-first nil) (add-to-list 'meow-mode-state-list '(notmuch-hello-mode . motion)) (add-to-list 'meow-mode-state-list '(notmuch-search-mode . motion)) (add-to-list 'meow-mode-state-list '(notmuch-tree-mode . motion)) (add-to-list 'meow-mode-state-list '(notmuch-show-mode . motion)) (setq mail-specify-envelope-from t) (setq message-sendmail-envelope-from 'header) (setq mail-envelope-from 'header)) (use-package notmuch-addr :ensure t) (with-eval-after-load 'notmuch-address (notmuch-addr-setup)) (setq sendmail-program "msmtp" send-mail-function 'smtpmail-send-it message-sendmail-f-is-evil t message-sendmail-envelope-from 'header message-send-mail-function 'message-send-mail-with-sendmail) (setq notmuch-fcc-dirs '(("julien.malka@ens.fr" . "ens/Sent") ("julien@malka.sh" . "work/Sent") ("jmalka@dgnum.eu" . "dgnum/Sent") ("julien.malka@telecom-paris.fr" . "telecom/Sent"))) #+end_src I want the number of unread messages in my ~notmuch-hello~ buffer. #+begin_src emacs-lisp :tangle init.el (defun luj/notmuch-hello-insert-buttons (searches) "Insert buttons for SEARCHES. SEARCHES must be a list of plists each of which should contain at least the properties :name NAME :query QUERY and :count COUNT, where QUERY is the query to start when the button for the corresponding entry is activated, and COUNT should be the number of messages matching the query. Such a plist can be computed with `notmuch-hello-query-counts'." (let* ((widest (notmuch-hello-longest-label searches)) (tags-and-width (notmuch-hello-tags-per-line widest)) (tags-per-line (car tags-and-width)) (column-width (cdr tags-and-width)) (column-indent 0) (count 0) (reordered-list (notmuch-hello-reflect searches tags-per-line)) ;; Hack the display of the buttons used. (widget-push-button-prefix "") (widget-push-button-suffix "")) ;; dme: It feels as though there should be a better way to ;; implement this loop than using an incrementing counter. (mapc (lambda (elem) ;; (not elem) indicates an empty slot in the matrix. (when elem (when (> column-indent 0) (widget-insert (make-string column-indent ? ))) (let* ((name (plist-get elem :name)) (query (plist-get elem :query)) (query-unread (concat query " and tag:unread")) (count-unread (string-to-number (notmuch-saved-search-count query-unread))) (oldest-first (cl-case (plist-get elem :sort-order) (newest-first nil) (oldest-first t) (otherwise notmuch-search-oldest-first))) (exclude (cl-case (plist-get elem :excluded) (hide t) (show nil) (otherwise notmuch-search-hide-excluded))) (search-type (plist-get elem :search-type)) (msg-count (plist-get elem :count))) (widget-insert (format "%8s(%2s) " (notmuch-hello-nice-number msg-count) (if (>= count-unread 100) "xx" (notmuch-hello-nice-number count-unread)))) (widget-create 'push-button :notify #'notmuch-hello-widget-search :notmuch-search-terms query :notmuch-search-oldest-first oldest-first :notmuch-search-type search-type :notmuch-search-hide-excluded exclude name) (setq column-indent (1+ (max 0 (- column-width (length name))))))) (cl-incf count) (when (eq (% count tags-per-line) 0) (setq column-indent 0) (widget-insert "\n"))) reordered-list) ;; If the last line was not full (and hence did not include a ;; carriage return), insert one now. (unless (eq (% count tags-per-line) 0) (widget-insert "\n")))) (advice-add 'notmuch-hello-insert-buttons :override #'luj/notmuch-hello-insert-buttons) #+end_src * Programming ** Git adventures ~Emacs~ has an amazing ~git~ client available as the ~magit~ package; it replaces all my ~git~ usage. ~Magit~ depends on a more recent version of ~transient~ than the one my ~emacs~ comes with, so we fetch it. #+begin_src emacs-lisp :tangle "init.el" (use-package transient :ensure t :defer t :config (setq transient-show-popup 0.5)) (use-package magit :ensure t :bind ("C-c d" . magit-discard) ("C-c g" . magit-status) :config (setq magit-diff-refine-hunk 'all) (setq git-commit-style-convention-checks '(non-empty-second-line overlong-summary-line)) (with-eval-after-load 'magit (setq magit-format-file-function #'magit-format-file-nerd-icons))) #+end_src I regularly work within ~nixpkgs~, a huge repository that can make ~git~ and consequently ~magit~ slow. Let's fix it by disabling features for this repository. #+begin_src emacs-lisp :tangle "init.el" (dir-locals-set-class-variables 'huge-git-repository '((nil . ((magit-refresh-buffers . nil) (magit-revision-insert-related-refs . nil))) (magit-status-mode . ((eval . (magit-disable-section-inserter 'magit-insert-tags-header)) (eval . (magit-disable-section-inserter 'magit-insert-recent-commits)) (eval . (magit-disable-section-inserter 'magit-insert-unpushed-to-pushremote)) (eval . (magit-disable-section-inserter 'magit-insert-unpushed-to-upstream-or-recent)) (eval . (magit-disable-section-inserter 'magit-insert-unpulled-from-pushremote)) (eval . (magit-disable-section-inserter 'magit-insert-unpulled-from-upstream)) )))) (dir-locals-set-directory-class "/home/julien/dev/nixpkgs/" 'huge-git-repository) #+end_src In order to add some integration with common ~git~ forges to open pull request, etc directly from ~emacs~, I also install ~forge~. #+begin_src emacs-lisp :tangle "init.el" (use-package forge :ensure t :after magit) #+end_src ** Projects ~project.el~ provide a lot of goodies to work on specific software projects. #+begin_src emacs-lisp :tangle "init.el" (use-package project :bind (("C-c p p" . project-switch-project) ("C-c p f" . project-find-file) ("C-c p b" . consult-project-buffer) ("C-c p c" . project-compile) ("C-c p d" . project-dired)) :custom (project-vc-merge-submodules nil) :config (add-to-list 'project-switch-commands '(project-dired "Dired")) (project-remember-projects-under "/home/julien/dev")) #+end_src ** Proced Is the ~emacs~ equivalent to ~htop~. #+begin_src emacs-lisp :tangle init.el (use-package proced :ensure nil :commands (proced) :config (setq proced-auto-update-flag 'visible) (setq proced-enable-color-flag t) (setq proced-auto-update-interval 5) (setq proced-descend t)) #+end_src ** Compilation The compilation buffer is very useful to launch ephemeral processes. #+begin_src emacs-lisp :tangle "init.el" (use-package ansi-color :hook (compilation-filter . ansi-color-compilation-filter)) (setq compilation-scroll-output t) #+end_src ** Eat (emulate a terminal) ~eat~ is a terminal emulator implemented directly in emacs-lisp. Apparently, less susceptible to flickering than the others out there. #+begin_src emacs-lisp :tangle "init.el" (use-package eat :ensure (:type git :repo "https://codeberg.org/vifon/emacs-eat.git" :branch "fish-integration" :files (:defaults "terminfo" "integration") :config (add-hook 'eshell-load-hook #'eat-eshell-mode))) (defun eat-meow-setup () (add-hook 'meow-normal-mode-hook 'eat-emacs-mode nil t) (add-hook 'meow-insert-mode-hook (lambda () (goto-char (point-max)) (eat-char-mode)) nil t)) (with-eval-after-load "eat" ;; Replace semi-char mode with emacs mode (advice-add 'eat-semi-char-mode :after 'eat-emacs-mode) (add-hook 'eat-mode-hook 'eat-emacs-mode) (add-hook 'eat-mode-hook 'eat-meow-setup)) #+end_src ** Direnv ~Direnv~ is at the core of my software engineering workflows. I use ~envrc~ over ~direnv.el~ because it has an integration with ~TRAMP~. #+begin_src emacs-lisp :tangle "init.el" (use-package envrc :ensure t :config (envrc-global-mode 1)) #+end_src ** LSP integration with eglot ~eglot~ is the builtin LSP client inside ~emacs~. It works well, but I add some customization to make it faster. #+begin_src emacs-lisp :tangle "init.el" (use-package eglot) #+end_src Including using ~eglot-booster~, that bufferize the LSP output instead of ~emacs~ and parses the ~json~. #+begin_src emacs-lisp :tangle "init.el" (use-package eglot-booster :ensure (:host github :repo "jdtsmith/eglot-booster") :after eglot :config (eglot-booster-mode)) #+end_src I don't like how I must open another buffer to get documentation, so let's go with ~eldoc-box~ #+begin_src emacs-lisp :tangle "init.el" (use-package eldoc-box :ensure t :config (eldoc-box-hover-at-point-mode) (add-hook 'eglot-managed-mode-hook #'eldoc-box-hover-mode t) ) #+end_src ** Programming modes #+begin_src emacs-lisp :tangle "init.el" (use-package prog-mode :ensure nil :hook (prog-mode . visual-line-mode) (prog-mode . display-line-numbers-mode)) #+end_src *** Nix #+begin_src emacs-lisp :tangle "init.el" (use-package nix-mode :ensure t :after eglot :hook (nix-mode . eglot-ensure) :config (add-to-list 'eglot-server-programs '(nix-mode . ("nixd")))) #+end_src *** Python #+begin_src emacs-lisp :tangle "init.el" (use-package python-mode :ensure t :after eglot :hook (python-mode . eglot-ensure)) #+end_src *** Rust #+begin_src emacs-lisp :tangle "init.el" (use-package rust-mode :ensure t :after eglot :hook (rust-mode . eglot-ensure)) #+end_src *** Latex Integrating LaTeX with AUCTex which is apparently an IDE-like for TeX files. #+begin_src emacs-lisp :tangle "init.el" (use-package auctex :ensure t :defer t :mode ("\\.tex\\'" . latex-mode) :config (setq TeX-auto-save t) (setq TeX-parse-self t) (setq-default TeX-master nil)) #+end_src #+begin_src emacs-lisp :tangle "init.el" (use-package latex-preview-pane :ensure t :custom (latex-preview-pane-multifile-mode 'AucTeX) :config (latex-preview-pane-enable) (setq latex-preview-pane-use-frame t)) #+end_src *** PDF Well, why not use it for PDF? Also integrate with /LaTeX/ etc. #+begin_src emacs-lisp :tangle "init.el" (use-package pdf-tools :ensure t :init (pdf-tools-install) :config (setq TeX-view-program-selection '((output-pdf "PDF Tools")) TeX-view-program-list '(("PDF Tools" TeX-pdf-tools-sync-view)) TeX-source-correlate-start-server t) (add-hook 'TeX-after-compilation-finished-functions #'TeX-revert-document-buffer) (setq pdf-view-use-scaling t) (setq pdf-view-resize-factor 1.05) :bind (:map pdf-view-mode-map ("" . pdf-view-previous-page-command) ("" . pdf-view-next-page-command))) #+end_src *** Flycheck #+begin_src emacs-lisp :tangle "init.el" (use-package flycheck :ensure t :bind (("C-c e e" . consult-flycheck) ("C-c e a" . eglot-code-actions)) :init (global-flycheck-mode)) (use-package flycheck-eglot :ensure t :after (flycheck eglot) :config (global-flycheck-eglot-mode 1)) (use-package consult-flycheck :ensure t) (use-package eglot-ltex :ensure (:host github :repo "emacs-languagetool/eglot-ltex") :init (setq eglot-ltex-server-path "ltex-cli" eglot-ltex-communication-channel 'stdio)) #+end_src *** Markdown #+begin_src emacs-lisp :tangle "init.el" (use-package markdown-mode :mode ("README\\.md\\'" . gfm-mode) :init (setq markdown-command "multimarkdown")) #+end_src *** Haskell #+begin_src emacs-lisp :tangle "init.el" (use-package haskell-mode :ensure t :after eglot :hook (haskell-mode . eglot-ensure)) #+end_src *** Typst #+begin_src emacs-lisp :tangle "init.el" (use-package typst-ts-mode :after eglot :ensure (:type git :repo "https://codeberg.org/meow_king/typst-ts-mode.git" :branch "main") :hook (typst-ts-mode . eglot-ensure) :config (add-to-list 'eglot-server-programs '(typst-ts-mode . ("tinymist")))) (use-package websocket :ensure t) (use-package typst-preview :after websocket :ensure (:host github :repo "havarddj/typst-preview.el")) #+end_src