From a295279bac71a706fc3b8b83e3b449c11da44b61 Mon Sep 17 00:00:00 2001 From: Julien Malka Date: Thu, 3 Apr 2025 11:00:08 +0200 Subject: [PATCH] init --- .dir-locals.el | 1 + .gitignore | 12 + ReadMe.org | 1538 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1551 insertions(+) create mode 100644 .dir-locals.el create mode 100644 .gitignore create mode 100644 ReadMe.org diff --git a/.dir-locals.el b/.dir-locals.el new file mode 100644 index 0000000..90fc409 --- /dev/null +++ b/.dir-locals.el @@ -0,0 +1 @@ +((nil . ((eval . (add-hook 'after-save-hook 'org-babel-tangle nil t))))) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..25840ae --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +etc +var +straight +elpaca +elpa +eln-cache +init.el +early-init.el +customizations.el +projects +fontaine-latest-state.eld +tree-sitter \ No newline at end of file diff --git a/ReadMe.org b/ReadMe.org new file mode 100644 index 0000000..0aeea06 --- /dev/null +++ b/ReadMe.org @@ -0,0 +1,1538 @@ +#+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 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)) +#+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 emacs + :ensure nil + :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-to-list '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 + :defer t + :config + (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 + + +* 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 + :ensure nil + :config + (fset #'jsonrpc--log-event #'ignore) + (setq eglot-events-buffer-size 0)) +#+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 + + +** 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