emacs-config/ReadMe.org
2025-04-03 11:00:08 +02:00

45 KiB
Raw Permalink Blame History

Luj's literate Emacs configuration

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 Protesilaos Stavrou's emacs configuration.

  (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))

We can then apply the macro to make frame adjustments only in desktop environments.

  (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)))

I also adjust the frame color to use the same background as Catpuccin mocha, which is my theme of choice.

  (set-face-attribute 'default nil :background "#1e1e2e" :foreground "#ffffff")

Let's setup also additional frame settings that are nice to have.

  (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)

Package manager bootstrap

I am using the elpaca package manager, that needs to be bootstrapped because it is not included in emacs.

  (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)

We now integrate elpaca with use-package:

  (elpaca elpaca-use-package
    (elpaca-use-package-mode))

General quality of life improvements

Remove littering

Prevent emacs from storing garbage everywhere in my repos.

    (use-package no-littering
      :ensure t
      :demand t
      :config
      (no-littering-theme-backups))

Save history and editing position

  (use-package savehist
    :init
    (savehist-mode))

  (save-place-mode 1)

Accept tangling this file as a hook in a local variable

  (add-to-list 'safe-local-eval-forms
             '(add-hook 'after-save-hook 'org-babel-tangle nil t))

No impure customizations

(setq custom-file (make-temp-file "emacs-custom-"))

Auto-reverting buffers

  (use-package autorevert
    :ensure nil
    :hook (after-init . global-auto-revert-mode)
    :config
    (setq auto-revert-verbose t))

Which key

We sometimes need a little help to figure out keybindings, using which-key for this.

  (use-package which-key
    :ensure t
    :config
    (which-key-mode))

Misc changes

  (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))

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.

  (use-package catppuccin-theme
    :ensure t
    :config
    (load-theme 'catppuccin :no-confirm))

I also configure modus-operandi as my backup theme when I need a light theme.

  (use-package modus-themes
    :ensure t)

Solaire mode

Allows to have darker colors for non editing buffers, a bit like an IDE.

  (use-package solaire-mode
    :ensure t
    :config
    (solaire-global-mode +1))

Mode line

I use the doom-modeline, it's nice and clean.

  (use-package doom-modeline :ensure t
  :init
  (doom-modeline-mode 1))

Nerd icons

Nerd icons are a nice addition to the UI, I try to enable them wherever I can.

  (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))

Line highlighting

  (use-package lin
    :ensure (:host github :repo "protesilaos/lin")
    :config
    (setq global-hl-line-sticky-flag t)
    (lin-global-mode)
    (global-hl-line-mode))

Fonts

  (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))))

Editing facilities

Meow

I use meow as a modal editing framework. It is less invasive than evil-mode for example.

  (defun meow-setup ()
    (meow-motion-overwrite-define-key
     '("s-SPC" . meow-keypad)
     '("j" . meow-next)
     '("k" . meow-prev)
     '("<escape>" . 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)
     '("<escape>" . ignore)))

  (use-package meow
    :ensure (:wait t)
    :demand t
    :custom
    (meow-keypad-leader-dispatch "C-c")
    :config
    (meow-global-mode 1)
    (meow-setup))

Numbers for region extension sometimes slow down my editing workflow, so I disable it.

  (use-package meow
    :ensure nil
    :config
    (setq meow-expand-hint-remove-delay 0)
    (setq meow-expand-hint-counts nil))

Avy

Avy is a nice addition to meow: it allows to jump to specific visible places in the frame using key chords.

  (use-package avy
    :ensure t
    :bind
    ("C-c j" . avy-goto-char))

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.

   (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))

Orderless

With orderless we can get fuzzy searches in the minibuffer.

  (use-package orderless
    :ensure t
    :custom
    (completion-styles '(orderless basic))
    (completion-category-overrides '((file (styles basic partial-completion)))))

Marginalia

Setup contextual information on minibuffer options.

  (use-package marginalia
    :ensure t
    :init
    (marginalia-mode))

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.

  (use-package consult
    :ensure t
    :bind
    (("C-c b" . consult-buffer)
     ("C-x C-b" . consult-buffer)))

In buffer completions

Corfu & Cape

Corfu is the equivalent of vertico for in buffer completions, for example LSP suggestions, but not limited to that.

  (use-package corfu
    :ensure t
    :config
    (setq corfu-auto t)
    :init
    (global-corfu-mode))

Cape is a completion provider for corfu. I mostly use it to complete filenames, mostly.

  (use-package cape
    :ensure t
    :bind ("M-p" . cape-prefix-map)
    :init
    (add-hook 'completion-at-point-functions #'cape-file))

Buffer and window management

First I define some keybindings that are helpful in order to make window management more straightforward.

  (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 <right>" . windmove-right)
  	("w <left>" . windmove-left)
  	("w <up>" . windmove-up)
  	("w <down>" . windmove-down)
  	("w x" . delete-window)
  	("w w" . other-window)
  	("w o" . ace-window)))

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.

  (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))

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.

  (use-package ace-window
    :ensure t
    :config
    (ace-window-display-mode))

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.

  (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))

Org modern

To make org-mode a bit cleaner.

  (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))

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.

  (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))

