1641 lines
49 KiB
Org Mode
1641 lines
49 KiB
Org Mode
#+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"
|
||
;;; -*- lexical-binding: t -*-
|
||
(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 '(20250621))
|
||
(defvar elpaca-installer-version 0.11)
|
||
(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)
|
||
(let ((load-source-file-function nil)) (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
|
||
|
||
** Long lines...
|
||
|
||
... Are painful
|
||
|
||
#+begin_src emacs-lisp :tangle "init.el"
|
||
(setq-default bidi-paragraph-direction 'left-to-right)
|
||
(setq bidi-inhibit-bpa t)
|
||
(global-so-long-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 165)
|
||
(jumbo
|
||
:inherit medium
|
||
:default-height 260)
|
||
(dell-docked
|
||
:inherit medium
|
||
:default-height 200)
|
||
|
||
(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)
|
||
'("<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))
|
||
#+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 <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)))
|
||
#+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 <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"))
|
||
#+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
|
||
("<left>" . pdf-view-previous-page-command)
|
||
("<right>" . 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
|