I also frequently take "daily" notes, and denote-journal is a great integration for that.

  (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"))

We also load denote-org, a package containing a handful of useful functions for denote notes that happen to be org files.

  (use-package denote-org
    :ensure (:wait (:host github :repo "protesilaos/denote-org")))

I have this helper function to bring any new file into my notes directory.

  (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)))))

Spell checking

I use jinx for spell checking.

  (use-package jinx
    :bind (("M-$" . jinx-correct)
  	 ("C-M-$" . jinx-languages))
    :hook
    (text-mode . jinx-mode)
    :init
    ;; Fix warnings with corfu, see <https://github.com/minad/corfu/discussions/457>
    (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"))

Finding synonyms

  (use-package powerthesaurus
    :ensure t)

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.

  (use-package org-ql
    :ensure t)

For some reason, org-ql is freaking out when there is no org header in a file, so I silence the warnings.

  (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))

Agenda configuration

To add new tasks to my agenda, I first use a capture template to generally write the task to todo.org.

  (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")))))

I will then refile the tasks in inbox to any of my agenda files sections.

  (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))

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.
  (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"))))))))

I have a little integration to remove empty lookbooks from my tasks.

  (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))

Whenever I mark a tasks as done, it gets archived in my daily note.

  (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 dont 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))

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.

  (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"))))

Mails

I use notmuch to index and search through my large mail inboxes.

   (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")))

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.

  (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)))

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.

  (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)

In order to add some integration with common git forges to open pull request, etc directly from emacs, I also install forge.

  (use-package forge
    :ensure t
    :after magit)

Projects

project.el provide a lot of goodies to work on specific software projects.

  (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"))

Proced

Is the emacs equivalent to htop.

  (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))

Compilation

The compilation buffer is very useful to launch ephemeral processes.

  (use-package ansi-color
    :hook (compilation-filter . ansi-color-compilation-filter))
  (setq compilation-scroll-output t)

Eat (emulate a terminal)

eat is a terminal emulator implemented directly in emacs-lisp. Apparently, less susceptible to flickering than the others out there.

  (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))

Direnv

Direnv is at the core of my software engineering workflows. I use envrc over direnv.el because it has an integration with TRAMP.

  (use-package envrc
    :ensure t
    :config
    (envrc-global-mode 1))

LSP integration with eglot

eglot is the builtin LSP client inside emacs. It works well, but I add some customization to make it faster.

  (use-package eglot
    :ensure nil
    :config
    (fset #'jsonrpc--log-event #'ignore)
    (setq eglot-events-buffer-size 0))

Including using eglot-booster, that bufferize the LSP output instead of emacs and parses the json.

  (use-package eglot-booster
    :ensure (:host github :repo "jdtsmith/eglot-booster")
    :after eglot
    :config (eglot-booster-mode))

Programming modes

  (use-package prog-mode
    :ensure nil
    :hook
    (prog-mode . visual-line-mode)
    (prog-mode . display-line-numbers-mode))

Nix

  (use-package nix-mode
    :ensure t
    :after eglot
    :hook
    (nix-mode . eglot-ensure)
    :config
    (add-to-list 'eglot-server-programs '(nix-mode . ("nixd"))))

Python

  (use-package python-mode
    :ensure t
    :after eglot
    :hook
    (python-mode . eglot-ensure))

Rust

  (use-package rust-mode
    :ensure t
    :after eglot
    :hook
    (rust-mode . eglot-ensure))

Latex

Integrating LaTeX with AUCTex which is apparently an IDE-like for TeX files.

  (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))
  (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))

PDF

Well, why not use it for PDF? Also integrate with LaTeX etc.

  (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
                ("<left>" . pdf-view-previous-page-command)
                ("<right>" . pdf-view-next-page-command)))

Flycheck

  (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))

Markdown

  (use-package markdown-mode
    :mode ("README\\.md\\'" . gfm-mode)
    :init (setq markdown-command "multimarkdown"))

Haskell

  (use-package haskell-mode
    :ensure t
    :after eglot
    :hook
    (haskell-mode . eglot-ensure))

Typst

  (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"))