Skip to content

Latest commit

 

History

History
4961 lines (3508 loc) · 150 KB

dotemacs.org

File metadata and controls

4961 lines (3508 loc) · 150 KB

Emacs literate configuration

Documentation

Forewords

This document is a literate configuration for GNU Emacs written in org format. It cannot be used directly and needs to be transformed to extract all the different code blocks into a single configuration file. This is done using the org-babel-tangle function that “extract the bodies of all source code blocks from the current file into their own source-specific files”. You can try by yourself by typing the sequence: C-c C-v t (org-babel-execute-subtree).

However, on my Emacs (28.1), there seems to be a bug that prevent a proper tangling. You’ll thus need to go to the Configuration section and execute it using C-c C-v s to fix this bug before tangling the file.

Each subsection may be tagged with:

  • :BINDING: The section defines some key binding
  • :HOOK: The section installs some hook
  • :ADVICE: The section installs some advice
  • :FACE: The section modifies some face
  • :MODE: The section activates a mode
  • :DEFER: The content of the section is deferred (lazy load)
  • :PERSONAL: The content is personal and should be adapted
  • :INACTIVE: The content of the section won’t be exported
  • :BUGFIX: The section contains (temporary) bug fix code
  • :OTHER: The section will be exported to a different file than default
  • :TIMER: The section install a timer
  • :OS: The section contains some system specific code

There is also a configuration mode defined in the section below to ease the writing.

When tangled, the process will create the file ~/.emacs.org/init.el ~/.emacs.org/early-init.el (from the Early init section). You can either copy these files to your emacs.d or you can use chemacs2 and add .emacs.org as an alternative profile.

Note also that the first time you will start emacs using this configuration, it will take a long time because a lof of packages will be dowloaded, installed and possibly compiled (if you use Emacs 28 and above). At next restart, the process should be very fast (650 ms on my machine using OSX).

To sort

(setq-default
 ad-redefinition-action 'accept         ; Silence warnings for redefinition
 custom-unlispify-menu-entries nil      ; Prefer kebab-case for titles
 custom-unlispify-tag-names nil         ; Prefer kebab-case for symbols
(put 'downcase-region 'disabled nil)    ; Enable downcase-region
(put 'upcase-region 'disabled nil)      ; Enable upcase-region
 native-comp-async-report-warnings-errors 'silent ; Skip compilation error buffers
 read-process-output-max (* 1024 1024)  ; Increase read size per process

To do [5/5]

  • [X] Tangle file when saving (org-auto-tangle)
  • [X] Hitting `space` on top node should open the node (instead of folding)
  • [X] Dim inactive entries in the sidebar
  • [X] Shift-tab to toggle sidebar (all entries)
  • [X] Filter sidebar (`f` key)

Core

Header

This will generate a header at the top of the tangled file to indicate it is generated and is not meant to be modified directly.

;; -*- lexical-binding: t -*-
;; This file has been generated from dotemacs.org file. DO NOT EDIT.
;; Sources are available from https://github.com/rougier/dotemacs

;; Copyright (C) 2022 Nicolas P. Rougier

;; This file is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 3, or (at your option)
;; any later version.

;; This file is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; For a full copy of the GNU General Public License
;; see <https://www.gnu.org/licenses/>.
(defvar my/init-start-time (current-time) "Time when init.el was started")
(defvar my/section-start-time (current-time) "Time when section was started")
(defun my/report-time (section)
  (message "%-36s %.2fs"
           (concat section " " "section time: ")
           (float-time (time-subtract (current-time) my/section-start-time))))
(message "---------------------------------------------------------------")

Early init

This code is meant to go to the early-init.el file.

(setq
 site-run-file nil                         ; No site-wide run-time initializations. 
 inhibit-default-init t                    ; No site-wide default library
 gc-cons-threshold most-positive-fixnum    ; Very large threshold for garbage
                                           ; collector during init
 package-enable-at-startup nil)            ; We'll use straight.el

(setq native-comp-eln-load-path
      (list (expand-file-name "eln-cache" user-emacs-directory)))

;; Reset garbage collector limit after init process has ended (8Mo)
(add-hook 'after-init-hook
          #'(lambda () (setq gc-cons-threshold (* 8 1024 1024))))

Packages management

Using straight.el for package management and disable checking (for speedup).

(setq straight-check-for-modifications nil)
(defvar bootstrap-version)
(let ((bootstrap-file
       (expand-file-name "straight/repos/straight.el/bootstrap.el" user-emacs-directory))
      (bootstrap-version 5))
  (unless (file-exists-p bootstrap-file)
    (with-current-buffer
        (url-retrieve-synchronously
         "https://raw.githubusercontent.com/raxod502/straight.el/develop/install.el"
         'silent 'inhibit-cookies)
      (goto-char (point-max))
      (eval-print-last-sexp)))
  (load bootstrap-file nil 'nomessage))

Library and theme load paths with a special case for mu4e (you may need to adapt this path).

(add-to-list 'load-path
             (expand-file-name "lisp" user-emacs-directory))
(add-to-list 'custom-theme-load-path
             (expand-file-name "theme" user-emacs-directory))
(add-to-list 'load-path 
  "/usr/local/Cellar/mu/1.6.10/share/emacs/site-lisp/mu/mu4e/mu4e.el")

Packages installation

Packages used in this specific configuration. You may want to adapt this list. The first time this sequence will be executed, it will take a long time. Then, at next restart, it should be very fast (less than 1 second because it won’t install anything basically).

(setq package-list
      '(cape                ; Completion At Point Extensions
        orderless           ; Completion style for matching regexps in any order
        vertico             ; VERTical Interactive COmpletion
        marginalia          ; Enrich existing commands with completion annotations
        consult             ; Consulting completing-read
        corfu               ; Completion Overlay Region FUnction
        deft                ; Quickly browse, filter, and edit plain text notes
        elpher              ; A friendly gopher and gemini client 
        elfeed              ; Emacs Atom/RSS feed reader
        elfeed-org          ; Configure elfeed with one or more org-mode files
        f                   ; Modern API for working with files and directories
        citar               ; Citation-related commands for org, latex, markdown
        citeproc            ; A CSL 1.0.2 Citation Processor
        flyspell-correct-popup ; Correcting words with flyspell via popup interface
        flyspell-popup      ; Correcting words with Flyspell in popup menus
        guess-language      ; Robust automatic language detection
        helpful             ; A better help buffer
        htmlize             ; Convert buffer text and decorations to HTML
        mini-frame          ; Show minibuffer in child frame on read-from-minibuffer
        imenu-list          ; Show imenu entries in a separate buffer
        magit               ; A Git porcelain inside Emacs.
        markdown-mode       ; Major mode for Markdown-formatted text
        multi-term          ; Managing multiple terminal buffers in Emacs.
        pinentry            ; GnuPG Pinentry server implementation
        use-package         ; A configuration macro for simplifying your .emacs
        vc-backup           ; VC backend for versioned backups
        yaml-mode           ; YAML mode
        consult-recoll      ; Consult interface for recoll query
        org-auto-tangle     ; Tangle org file when it is saved
        exec-path-from-shell; Get environment variables such as $PATH from the shell 
        which-key))         ; Display available keybindings in popup

;; Install packages that are not yet installed
(dolist (package package-list)
  (straight-use-package package))

;; Special case for pdf-tools that has recently (2022) changed maintainer
(straight-use-package
 '(pdf-tools :type git :host github :repo "vedang/pdf-tools"))

;; Denote not yet on ELPA (2022-06-19)
(straight-use-package
 '(denote :type git :host github :repo "protesilaos/denote"))

My personal packages

;; Display org properties in the agenda buffer (modified version)
(straight-use-package
 '(org-agenda-property :type git :host github :repo "Malabarba/org-agenda-property"
                       :fork (:host github :repo "rougier/org-agenda-property")))

;; NANO splash
(straight-use-package
 '(nano-splash :type git :host github :repo "rougier/nano-splash"))

;; NANO theme
(straight-use-package
 '(nano-theme :type git :host github :repo "rougier/nano-theme"))

;; NANO modeline
(straight-use-package
 '(nano-modeline :type git :host github :repo "rougier/nano-modeline"))

;; NANO agenda
(straight-use-package
 '(nano-agenda :type git :host github :repo "rougier/nano-agenda"))

;; NANO agenda
(straight-use-package
 '(minibuffer-header :type git :host github :repo "rougier/minibuffer-header"))

;; SVG tags, progress bars & icons
(straight-use-package
 '(svg-lib :type git :host github :repo "rougier/svg-lib"))

;; Replace keywords with SVG tags
(straight-use-package
 '(svg-tag-mode :type git :host github :repo "rougier/svg-tag-mode"))

;; Dashboard for mu4e
(straight-use-package
 '(mu4e-dashboard :type git :host github :repo "rougier/mu4e-dashboard"))

;; Folding mode for mu4e
(straight-use-package
 '(mu4e-folding :type git :host github :repo "rougier/mu4e-folding"))

;; Relative date formatting
(straight-use-package
 '(relative-date :type git :host github :repo "rougier/relative-date"))

;; org imenu
(straight-use-package
 '(org-imenu :type git :host github :repo "rougier/org-imenu"))

;; pdf-drop-mode
(straight-use-package
 '(pdf-drop-mode :type git :host github :repo "rougier/pdf-drop-mode"))


;; Bilbliography manager in org mode
(straight-use-package
  '(org-bib :type git :host github :branch "org-imenu" :repo "rougier/org-bib-mode"))

Startup

Emacs does a lot of things at startup and here, we disable pretty much everything.

(setq-default
 inhibit-startup-screen t               ; Disable start-up screen
 inhibit-startup-message t              ; Disable startup message
 inhibit-startup-echo-area-message t    ; Disable initial echo message
 initial-scratch-message ""             ; Empty the initial *scratch* buffer
 initial-buffer-choice t)               ; Open *scratch* buffer at init

We’ll use the bind-key function (from use-package) for bindings. Then we can use describe-personal-keybindings to check for personal bindings.

;; (require 'bind-key)

Encoding

We tell emacs to use UTF-8 encoding as much as possible.

(set-default-coding-systems 'utf-8)     ; Default to utf-8 encoding
(prefer-coding-system       'utf-8)     ; Add utf-8 at the front for automatic detection.
(set-terminal-coding-system 'utf-8)     ; Set coding system of terminal output
(set-keyboard-coding-system 'utf-8)     ; Set coding system for keyboard input on TERMINAL
(set-language-environment "English")    ; Set up multilingual environment

Path

Extending executable for recollq program

(setenv "PATH" (concat (getenv "PATH") ":" "/Users/rougier/Applications/recoll.app/Contents/MacOS"))
(setq exec-path (append exec-path '("/Users/rougier/Applications/recoll.app/Contents/MacOS")))

(setenv "PATH" (concat (getenv "PATH") ":" "/Users/rougier/bin"))
(setq exec-path (append exec-path '("/Users/rougier/bin")))

Recovery

If Emacs or the computer crashes, you can recover the files you were editing at the time of the crash from their auto-save files. To do this, start Emacs again and type the command M-x recover-session. Here, we parameterize how files are saved in the background.

(setq auto-save-list-file-prefix ; Prefix for generating auto-save-list-file-name
      (expand-file-name ".auto-save-list/.saves-" user-emacs-directory)
      auto-save-default t        ; Auto-save every buffer that visits a file
      auto-save-timeout 20       ; Number of seconds between auto-save
      auto-save-interval 200)    ; Number of keystrokes between auto-saves

Backups

Emacs carefully copies the old contents to another file, called the “backup” file, before actually saving. Emacs makes a backup for a file only the first time the file is saved from a buffer. No matter how many times you subsequently save the file, its backup remains unchanged. However, if you kill the buffer and then visit the file again, a new backup file will be made. Here, we activate backup and parameterize the number of backups to keep.

(setq backup-directory-alist       ; File name patterns and backup directory names.
      `(("." . ,(expand-file-name "backups" user-emacs-directory)))
      make-backup-files t          ; Backup of a file the first time it is saved.
      vc-make-backup-files t       ; No backup of files under version contr
      backup-by-copying t          ; Don't clobber symlinks
      version-control t            ; Version numbers for backup files
      delete-old-versions t        ; Delete excess backup files silently
      kept-old-versions 6          ; Number of old versions to keep
      kept-new-versions 9          ; Number of new versions to keep
      delete-by-moving-to-trash t) ; Delete files to trash

;; Back
(require 'vc-backup)

Bookmarks

(setq bookmark-default-file (expand-file-name "bookmark" user-emacs-directory))

Recent files

50 Recents files with some exclusion (regex patterns).

(require 'recentf)

(setq recentf-max-menu-items 10
      recentf-max-saved-items 100
      recentf-exclude '("/Users/rougier/Documents/Mail.+"
                        "/Users/rougier/Documents/Notes.+"
                        ))

(let (message-log-max)
  (recentf-mode 1))

History

Remove text properties for kill ring entries (see https://emacs.stackexchange.com/questions/4187). This saves a lot of time when loading it.

(defun unpropertize-kill-ring ()
  (setq kill-ring (mapcar 'substring-no-properties kill-ring)))

(add-hook 'kill-emacs-hook 'unpropertize-kill-ring)

We save every possible history we can think of.

(require 'savehist)

(setq kill-ring-max 50
      history-length 50)

(setq savehist-additional-variables
      '(kill-ring
        command-history
        set-variable-value-history
        custom-variable-history   
        query-replace-history     
        read-expression-history   
        minibuffer-history        
        read-char-history         
        face-name-history         
        bookmark-history
        file-name-history))

 (put 'minibuffer-history         'history-length 50)
 (put 'file-name-history          'history-length 50)
 (put 'set-variable-value-history 'history-length 25)
 (put 'custom-variable-history    'history-length 25)
 (put 'query-replace-history      'history-length 25)
 (put 'read-expression-history    'history-length 25)
 (put 'read-char-history          'history-length 25)
 (put 'face-name-history          'history-length 25)
 (put 'bookmark-history           'history-length 25)

No duplicates in history

(setq history-delete-duplicates t)

Start history mode.

(let (message-log-max)
  (savehist-mode))

Cursor

Record cursor position from one session ot the other

(setq save-place-file (expand-file-name "saveplace" user-emacs-directory)
      save-place-forget-unreadable-files t)

(save-place-mode 1)

Customization

Since init.el will be generated from this file, we save customization in a dedicated file.

(setq custom-file (concat user-emacs-directory "custom.el"))

(when (file-exists-p custom-file)
  (load custom-file nil t))

Server

Server start.

(require 'server)

(unless (server-running-p)
  (server-start))

Benchmark

(my/report-time "Core")

Personal library

(setq my/section-start-time (current-time))

Some functions that are used throughout this configuration.

String related

A set of functions to join two strings such as to fit a given width. This will be used for displaying elfeed posts, privileging the right part (tag and feed).

(defun my/string-pad-right (len s)
  "If S is shorter than LEN, pad it on the right,
   if S is longer than LEN, truncate it on the right."

  (if (> (length s) len)
      (concat (substring s 0 (- len 1)) "")
    (concat s (make-string (max 0 (- len (length s))) ?\ ))))
(defun my/string-pad-left (len s)
  "If S is shorter than LEN, pad it on the left,
   if S is longer than LEN, truncate it on the left."
  
  (if (> (length s) len)
      (concat  "" (substring s (- (length s) len -1)))
    (concat (make-string (max 0 (- len (length s))) ?\ ) s)))
(defun my/string-join (len left right &optional spacing)
  "Join LEFT and RIGHT strings to fit LEN characters with at least SPACING characters 
between them. If len is negative, it is retrieved from current window width."

    (let* ((spacing (or spacing 3))
           (len (or len (window-body-width)))
           (len (if (< len 0)
                   (+ (window-body-width) len)
                  len)))
      (cond ((> (length right) len)
             (my/string-pad-left len right))

            ((> (length right) (- len spacing))
             (my/string-pad-left len (concat (make-string spacing ?\ )
                                        right)))

            ((> (length left) (- len spacing (length right)))
             (concat (my/string-pad-right (- len spacing (length right)) left)
                     (concat (make-string spacing ?\ )
                             right)))
            (t
             (concat left
                     (make-string (- len (length right) (length left)) ?\ )
                     right)))))

Date related

A set of date related functions, mostly used for mail display.

(defun my/date-day (date)
  "Return DATE day of month (1-31)."

  (nth 3 (decode-time date)))
(defun my/date-month (date)
  "Return DATE month number (1-12)."
  
  (nth 4 (decode-time date)))
(defun my/date-year (date)
  "Return DATE year."

  (nth 5 (decode-time date)))
(defun my/date-equal (date1 date2)
  "Check if DATE1 is equal to DATE2."
  
  (and (eq (my/date-day date1)
           (my/date-day date2))
       (eq (my/date-month date1)
           (my/date-month date2))
       (eq (my/date-year date1)
           (my/date-year date2))))
(defun my/date-inc (date &optional days months years)
  "Return DATE + DAYS day & MONTH months & YEARS years"

  (let ((days (or days 0))
        (months (or months 0))
        (years (or years 0))
        (day (my/date-day date))
        (month (my/date-month date))
        (year (my/date-year date)))
    (encode-time 0 0 0 (+ day days) (+ month months) (+ year years))))
(defun my/date-dec (date &optional days months years)
  "Return DATE - DAYS day & MONTH months & YEARS years"
  
  (let ((days (or days 0))
        (months (or months 0))
        (years (or years 0)))
    (my/date-inc date (- days) (- months) (- years))))
(defun my/date-today ()
  "Return today date."
  
  (current-time))
(defun my/date-is-today (date)
  "Check if DATE is today."
  
  (my/date-equal (current-time) date))
(defun my/date-is-yesterday (date)
  "Check if DATE is yesterday."
  
  (my/date-equal (my/date-dec (my/date-today) 1) date))
(defun my/date-relative (date)
  "Return a string with a relative date format."

  (let* ((now (current-time))
         (delta (float-time (time-subtract now date)))
         (days (ceiling (/ (float-time (time-subtract now date)) (* 60 60 24)))))
    (cond ((< delta (*       3 60))     "now")
          ((< delta (*      60 60))     (format "%d minutes ago" (/ delta   60)))
          ;;  ((< delta (*    6 60 60))     (format "%d hours ago"   (/ delta 3600)))
          ((my/date-is-today date)      (format-time-string "%H:%M" date))
          ((my/date-is-yesterday date)  (format "Yesterday"))
          ((< delta (* 4 24 60 60))     (format "%d days ago" (+ days 1)))
          (t                            (format-time-string "%d %b %Y" date)))))

Mini frame

A set of functions to create a mini-frame over the header line.

(defun my/mini-frame (&optional height foreground background border)
  "Create a child frame positionned over the header line whose
width corresponds to the width of the current selected window.

The HEIGHT in lines can be specified, as well as the BACKGROUND
color of the frame. BORDER width (pixels) and color (FOREGROUND)
can be also specified."
  
  (interactive)
  (let* ((foreground (or foreground
                         (face-foreground 'font-lock-comment-face nil t)))
         (background (or background (face-background 'highlight nil t)))
         (border (or border 1))
         (height (round (* (or height 8) (window-font-height))))
         (edges (window-pixel-edges))
         (body-edges (window-body-pixel-edges))
         (top (nth 1 edges))
         (bottom (nth 3 body-edges))
         (left (- (nth 0 edges) (or left-fringe-width 0)))
         (right (+ (nth 2 edges) (or right-fringe-width 0)))
         (width (- right left))

         ;; Window divider mode
         (width (- width (if (and (bound-and-true-p window-divider-mode)
                                  (or (eq window-divider-default-places 'right-only)
                                      (eq window-divider-default-places t))
                                (window-in-direction 'right (selected-window)))
                             window-divider-default-right-width
                           0)))
         (y (- top border))
         (child-frame-border (face-attribute 'child-frame-border :background)))
    (set-face-attribute 'child-frame-border t :background foreground)
    (let ((frame (make-frame
                  `((parent-frame . ,(window-frame))
                    (delete-before . ,(window-frame))
                    (minibuffer . nil)
                    (modeline . nil)
                    (left . ,(- left border))
                    (top . ,y)
                    (width . (text-pixels . ,width))
                    (height . (text-pixels . ,height))
                    ;; (height . ,height)
                    (child-frame-border-width . ,border)
                    (internal-border-width . ,border)
                    (background-color . ,background)
                    (horizontal-scroll-bars . nil)
                    (menu-bar-lines . 0)
                    (tool-bar-lines . 0)
                    (desktop-dont-save . t)
                    (unsplittable . nil)
                    (no-other-frame . t)
                    (undecorated . t)
                    (pixelwise . t)
                    (visibility . t)))))
      (set-face-attribute 'child-frame-border t :background child-frame-border)
      frame)))
(defun my/mini-frame-reset (frame)
  "Reset FRAME size and position.

  Move frame at the top of parent frame and resize it
  horizontally to fit the width of current selected window."
  
  (interactive)
  (let* ((border (frame-parameter frame 'internal-border-width))
         (height (frame-parameter frame 'height)))
    (with-selected-frame (frame-parent frame)
      (let* ((edges (window-pixel-edges))
             (body-edges (window-body-pixel-edges))
             (top (nth 1 edges))
             (bottom (nth 3 body-edges))
             (left (- (nth 0 edges) (or left-fringe-width 0)))
             (right (+ (nth 2 edges) (or right-fringe-width 0)))
             (width (- right left))
             (y (- top border)))
        (set-frame-width frame width nil t)
        (set-frame-height frame height)
        (set-frame-position frame (- left border) y)))))
(defun my/mini-frame-shrink (frame &optional delta)
  "Make the FRAME DELTA lines smaller.

  If no argument is given, make the frame one line smaller. If
  DELTA is negative, enlarge frame by -DELTA lines."

  (interactive)
  (let ((delta (or delta -1)))
    (when (and (framep frame)
               (frame-live-p frame)
               (frame-visible-p frame))
      (set-frame-parameter frame 'height
                           (+ (frame-parameter frame 'height) delta)))))

Mu4e related

A set of mail (mu4e) related functions.

(defun my/mu4e-get-account (msg)
  "Get MSG related account."
  
  (let* ((maildir (mu4e-message-field msg :maildir))
         (maildir (substring maildir 1)))
    (nth 0 (split-string maildir "/"))))
(defun my/mu4e-get-maildir (msg)
  "Get MSG related maildir."
  
  (let* ((maildir (mu4e-message-field msg :maildir))
         (maildir (substring maildir 1)))
    (nth 0 (reverse (split-string maildir "/")))))
(defun my/mu4e-get-mailbox (msg)
  "Get MSG related mailbox as 'account - maildir' "
  
  (format "%s - %s" (mu4e-get-account msg) (mu4e-get-maildir msg)))
(defun my/mu4e-get-sender (msg)
  "Get MSG sender."
  
  (let ((addr (cdr-safe (car-safe (mu4e-message-field msg :from)))))
      (mu4e~headers-contact-str (mu4e-message-field msg :from))))

Configuration

General

This section is meant to ease the writing of the configuration file using a dedicated minor mode (my/config-mode) with a few key bindings:

C-` : Toggle navigation sidebar C-c C-p : Go to previous subsection C-c C-n : Go to next subsection C-c C-S-p : Go to previous section C-c C-S-n : Go to next section C-c t : Fold code blocks C-c f : Filter block visibility (sidebar) C-c C-v t : Export (tangle) code C-c C-v s : Execute current subsection

Before being able to use it, you need to execute the whole subtree using org-babel-execute-subtree (generally bound to C-c C-v s).

Sidebar

This defines an org sidebar using imenu-list.

(require 'imenu)
(require 'imenu-list)

(defun my/org-tree-to-indirect-buffer ()
  "Create indirect buffer, narrow it to current subtree and unfold blocks"
  
  (org-tree-to-indirect-buffer)
  (org-show-block-all)
  (setq-local my/org-blocks-hidden nil))

(defun my/org-sidebar ()
  "Open an imenu list on the left that allow navigation."
  
  (interactive)
  (setq imenu-list-after-jump-hook #'my/org-tree-to-indirect-buffer
        imenu-list-position 'left
        imenu-list-size 36
        imenu-list-focus-after-activation t)

  (let ((heading (substring-no-properties (or (org-get-heading t t t t) ""))))
    (when (buffer-base-buffer)
      (switch-to-buffer (buffer-base-buffer)))
    (imenu-list-minor-mode)
    (imenu-list-stop-timer)
    (hl-line-mode)
    (face-remap-add-relative 'hl-line :inherit 'nano-subtle)
    (setq header-line-format
          '(:eval
            (nano-modeline-render nil
                                  (buffer-name imenu-list--displayed-buffer)
                                  "(outline)"
                                  "")))
    (setq-local cursor-type nil)
    (when (> (length heading) 0)
      (goto-char (point-min))
      (search-forward heading)
      (imenu-list-display-dwim))))

This toggles the org-sidebar.

(defun my/org-sidebar-toggle ()
  "Toggle the org-sidebar"
  
  (interactive)
  (if (get-buffer-window "*Ilist*")
      (progn 
        (quit-window nil (get-buffer-window "*Ilist*"))
        (switch-to-buffer (buffer-base-buffer)))
    (my/org-sidebar)))

Make sure tangle is applied to the base buffer and not the subtree.

(defun my/org-babel-tangle ()
  "Write code blocks to source-specific files from the base buffer."

  (interactive)
  (with-current-buffer (or (buffer-base-buffer)
                           (current-buffer))
    (org-babel-tangle)))

Toggle code blocks folding, starting folded.

(defvar my/org-blocks-hidden nil)

(defun my/org-toggle-blocks ()
  "Toggle code blocks folding."
  
  (interactive)
  (if my/org-blocks-hidden
      (org-show-block-all)
    (org-hide-block-all))
  (setq-local my/org-blocks-hidden (not my/org-blocks-hidden)))

(add-hook 'config-mode-hook #'my/org-toggle-blocks)
(defvar my/imenu-list-folding-status t
  "Folding status of the imenu-list")

(defun my/imenu-list-toggle-folding ()
  "Toggle top level nodes of the imenu-list buffer"
  
  (interactive)
  (with-current-buffer "*Ilist*"
    (save-excursion
      (goto-char (point-min))
      (while (re-search-forward "^\\+ " nil t)
        (if my/imenu-list-folding-status
            (hs-hide-block)
          (hs-show-block)))
      (setq my/imenu-list-folding-status (not my/imenu-list-folding-status)))))

(bind-key "S-<tab>" #'my/imenu-list-toggle-folding imenu-list-major-mode-map)

Some information for when a top node is closed

(defun my/display-code-line-counts (ov)
  (when (eq 'code (overlay-get ov 'hs))
    (overlay-put ov 'display
                 (propertize
                  (format " [%d sections] … "
                          (- (count-lines (overlay-start ov)
                                       (overlay-end ov)) 1))
                  'face 'nano-faded))))

(setq hs-set-up-overlay #'my/display-code-line-counts)
(defun my/imenu-list-display-dwim ()
  "Display or toggle the entry at `point'."
  (interactive)
  (save-selected-window
    (save-excursion
      (my/imenu-list-ret-dwim))))

(defun my/imenu-list-ret-dwim ()
  "Jump to or toggle the entry at `point'."
  (interactive)
  (save-excursion
    (let ((entry (imenu-list--find-entry)))
      (when (imenu--subalist-p entry)
        (setq entry (cons
                     (car entry)
                     (get-text-property 0 'marker (car entry)))))
      (imenu-list--goto-entry entry))))

(bind-key "<SPC>" #'my/imenu-list-display-dwim imenu-list-major-mode-map)
(bind-key "<return>" #'my/imenu-list-ret-dwim imenu-list-major-mode-map)

Filter

This provide the my/org-imenu-filter that allow to filter sidebar entries using the specified expression (e.g. “+HOOK +DEFER”).

(bind-key "C-c f" #'my/org-imenu-filter)
(bind-key "f" #'my/org-imenu-filter imenu-list-major-mode-map)
(bind-key "U" #'imenu-list-refresh imenu-list-major-mode-map)

3 levels for org-imenu

(setq org-imenu-depth 3)
(require 'org)
(require 'svg-tag-mode)

(defvar my/org-imenu-filter-history
  '("BINDING" "HOOK" "ADVICE" "FACE" "MODE" "DEFER"
    "PERSONAL" "INACTIVE" "BUGFIX" "OTHER" "TIMER" "OS")
  "Filter history list.")

(defvar my/org-imenu-filter-function
  (cdr (org-make-tags-matcher "*"))
  "Filter function to decide if a headline is kept")

(defun my/org-imenu-filter ()
  "Define and apply a new filter"
  
  (interactive)
  (let* ((match (completing-read-multiple
                 "FILTER: "
                 my/org-imenu-filter-history
                 nil nil nil
                 'my/org-imenu-filter-history))
         (match (mapconcat #'identity match " ")))
    (when (string= "" match)
      (setq match "*"))
    (setq my/org-imenu-filter-function
          (cdr (org-make-tags-matcher match)))
    (imenu-list-refresh)))


(defun my/org-imenu-filter-tree (&optional bound parent-match)
  "Build a imenu list using current filter function"
  
  (let* ((headlines '()))
    (save-excursion
      (org-with-wide-buffer
       (unless bound
         (setq bound (point-max))
         (goto-char (point-min)))
       (while (re-search-forward org-heading-regexp bound t)
         (let* ((element (org-element-at-point))
                (begin (org-element-property :begin element))
                (end (org-element-property :end element))
                (marker (copy-marker begin))
                (level (org-element-property :level element))
                (tags (save-excursion
                         (goto-char begin)
                          (org-get-tags)))
                (match (save-excursion
                         (goto-char begin)
                         (funcall my/org-imenu-filter-function
                                  nil (org-get-tags) level)))
                
                (title (org-element-property :raw-value element))
                (title (org-link-display-format
                        (substring-no-properties title)))
                (title (propertize title 'org-imenu-marker marker
                                         'org-imenu t))
                (title (if (member "INACTIVE" tags)
                           (propertize title 'face 'nano-faded)
                         title))
                (svg-tags (mapconcat #'(lambda (tag)
                                     (propertize tag 'display (svg-tag-make tag :face 'nano-faded)))
                                 tags " "))
                (title (if tags (format "%s %s" title svg-tags) title))
                (title (propertize title 'marker marker))
                (children (my/org-imenu-filter-tree end match)))
           (goto-char end)

           (cond ((> level org-imenu-depth)
                  nil)
                 ((> (length children) 0)
                  (add-to-list 'headlines (append (list title) children) t))
                 ((or match parent-match)
                  (add-to-list 'headlines (cons title marker) t)))))))
    headlines))

(advice-add #'org-imenu-get-tree :override #'my/org-imenu-filter-tree)

Configuration mode

This section defines the my/config-mode to ease navigating and interacting with the configuration file.

Navigation commands using the ilist menu.

(defun my/config-mode-prev-header ()
  "Move to previous header"
  
  (interactive)
  (with-current-buffer "*Ilist*"
    (search-backward-regexp "^  ")
    (imenu-list-display-dwim)))

(defun my/config-mode-next-header ()
  "Move to next header"
    
  (interactive)
  (with-current-buffer "*Ilist*"
    (forward-line)
    (search-forward-regexp "^  ")
    (imenu-list-display-dwim)))

(defun my/config-mode-prev-section ()
  "Move to previous section"
    
  (interactive)
  (with-current-buffer "*Ilist*"
    (search-backward-regexp "\\+ " nil nil 2)
    (forward-line)
    (imenu-list-display-dwim)))

(defun my/config-mode-next-section ()
  "Move to next section"
    
  (interactive)
  (with-current-buffer "*Ilist*"
    (previous-line)
    (search-forward-regexp "\\+ ")
    (forward-line)
    (imenu-list-display-dwim)))

A minor mode for configuration

(define-minor-mode my/config-mode
  "Configuration mode"
  
  :init-value nil
  :global nil
  :keymap (let* ((map (make-sparse-keymap)))
            (bind-key "C-c C-p"   #'my/config-mode-prev-header map)
            (bind-key "C-c C-n"   #'my/config-mode-next-header map)
            (bind-key "C-c C-S-p" #'my/config-mode-prev-section map)
            (bind-key "C-c C-S-n" #'my/config-mode-next-section map)
            (bind-key "C-`"       #'my/org-sidebar-toggle map)
            (bind-key "C-c C-v t" #'my/org-babel-tangle map)
            (bind-key "C-c t"     #'my/org-toggle-blocks map)
            map)

  (require 'org)
  (if my/config-mode
      (my/org-sidebar)))

A shortcut to edit configuration

(defun my/config ()
  "Create a new for editing configuration"
  
  (interactive)
  (select-frame (make-frame '((name . "my/config")
                              (width . 150)
                              (height . 45))))
  (find-file "~/Documents/GitHub/dotemacs/dotemacs.org")
  (my/config-mode))

An autoload function for my/config (that will load org mode).

(autoload 'my/config
  (expand-file-name "init.el" user-emacs-directory)
  "Autoloaded my/config command."
  t)

Bugfix

Temporary bugfix for babel emacs-lisp that does not take into account prologue/epilogue. See https://list.orgmode.org/CA+G3_PNrdhx0Ejzw8UO7DgZ+ju1B7Ar_eTch5MMViEpKGwqq3w@mail.gmail.com/T/ (November 2020)

(defun my/org-babel-expand-body:emacs-lisp (orig-fun body params)
  "Expand BODY according to PARAMS and call original function with new body"

  (let* ((pro (or (cdr (assq :prologue params)) ""))
         (epi (or (cdr (assq :epilogue params)) ""))
         (body (concat pro body epi)))
    (apply orig-fun `(,body ,params))))

(advice-add 'org-babel-expand-body:emacs-lisp
            :around
            #'my/org-babel-expand-body:emacs-lisp)

Auto-tangle

Automatically tangle org-mode files with the option #+auto_tangle: t

(add-hook 'org-mode-hook 'org-auto-tangle-mode)

Benchmark

(my/report-time "Personal library")

Interface

(setq my/section-start-time (current-time))

Frame

A make-frame rewrote that creates the frame and switch to the *scratch* buffer.

(defun my/make-frame ()
  "Create a new frame and switch to *scratch* buffer."

  (interactive)
  (select-frame (make-frame))
  (switch-to-buffer "*scratch*"))

A function that close the current frame and kill emacs if it was the last frame.

(defun my/kill-emacs ()
  "Delete frame or kill Emacs if there is only one frame."
  
  (interactive)
  (condition-case nil
      (delete-frame)
    (error (save-buffers-kill-terminal))))

Default frame geometry (large margin: 24 pixels).

(require 'frame)

;; Default frame settings
(setq default-frame-alist '((min-height . 1)  '(height . 45)
                            (min-width  . 1)  '(width  . 81)
                            (vertical-scroll-bars . nil)
                            (internal-border-width . 24)
                            (left-fringe . 0)
                            (right-fringe . 0)
                            (tool-bar-lines . 0)
                            (menu-bar-lines . 1)))

;; Default frame settings
(setq initial-frame-alist default-frame-alist)

Frame related binding (self explanatory).

(bind-key "M-n"        #'my/make-frame)
(bind-key "C-x C-c"    #'my/kill-emacs)
(bind-key "M-`"        #'other-frame)
(bind-key "C-z"        nil)
(bind-key "<M-return>" #'toggle-frame-maximized)

For frame maximization, we have to make a specific case for org-mode.

(with-eval-after-load 'org
  (bind-key "<M-return>" #'toggle-frame-maximized 'org-mode-map))

Window

Margin and divider mode.

(setq-default window-divider-default-right-width 24
              window-divider-default-places 'right-only
              left-margin-width 0
              right-margin-width 0
              window-combination-resize nil) ; Do not resize windows proportionally

(window-divider-mode 1)

Toggle the dedicated flag on the current window

;; Make a window dedicated
(defun my/toggle-window-dedicated ()
  "Toggle whether the current active window is dedicated or not"
  (interactive)
  (message
   (if (let (window (get-buffer-window (current-buffer)))
     (set-window-dedicated-p window (not (window-dedicated-p window))))
       "Window '%s' is dedicated"
     "Window '%s' is normal")
   (current-buffer))
  (force-window-update))

(bind-key "C-c d" #'my/toggle-window-dedicated)

Buffer

Size of temporary buffers

(temp-buffer-resize-mode)
(setq temp-buffer-max-height 8)

Unique buffer names

(require 'uniquify)

(setq uniquify-buffer-name-style 'reverse
      uniquify-separator ""
      uniquify-after-kill-buffer-p t
      uniquify-ignore-buffers-re "^\\*")

No question after killing a buffer (kill-buffer asks you which buffer to switch to)

(bind-key "C-x k" #'kill-current-buffer)

File

Follow symlinks without prompt

(setq vc-follow-symlinks t)

Dialogs

Emacs can use a large number of dialogs and popups. Here we get rid of them.

(setq-default show-help-function nil    ; No help text
              use-file-dialog nil       ; No file dialog
              use-dialog-box nil        ; No dialog box
              pop-up-windows nil)       ; No popup windows

(tooltip-mode -1)                       ; No tooltips
(scroll-bar-mode -1)                    ; No scroll bars
(tool-bar-mode -1)                      ; No toolbar

Specific case for OSX since menubar is desktop-wide (see emacs.stackexchange.com/questions/28121) and emacs-mac documentation.

Unlike the original Emacs, enabling or disabling Menu Bar mode (@pxref{Menu Bars}) does not affect the appearance of the mexnu bar on the Mac port because it does not make sense on OS X having the global menu bar. Instead, the value of the @code{menu-bar-lines} frame parameter affects the system-wide full screen behavior of the frame. In most cases, disabling the menu bar of a particular frame by default means that it is a utility frame used for a subsidiary purpose together with other frames, rather than an ordinary frame on its own. Examples include the speedbar (@pxref{Speedbar}) and Ediff Control Panel (@pxref{Top, Ediff, Ediff, ediff, The Ediff Manual}). Using this heuristics, the Mac port regards a frame having a menu bar as an ordinary frame that is eligible for full screen. Conversely, a frame without a menu bar is considered as a utility frame and it can coexist with a full screen ordinary frame and other utility frames in a same desktop (or Space) for full screen. Note that a utility frame doesn’t have the full screen button on the title bar. If you don’t see the full screen button while it is supposed to be there, then check the menu bar setting.

(menu-bar-mode 1)

Keyboard

The mode displays the key bindings following your currently entered incomplete command (a ;; prefix) in a popup.

(require 'which-key)

(which-key-mode)

Cursor

We set the appearance of the cursor: horizontal line, 2 pixels thick, no blinking

(setq-default cursor-in-non-selected-windows nil ; Hide the cursor in inactive windows
              cursor-type '(hbar . 2)            ; Underline-shaped cursor
              cursor-intangible-mode t           ; Enforce cursor intangibility
              x-stretch-cursor nil)              ; Don't stretch cursor to the glyph width

(blink-cursor-mode 0)                            ; Still cursor

Text

Pretty self-explanatory

(setq-default use-short-answers t                     ; Replace yes/no prompts with y/n
              confirm-nonexistent-file-or-buffer nil) ; Ok to visit non existent files

Replace region when inserting text

(delete-selection-mode 1)

A smarter fill/unfill command

(defun my/fill-unfill ()
  "Like `fill-paragraph', but unfill if used twice."
  
  (interactive)
  (let ((fill-column
         (if (eq last-command #'my/fill-unfill)
             (progn (setq this-command nil)
                    (point-max))
           fill-column)))
    (call-interactively #'fill-paragraph)))

(bind-key "M-q"  #'my/fill-unfill)
;; (bind-key [remap fill-paragraph]  #'my/fill-unfill)

Sound

Disable the bell (auditory or visual).

(setq-default visible-bell nil             ; No visual bell      
              ring-bell-function 'ignore)  ; No bell

Mouse

Mouse behavior can be finely controlled using the mouse-avoidance-mode.

(setq-default mouse-yank-at-point t) ; Yank at point rather than pointer
(mouse-avoidance-mode 'exile)        ; Avoid collision of mouse with point

Mouse active in tty mode.

(unless (display-graphic-p)
  (xterm-mouse-mode 1)
  (global-set-key (kbd "<mouse-4>") #'scroll-down-line)
  (global-set-key (kbd "<mouse-5>") #'scroll-up-line))

Scroll

Smoother scrolling.

(setq-default scroll-conservatively 101       ; Avoid recentering when scrolling far
              scroll-margin 2                 ; Add a margin when scrolling vertically
              recenter-positions '(5 bottom)) ; Set re-centering positions

Clipboard

Allows system and Emacs clipboard to communicate smoothly (both ways)

(setq-default select-enable-clipboard t) ; Merge system's and Emacs' clipboard

Make sure clipboard works properly in tty mode on OSX.

(defun my/paste-from-osx ()
  (shell-command-to-string "pbpaste"))

(defun my/copy-to-osx (text &optional push)
  (let ((process-connection-type nil))
    (let ((proc (start-process "pbcopy" "*Messages*" "pbcopy")))
      (process-send-string proc text)
      (process-send-eof proc))))

(when (and (not (display-graphic-p))
           (eq system-type 'darwin))
  (setq interprogram-cut-function   #'my/copy-to-osx
        interprogram-paste-function #'my/paste-from-osx))

Help

Helpful is an alternative to the built-in Emacs help that provides much more contextual information. It is a bit slow to load so we do need load it explicitely.

(setq help-window-select t)             ; Focus new help windows when opened

(bind-key "C-h f"   #'helpful-callable) ; Look up callable
(bind-key "C-h v"   #'helpful-variable) ; Look up variable
(bind-key "C-h k"   #'helpful-key)      ; Look up key 
(bind-key "C-c C-d" #'helpful-at-point) ; Look up the current symbol at point
(bind-key "C-h F"   #'helpful-function) ; Look up *F*unctions (excludes macros).
(bind-key "C-h C"   #'helpful-command)  ; Look up *C*ommands.

Benchmark

(my/report-time "Interface")

Visual

(setq my/section-start-time (current-time))

Colors

A consistent theme for GNU Emacs. The light theme is based on Material colors and the dark theme is based on Nord colors. The theme is based on a set of six faces (only).

(require 'nano-theme)
(setq nano-fonts-use t) ; Use theme font stack
(nano-light)            ; Use theme light version
(nano-mode)             ; Recommended settings


(defun my/set-face (face style)
  "Reset FACE and make it inherit STYLE."
  (set-face-attribute face nil
                      :foreground 'unspecified :background 'unspecified
                      :family     'unspecified :slant      'unspecified
                      :weight     'unspecified :height     'unspecified
                      :underline  'unspecified :overline   'unspecified
                      :box        'unspecified :inherit    style))
(my/set-face 'italic 'nano-faded)

We still want the transient nano splash screen

(require 'nano-splash)

Fonts

This is the font stack we install:

  • Default font: Roboto Mono 14pt Light
  • Italic font: Victor Mono 14pt Semilight
  • Bold font: Roboto Mono 14pt Regular
  • Unicode font: Inconsolata 16pt Light
  • Icon font: Roboto Mono Nerd 12pt Light

Text excerpt using a gorgeous and true italic font (Victor Mono), chosen to really stand out from the default font (Roboto Mono). ┌───────────────────────────────────────────────┐ │  The quick brown fox jumps over the lazy dog │ │  The quick brown fox jumps over the lazy dog ┼─ Victor Mono Italic │  The quick brown fox jumps over the lazy dog ├─ Inconsolata └─┼───────────────────────────┼─────────────────┘ Roboto Mono Nerd Roboto Mono

Note that the Victor Mono needs to be hacked such as to have the same line height as Roboto Mono. To do that, you can use the font-line utility (github.com/source-foundry/font-line): copy all the italic faces from the Victor Mono ttf file into a directoy and type: font-line percent 10 *.ttf. This will create a new set of files that you can use to replace the Victor Mono italic faces on your system.

(set-face-attribute 'default nil
                    :family "Roboto Mono"
                    :weight 'light
                    :height 140)

(set-face-attribute 'bold nil
                    :family "Roboto Mono"
                    :weight 'regular)

(set-face-attribute 'italic nil
                    :family "Victor Mono"
                    :weight 'semilight
                    :slant 'italic)

(set-fontset-font t 'unicode
                    (font-spec :name "Inconsolata Light"
                               :size 16) nil)

(set-fontset-font t '(#xe000 . #xffdd)
                     (font-spec :name "RobotoMono Nerd Font"
                                :size 12) nil)

Typography

(setq-default fill-column 80                          ; Default line width 
              sentence-end-double-space nil           ; Use a single space after dots
              bidi-paragraph-direction 'left-to-right ; Faster
              truncate-string-ellipsis "")           ; Nicer ellipsis

Changing the symbol for truncation (…) and wrap (↩).

(require 'nano-theme)

;; Nicer glyphs for continuation and wrap 
(set-display-table-slot standard-display-table
                        'truncation (make-glyph-code ?… 'nano-faded))

(defface wrap-symbol-face
  '((t (:family "Fira Code"
        :inherit nano-faded)))
  "Specific face for wrap symbol")

(set-display-table-slot standard-display-table
                        'wrap (make-glyph-code ?↩ 'wrap-symbol-face))

Fix a bug on OSX in term mode & zsh (spurious “%” after each command)

(when (eq system-type 'darwin)
  (add-hook 'term-mode-hook
            (lambda ()
              (setq buffer-display-table (make-display-table)))))

Make sure underline is positionned at the very bottom.

(setq x-underline-at-descent-line nil
      x-use-underline-position-properties t
      underline-minimum-offset 10)

Benchmark

(my/report-time "Visual")

Editing

(setq my/section-start-time (current-time))

Default mode

Default & initial mode is text.

(setq-default initial-major-mode 'text-mode   ; Initial mode is text
              default-major-mode 'text-mode)  ; Default mode is text

Visual line mode for prog and text modes

(add-hook 'text-mode-hook 'visual-line-mode)
(add-hook 'prog-mode-hook 'visual-line-mode)

Tabulations

No tabulation, ever.

(setq-default indent-tabs-mode nil        ; Stop using tabs to indent
              tab-always-indent 'complete ; Indent first then try completions
              tab-width 4)                ; Smaller width for tab characters

;; Let Emacs guess Python indent silently
(setq python-indent-guess-indent-offset t
      python-indent-guess-indent-offset-verbose nil)

Parenthesis

Paren mode for highlighting matcing paranthesis

(require 'paren)
;; (setq show-paren-style 'expression)
(setq show-paren-style 'parenthesis)
(setq show-paren-when-point-in-periphery t)
(setq show-paren-when-point-inside-paren nil)
(show-paren-mode)

Imenu list

Imenu setup

(require 'imenu-list)

(setq-default imenu-list-position 'left
              imenu-max-item-length 1000)

Highlighting

Highlighting of the current line (native mode)

(require 'hl-line)

(global-hl-line-mode)

PDF Tools

For retina display (OSX)

;; (require 'pdf-tools)

(add-hook 'doc-view-mode-hook 'pdf-tools-install)

(setq-default pdf-view-use-scaling t
              pdf-view-use-imagemagick nil)

Benchmark

(my/report-time "Editing")

Completion

(setq my/section-start-time (current-time))

Corfu

Corfu enhances completion at point with a small completion popup.

(require 'corfu)

(setq corfu-cycle t                ; Enable cycling for `corfu-next/previous'
      corfu-auto t                 ; Enable auto completion
      corfu-auto-delay 60.0        ; Delay before auto-completion shows up
      corfu-separator ?\s          ; Orderless field separator
      corfu-quit-at-boundary nil   ; Never quit at completion boundary
      corfu-quit-no-match t        ; Quit when no match
      corfu-preview-current nil    ; Disable current candidate preview
      corfu-preselect-first nil    ; Disable candidate preselection
      corfu-on-exact-match nil     ; Configure handling of exact matches
      corfu-echo-documentation nil ; Disable documentation in the echo area
      corfu-scroll-margin 5)       ; Use scroll margin

(global-corfu-mode)

A few more useful configurations…

;; TAB cycle if there are only few candidates
(setq completion-cycle-threshold 3)

;; Emacs 28: Hide commands in M-x which do not apply to the current mode.
;; Corfu commands are hidden, since they are not supposed to be used via M-x.
(setq read-extended-command-predicate
      #'command-completion-default-include-p)

;; Enable indentation+completion using the TAB key.
;; completion-at-point is often bound to M-TAB.
(setq tab-always-indent 'complete)

;; Completion in source blocks
(require 'cape)

(add-to-list 'completion-at-point-functions 'cape-symbol)

Orderless

Allow completion based on space-separated tokens, out of order.

(require 'orderless)
  
(setq completion-styles '(substring orderless basic)
      orderless-component-separator 'orderless-escapable-split-on-space
      read-file-name-completion-ignore-case t
      read-buffer-completion-ignore-case t
      completion-ignore-case t)

Benchmark

(my/report-time "Completion")

Minibuffer & Modeline

(setq my/section-start-time (current-time))

Consult

We replace some of emacs functions with their consult equivalent

(require 'consult)

(setq consult-preview-key nil) ; No live preview

(bind-key "C-x C-r" #'consult-recent-file)
(bind-key "C-x h"   #'consult-outline)
(bind-key "C-x b"   #'consult-buffer)
(bind-key "C-c h"   #'consult-history)
;; (bind-key "M-:"     #'consult-complex-command)

For the consult-goto-line and consult-line commands, we define our owns with live preview (independently of the consult-preview-key)

(defun my/consult-line ()
  "Consult line with live preview"
  
  (interactive)
  (let ((consult-preview-key 'any)
        (mini-frame-resize 'grow-only)) ;; !! Important
    (consult-line)))

(bind-key "C-s"   #'my/consult-line)
(defun my/consult-goto-line ()
  "Consult goto line with live preview"
  
  (interactive)
  (let ((consult-preview-key 'any))
    (consult-goto-line)))

(bind-key "M-g g"   #'my/consult-goto-line)
(bind-key "M-g M-g" #'my/consult-goto-line)

Vertico

Vertico provides a performant and minimalistic vertical completion UI based on the default completion system but aims to be highly flexible, extensible and modular.

(require 'vertico)

;; (setq completion-styles '(basic substring partial-completion flex))

(setq vertico-resize nil        ; How to resize the Vertico minibuffer window.
      vertico-count 8           ; Maximal number of candidates to show.
      vertico-count-format nil) ; No prefix with number of entries

(vertico-mode)

Tweaking settings

(setq vertico-grid-separator
      #("  |  " 2 3 (display (space :width (1))
                             face (:background "#ECEFF1")))

      vertico-group-format
      (concat #(" " 0 1 (face vertico-group-title))
              #(" " 0 1 (face vertico-group-separator))
              #(" %s " 0 4 (face vertico-group-title))
              #(" " 0 1 (face vertico-group-separator
                          display (space :align-to (- right (-1 . right-margin) (- +1)))))))

(set-face-attribute 'vertico-group-separator nil
                    :strike-through t)
(set-face-attribute 'vertico-current nil
                    :inherit '(nano-strong nano-subtle))
(set-face-attribute 'completions-first-difference nil
                    :inherit '(nano-default))

Bind shift-tab for completion

(bind-key "<backtab>" #'minibuffer-complete vertico-map)

Completion-at-point and completion-in-region (see https://github.com/minad/vertico#completion-at-point-and-completion-in-region)

(setq completion-in-region-function
      (lambda (&rest args)
        (apply (if vertico-mode
                   #'consult-completion-in-region
                 #'completion--in-region)
               args)))

Prefix the current candidate (See https://github.com/minad/vertico/wiki#prefix-current-candidate-with-arrow)

(defun minibuffer-format-candidate (orig cand prefix suffix index _start)
  (let ((prefix (if (= vertico--index index)
                    ""
                  "   "))) 
    (funcall orig cand prefix suffix index _start)))

(advice-add #'vertico--format-candidate
           :around #'minibuffer-format-candidate)

See https://kristofferbalintona.me/posts/vertico-marginalia-all-the-icons-completion-and-orderless/#vertico

(defun vertico--prompt-selection ()
  "Highlight the prompt"

  (let ((inhibit-modification-hooks t))
    (set-text-properties (minibuffer-prompt-end) (point-max)
                         '(face (nano-strong nano-salient)))))

See minad/vertico#145

 
(defun minibuffer-vertico-setup ()

  (setq truncate-lines t)
  (setq completion-in-region-function
        (if vertico-mode
            #'consult-completion-in-region
          #'completion--in-region)))

(add-hook 'vertico-mode-hook #'minibuffer-vertico-setup)
(add-hook 'minibuffer-setup-hook #'minibuffer-vertico-setup)

Marginalia

Pretty straightforward.

(require 'marginalia)

(setq-default marginalia--ellipsis ""    ; Nicer ellipsis
              marginalia-align 'right     ; right alignment
              marginalia-align-offset -1) ; one space on the right

(marginalia-mode)

Modeline

We’re using nano-modeline and modify some settings here.

(require 'nano-theme)
(require 'nano-modeline)

(setq nano-modeline-prefix 'status)
(setq nano-modeline-prefix-padding 1)

(set-face-attribute 'header-line nil)
(set-face-attribute 'mode-line nil
                    :foreground (face-foreground 'nano-subtle-i)
                    :background (face-foreground 'nano-subtle-i)
                    :inherit nil
                    :box nil)
(set-face-attribute 'mode-line-inactive nil
                    :foreground (face-foreground 'nano-subtle-i)
                    :background (face-foreground 'nano-subtle-i)
                    :inherit nil
                    :box nil)

(set-face-attribute 'nano-modeline-active nil
                    :underline (face-foreground 'nano-default-i)
                    :background (face-background 'nano-subtle)
                    :inherit '(nano-default-)
                    :box nil)
(set-face-attribute 'nano-modeline-inactive nil
                    :foreground 'unspecified
                    :underline (face-foreground 'nano-default-i)
                    :background (face-background 'nano-subtle)
                    :box nil)

(set-face-attribute 'nano-modeline-active-name nil
                    :foreground "black"
                    :inherit '(nano-modeline-active nano-strong))
(set-face-attribute 'nano-modeline-active-primary nil
                    :inherit '(nano-modeline-active))
(set-face-attribute 'nano-modeline-active-secondary nil
                    :inherit '(nano-faded nano-modeline-active))

(set-face-attribute 'nano-modeline-active-status-RW nil
                    :inherit '(nano-faded-i nano-strong nano-modeline-active))
(set-face-attribute 'nano-modeline-active-status-** nil
                    :inherit '(nano-popout-i nano-strong nano-modeline-active))
(set-face-attribute 'nano-modeline-active-status-RO nil
                    :inherit '(nano-default-i nano-strong nano-modeline-active))

(set-face-attribute 'nano-modeline-inactive-name nil
                    :inherit '(nano-faded nano-strong
                               nano-modeline-inactive))
(set-face-attribute 'nano-modeline-inactive-primary nil
                    :inherit '(nano-faded nano-modeline-inactive))

(set-face-attribute 'nano-modeline-inactive-secondary nil
                    :inherit '(nano-faded nano-modeline-inactive))
(set-face-attribute 'nano-modeline-inactive-status-RW nil
                    :inherit '(nano-modeline-inactive-secondary))
(set-face-attribute 'nano-modeline-inactive-status-** nil
                    :inherit '(nano-modeline-inactive-secondary))
(set-face-attribute 'nano-modeline-inactive-status-RO nil
                    :inherit '(nano-modeline-inactive-secondary))

We set a thin modeline

(defun my/thin-modeline ()
  "Transform the modeline in a thin faded line"
  
  (nano-modeline-face-clear 'mode-line)
  (nano-modeline-face-clear 'mode-line-inactive)
  (setq mode-line-format (list ""))
  (setq-default mode-line-format (list ""))
  (set-face-attribute 'mode-line nil
                      :box nil
                      :inherit nil
                      :foreground (face-background 'nano-subtle)
                      :background (face-background 'nano-subtle)
                      :height 0.1)
  (set-face-attribute 'mode-line-inactive nil
                      :box nil
                      :inherit nil
                      :foreground (face-background 'nano-subtle)
                      :background (face-background 'nano-subtle)
                      :height 0.1))

(add-hook 'nano-modeline-mode-hook #'my/thin-modeline)

We start the nano modeline.

(nano-modeline-mode 1)

Minibuffer

Headerline (fake) for minibuffer

(require 'minibuffer-header)

(setq minibuffer-header-show-message t
      minibuffer-header-hide-prompt t
      minibuffer-header-default-message "")

(set-face-attribute 'minibuffer-header-face nil
                    :inherit 'nano-subtle
                    :extend t)
(set-face-attribute 'minibuffer-header-message-face nil
                    :inherit '(nano-subtle nano-faded)
                    :extend t)

This should be an advice but it is simpler to rewrite the function

(defun my/minibuffer-header-format (prompt)
  "Minibuffer header"
  
  (let* ((prompt (replace-regexp-in-string "[: \t]*$" "" prompt))
         (depth (minibuffer-depth))
         (prompt (cond ((string= prompt "M-x") "Extended command")
                       ((string= prompt "Function") "Help on function")
                       ((string= prompt "Callable") "Help on function or macro")
                       ((string= prompt "Variable") "Help on variable")
                       ((string= prompt "Command") "Help on command")
                       ((string= prompt "Eval") "Evaluate lisp expression")
                       (t prompt))))
    (concat
     (propertize (format " %d " depth)
                 'face `(:inherit (nano-salient-i nano-strong)
                         :extend t))
     (propertize " "
                 'face 'nano-subtle 'display `(raise ,nano-modeline-space-top))

     (propertize prompt
                 'face `(:inherit (nano-subtle nano-strong nano-salient)
                         :extend t))
     (propertize " "
                 'face 'nano-subtle 'display `(raise ,nano-modeline-space-bottom))
     (propertize "\n" 'face 'highlight)
     (propertize " " 'face 'highlight
                     'display `(raise ,nano-modeline-space-top))
     (propertize "︎︎" 'face '(:inherit (nano-salient nano-strong)))
     (propertize " " 'face 'highlight
                     'display `(raise ,nano-modeline-space-bottom)))))

(setq minibuffer-header-format #'my/minibuffer-header-format)

Activate minibuffer header

(minibuffer-header-mode)

Some styling setting for the minibuffer

(defun my/minibuffer-setup ()

  (set-window-margins nil 0 0)
  (set-fringe-style '(0 . 0))
  (cursor-intangible-mode t)
  (face-remap-add-relative 'default :inherit 'highlight))

(add-hook 'minibuffer-setup-hook #'my/minibuffer-setup)

Showing key binding for the current command

;; Code from https://stackoverflow.com/questions/965263
(defun my/lookup-function (keymap func)
  (let ((all-bindings (where-is-internal (if (symbolp func)
                                             func
                                           (cl-first func))
                                         keymap))
        keys key-bindings)
    (dolist (binding all-bindings)
      (when (and (vectorp binding)
                 (integerp (aref binding 0)))
        (push binding key-bindings)))
    (push (mapconcat #'key-description key-bindings " or ") keys)
    (car keys)))


(defun my/minibuffer-show-last-command-setup ()
  (setq minibuffer-header-default-message
   (my/lookup-function (current-global-map) this-command)))

(add-hook 'minibuffer-setup-hook #'my/minibuffer-show-last-command-setup)

(defun my/minibuffer-show-last-command-exit ()
  (setq minibuffer-header-default-message ""))
(add-hook 'minibuffer-exit-hook #'my/minibuffer-show-last-command-exit)

Vertico will disable truncate lines when point is too far on the right. Problem is that it’ll mess up with our fake headerline. We thus rewrite here the function to have truncate lines always on.

(defun my/vertico--resize-window (height)
  "Resize active minibuffer window to HEIGHT."
;;  (setq-local truncate-lines (< (point) (* 0.8 (vertico--window-width)))
    (setq-local truncate-lines t
                resize-mini-windows 'grow-only
                max-mini-window-height 1.0)
  (unless (frame-root-window-p (active-minibuffer-window))
    (unless vertico-resize
      (setq height (max height vertico-count)))
    (let* ((window-resize-pixelwise t)
           (dp (- (max (cdr (window-text-pixel-size))
                       (* (default-line-height) (1+ height)))
                  (window-pixel-height))))
      (when (or (and (> dp 0) (/= height 0))
                (and (< dp 0) (eq vertico-resize t)))
        (window-resize nil dp nil nil 'pixelwise)))))

(advice-add #'vertico--resize-window :override #'my/vertico--resize-window)

No prompt editing and recursive minibuffer

(setq minibuffer-prompt-properties '(read-only t
                                     cursor-intangible t
                                     face minibuffer-prompt)
      enable-recursive-minibuffers t)

Miniframe

(require 'mini-frame)

(defcustom my/minibuffer-position 'bottom
  "Minibuffer position, one of 'top or 'bottom"
  :type '(choice (const :tag "Top"    top)
                 (const :tag "Bottom" bottom))
  :group 'nano-minibuffer)


(defun my/minibuffer--frame-parameters ()
  "Compute minibuffer frame size and position."

  ;; Quite precise computation to align the minibuffer and the
  ;; modeline when they are both at top position
  (let* ((edges (window-pixel-edges)) ;; (left top right bottom)
         (body-edges (window-body-pixel-edges)) ;; (left top right bottom)
         (left (nth 0 edges)) ;; Take margins into account
         (top (nth 1 edges)) ;; Drop header line
         (right (nth 2 edges)) ;; Take margins into account
         (bottom (nth 3 body-edges)) ;; Drop header line
         (left (if (eq left-fringe-width 0)
                   left
                 (- left (frame-parameter nil 'left-fringe))))
         (right (nth 2 edges))
         (right (if (eq right-fringe-width 0)
                    right
                  (+ right (frame-parameter nil 'right-fringe))))
         (border 1)
         (width (- right left (* 1 border)))

         ;; Window divider mode
         (width (- width (if (and (bound-and-true-p window-divider-mode)
                                  (or (eq window-divider-default-places 'right-only)
                                      (eq window-divider-default-places t))
                                  (window-in-direction 'right (selected-window)))
                             window-divider-default-right-width
                           0)))
         (y (- top border)))

    (append `((left-fringe . 0)
              (right-fringe . 0)
              (user-position . t) 
              (foreground-color . ,(face-foreground 'highlight nil 'default))
              (background-color . ,(face-background 'highlight nil 'default)))
            (cond ((and (eq my/minibuffer-position 'bottom))
                   `((top . -1)
                     (left . 0)
                     (width . 1.0)
                     (child-frame-border-width . 0)
                     (internal-border-width . 0)))
                  (t
                   `((left . ,(- left border))
                     (top . ,y)

                     (width . (text-pixels . ,width))
                     (child-frame-border-width . ,border)
                     (internal-border-width . 0)))))))

  (set-face-background 'child-frame-border (face-foreground 'nano-faded))
  (setq mini-frame-default-height 3)
  (setq mini-frame-create-lazy t)
  (setq mini-frame-show-parameters 'my/minibuffer--frame-parameters)
  (setq mini-frame-ignore-commands
        '("edebug-eval-expression" debugger-eval-expression))
  (setq mini-frame-internal-border-color (face-foreground 'nano-faded))

  (setq mini-frame-resize-min-height 3)
  (setq mini-frame-resize t)
  ;; (setq mini-frame-resize 'grow-only)
  ;; (setq mini-frame-default-height (+ 1 vertico-count))
  ;; (setq mini-frame-resize-height (+ 1 vertico-count))
  ;; (setq mini-frame-resize nil)

Mini-frame mode OFF

;; (mini-frame-mode 1)

More a hack than a fix but the code below improve the mini-frame resize by setting position explicity. CURRENTLY INACTIVE

(defun my/mini-frame--resize-mini-frame (frame)
  "Resize FRAME vertically only.
This function used as value for `resize-mini-frames' variable."
  (funcall mini-frame--fit-frame-function
           frame
           mini-frame-resize-max-height
           (if (eq mini-frame-resize 'grow-only)
               (max (frame-parameter frame 'height)
                    mini-frame-resize-min-height)
             mini-frame-resize-min-height)
           ;; A max-width must be included to work around a bug in Emacs which
           ;; causes wrapping to not be taken into account in some situations
           ;; https://debbugs.gnu.org/cgi/bugreport.cgi?bug=56102
           (window-body-width)
           nil
           'vertically)

  (if (eq my/minibuffer-position 'top)
      (modify-frame-parameters  mini-frame-completions-frame `((top . 0)))
    (modify-frame-parameters  mini-frame-completions-frame `((top . (- 1))))))

Benchmark

(my/report-time "Minibuffer/Modeline")

Mail

For OSX users, make sure to have a look at: https://macowners.club/posts/email-emacs-mu4e-macos/

(setq my/section-start-time (current-time))

Setup

Mu4e doesn’t come with an autoload function, we declare it here.

(autoload 'mu4e
  "/usr/local/Cellar/mu/1.8.7/share/emacs/site-lisp/mu/mu4e/mu4e.el"
  "Start mu4e daemon and show its main window." t)

Lots of options. Make sure to adapt paths to your system.

(setq mu4e-mu-binary "/usr/local/bin/mu"
      mu4e-maildir "~/Documents/Mail"
      mu4e-attachment-dir "~/Downloads"
      mu4e-get-mail-command "/usr/local/bin/mbsync -a"

      mu4e-update-interval 300            ; Update interval (seconds)
      mu4e-index-cleanup t                ; Cleanup after indexing
      mu4e-index-update-error-warning t   ; Warnings during update
      mu4e-hide-index-messages t          ; Hide indexing messages
      mu4e-index-update-in-background t   ; Background update
      mu4e-change-filenames-when-moving t ; Needed for mbsync
      mu4e-index-lazy-check nil           ; Don't be lazy, index everything
        
      mu4e-confirm-quit nil
      mu4e-split-view 'single-window

      mu4e-headers-auto-update nil
      mu4e-headers-date-format "%d-%m"
      mu4e-headers-time-format "%H:%M"
      mu4e-headers-from-or-to-prefix '("" . "To ")
      mu4e-headers-include-related t
      mu4e-headers-skip-duplicates t)

How to handle various MIME data.

(require 'mailcap)

(push '((viewer . "open %s 2> /dev/null &")
        (type . "application/pdf")
        (test . window-system))
      mailcap-user-mime-data)

(when (fboundp 'imagemagick-register-types)
  (imagemagick-register-types))

Some bindings to avoid confirmation for execution (headers and message view)

(bind-key "x" (lambda() (interactive) (mu4e-mark-execute-all t)) mu4e-headers-mode-map)
(bind-key "x" (lambda() (interactive) (mu4e-mark-execute-all t)) mu4e-view-mode-map)

Accounts

General

General information about me.

;; User name
(setq user-full-name "Nicolas P. Rougier")

;; Main user mail address
(setq user-mail-address "nicolas.rougier@inria.fr")

;; Common signature for all accounts.
(setq mu4e-signature (concat
                      "Nicolas P. Rougier —— www.labri.fr/perso/nrougier\n"
                      "Institute of Neurodegenerative Diseases, Bordeaux"
;;  "Nicolas P. Rougier — Institute of Neurodegenerative Diseases\n"
;;  "University of Bordeaux — https://www.labri.fr/perso/nrougier\n"
;;  "Nicolas P. Rougier —— Research Director\n"
;;  "Institute of Neurodegenerative Diseases\n"
;;  "Bordeaux —— www.labri.fr/perso/nrougier"
  ))

Because we’ll use mu4e-contexts, we reset single account settings.

(setq mu4e-contexts nil
      mu4e-drafts-folder nil
      mu4e-compose-reply-to-address nil
      mu4e-compose-signature t
      mu4e-compose-signature-auto-include t
      mu4e-sent-folder nil
      mu4e-trash-folder nil)
(setq mu4e-context-policy 'pick-first  ; How to determine context when entering headers view
      mu4e-compose-context-policy nil) ; Do not modify context when composing

Refile/archive depending on the context (via maildir)

(defun my/mu4e-refile-folder (msg)
  "Contextual refile"
  
  (let ((maildir (mu4e-message-field msg :maildir)))
    (cond
     ((string-match "inria" maildir) "/inria/archive")
     ((string-match "gmail" maildir) "/gmail/archive")
     ((string-match "univ"  maildir) "/univ/archive")
     (t ""))))

(setq mu4e-refile-folder 'my/mu4e-refile-folder)

University

(add-to-list 'mu4e-contexts
             (make-mu4e-context
              :name "University"
              :enter-func (lambda () (mu4e-message "Entering university context"))
              :leave-func (lambda () (mu4e-message "Leaving university context"))
              :match-func (lambda (msg)
                            (when msg (mu4e-message-contact-field-matches msg
                                       :to "nicolas.rougier@u-bordeaux.fr")))
              :vars `((user-mail-address . "nicolas.rougier@u-bordeaux.fr"  )
                      (user-full-name . "Nicolas P. Rougier (university)" )
                      (mu4e-compose-signature . ,mu4e-signature)
                      (mu4e-sent-folder . "/univ/sent")
                      (mu4e-trash-folder . "/univ/trash")
                      (mu4e-drafts-folder . "/univ/drafts")
                      (mu4e-maildir-shortcuts . (("/univ/inbox" . ?i)
                                                 ("/univ/archive" . ?a)
                                                 ("/univ/sent" . ?s)))
                      (smtpmail-smtp-server . "smtpauth.u-bordeaux.fr")
                      (smtpmail-stream-type . starttls)
                      (smtpmail-smtp-service . 587))))

GMail

To store the password in OSX keychain:

=security add-internet-password -l ‘smtp.gmail.com -s ‘smtp.gmail.com’ -a ‘nicolas.rougier@gmail.com’ -P 587 -r smtp -T Emacs -U -w “password12345”=

(add-to-list 'mu4e-contexts
             (make-mu4e-context
              :name "gmail"
              :enter-func (lambda () (mu4e-message "Entering gmail context"))
              :leave-func (lambda () (mu4e-message "Leaving gmail context"))
              :match-func (lambda (msg)
                            (when msg (mu4e-message-contact-field-matches msg
                                      :to "nicolas.rougier@gmail.com")))
              :vars `((user-mail-address . "nicolas.rougier@gmail.com"  )
                      (user-full-name . "Nicolas P. Rougier (gmail)" )
                      ;; don't save messages to Sent Messages,
                      ;; Gmail/IMAP takes care of this
                      ;; (mu4e-sent-messages-behavior 'delete)
                      (mu4e-compose-signature . ,mu4e-signature)
                      (mu4e-sent-folder . "/gmail/sent")
                      (mu4e-trash-folder . "/gmail/trash")
                      (mu4e-drafts-folder . "/gmail/drafts")
                      (mu4e-maildir-shortcuts . (("/gmail/inbox" . ?i)
                                                 ("/gmail/archive". ?a)
                                                 ("/gmail/sent" . ?s)))
                      (smtpmail-smtp-server . "smtp.gmail.com")
                      (smtpmail-stream-type . starttls)
                      (smtpmail-smtp-service . 587))))

Inria

(add-to-list 'mu4e-contexts
             (make-mu4e-context
              :name "inria"
              :enter-func (lambda () (mu4e-message "Entering inria context"))
              :leave-func (lambda () (mu4e-message "Leaving inria context"))
              :match-func (lambda (msg)
                            (when msg (mu4e-message-contact-field-matches msg
                                       :to "nicolas.rougier@inria.fr")))
              :vars `((user-mail-address . "nicolas.rougier@inria.fr")
                      (user-full-name . "Nicolas P. Rougier (inria)")
                      (mu4e-compose-signature . ,mu4e-signature)
                      (mu4e-sent-folder . "/inria/sent")
                      (mu4e-trash-folder . "/inria/trash")
                      (mu4e-drafts-folder . "/inria/drafts")
                      (mu4e-maildir-shortcuts . (("/inria/inbox"   . ?i)
                                                 ("/inria/archive" . ?a)
                                                 ("/inria/sent"    . ?s)))
                      (smtpmail-smtp-server . "smtp.inria.fr")
                      (smtpmail-stream-type . starttls)
                      (smtpmail-smtp-service . 587))))

Encryption

See https://www.djcbsoftware.nl/code/mu/mu4e/Reading-messages.html

(require 'epg-config)

(setq epg-gpg-program "/usr/local/bin/gpg"  ; What gpg program to use
      epg-user-id "gpg_key_id"              ; GnuPG ID of your default identity
      mml2015-use 'epg                      ; The package used for PGP/MIME.
      mml2015-encrypt-to-self t             ; Add our own key ID to recipient list
      mml2015-sign-with-sender t)           ; Use message sender to find a key to sign with.

;;(setq epa-file-cache-passphrase-for-symmetric-encryption nil)
;;(require 'epa-file)
;;(epa-file-enable)
;;(setq epa-file-select-keys nil)
(setq epa-pinentry-mode 'loopback)
(pinentry-start)

Read

Various settings

(setq mu4e-show-images t
      mu4e-use-fancy-chars nil
      mu4e-view-html-plaintext-ratio-heuristic  most-positive-fixnum
      mu4e-html2text-command 'mu4e-shr2text
      shr-use-fonts nil   ; Simple HTML Renderer / no font
      shr-use-colors nil) ; Simple HTML Renderer / no color

n/p for nevigating unread mails

(bind-key "n" #'mu4e-headers-next-unread mu4e-headers-mode-map)
(bind-key "p" #'mu4e-headers-prev-unread mu4e-headers-mode-map)

Custom faces

(set-face-attribute 'mu4e-system-face nil :inherit 'nano-critical)
(set-face-attribute 'mu4e-header-marks-face nil :inherit 'nano-critical)
(set-face-attribute 'mu4e-header-highlight-face nil :inherit 'nano-salient-i)
 

Write

See www.gnu.org/software/emacs/manual/html_node/message/Insertion-Variables.html

(setq message-send-mail-function 'smtpmail-send-it
      message-cite-reply-position 'below
      message-citation-line-format "%N [%Y-%m-%d at %R] wrote:"
      message-citation-line-function 'message-insert-formatted-citation-line
      message-yank-prefix       "> "
      message-yank-cited-prefix "> "
      message-yank-empty-prefix "> "
      message-indentation-spaces 1
      message-kill-buffer-on-exit t

      mu4e-compose-format-flowed t
      mu4e-compose-complete-only-personal t
      mu4e-compose-complete-only-after "2021-01-01" ; Limite address auto-completion
      mu4e-compose-dont-reply-to-self t
      mu4e-compose-crypto-reply-policy 'sign-and-encrypt)
      

Multi-language flyspell in compose mode

(require 'flyspell)
(require 'guess-language)
(require 'flyspell-correct-popup)

;; (bind-key "C-;" #'flyspell-popup-wrapper flyspell-mode-map)
(bind-key "C-;" #'flyspell-popup-correct flyspell-mode-map)

;; Automatically detect language for Flyspell
(with-eval-after-load 'guess-language  
    (add-hook 'text-mode-hook #'guess-language-mode)
    (setq guess-language-langcodes '((en . ("en_GB" "English"))
                                     (fr . ("fr_FR" "French")))
          guess-language-languages '(en fr)
          guess-language-min-paragraph-length 45))

(setq flyspell-generic-check-word-predicate  'mail-mode-flyspell-verify)
(defun my/mu4e-compose-hook ()
  "Settings for message composition."

  (auto-save-mode -1)
  (turn-off-auto-fill)
  (set-fill-column 79)
  (setq flyspell-generic-check-word-predicate
        'mail-mode-flyspell-verify)
  (flyspell-mode))

(add-hook 'mu4e-compose-mode-hook #'my/mu4e-compose-hook)

Bookmarks

(setq mu4e-bookmarks
      '((:name "Unread"
         :key ?u
         :show-unread t
         :query "flag:unread AND NOT flag:trashed")

        (:name "Inbox"
         :key ?i
         :show-unread t 
         :query "m:/inria/inbox or m:/univ/inbox or m:/gmail/inbox")

        (:name "Today"
         :key ?t
         :show-unread t 
         :query "date:today..now")

        (:name "Yesterday"
         :key ?y
         :show-unread t
         :query "date:2d..today and not date:today..now")

        (:name "Last week"
         :key ?w
         :hide-unread t 
         :query "date:7d..now")

        (:name "Flagged"
         :key ?f
         :show-unread t 
         :query "flag:flagged")

        (:name "Sent"
         :key ?s
         :hide-unread t 
         :query "from:Nicolas.Rougier")

        (:name "Drafts"
         :key ?d
         :hide-unread t
         :query "flag:draft")))

Tags

This provides a tag action inside the mu4e headers view to quickly tag message. Tags are saved in the mu4e-tag-history variable that is also saved from one session to the other. We use the consult-completing-read-multiple function to read tags.

(defvar mu4e-tag-history '()
  "Mu4e tag history list.")

(add-to-list 'savehist-additional-variables 'mu4e-tag-history)

(put 'mu4e-tag-history 'history-length 100)

Here is the actual tag action

(defun mu4e-tag-read (target msg)
  "Ask for tags to be added and/or removed."

  (let* ((tags nil)
         (old-tags (mu4e-message-field msg :tags))
         (new-tags (completing-read-multiple
                    "TAGS: "
                    mu4e-tag-history
                    nil
                    nil
                    (mapconcat #'identity old-tags ",")
                    'mu4e-tag-history)))
      (dolist (tag old-tags)
        (let ((tag (string-trim tag)))
          (if (and (> (length tag) 0)
                   (not (member tag new-tags)))
              (push (concat "-" tag) tags))))
      (dolist (tag new-tags)
        (let ((tag (string-trim tag)))
          (if (and (> (length tag) 0)
                   (not (member tag old-tags)))
              (push (concat "+" tag) tags))))
      (mapconcat #'identity tags ",")))

;; Add the mark to mu4e. If the action does nothing, the header is marked anyway.
;; I Did not find a way to cancel the marks
(add-to-list 'mu4e-marks
             '(tag
               :char       "g"
               :prompt     "gtag"
               :dyn-target mu4e-tag-read
               :action      (lambda (docid msg target)
                              (when (> (length target) 0)
                                  (mu4e-action-retag-message msg target)))))

;; Tell mu4e about the new mark
;; See https://www.djcbsoftware.nl/code/mu/mu4e/Adding-a-new-kind-of-mark.html 
(mu4e~headers-defun-mark-for tag)

We bind the tagging with the “g” key in mu4e-headers-mode.

(bind-key "g" #'mu4e-headers-mark-for-tag mu4e-headers-mode-map)

Folding

Some face specification for folding.

(require 'mu4e-folding)

(set-face-attribute 'mu4e-folding-root-folded-face nil
                    :background (face-background 'default)
                    :extend t)
(set-face-attribute 'mu4e-folding-root-unfolded-face nil
                    :background (face-background 'default)
                    :extend t)
(set-face-attribute 'mu4e-folding-child-folded-face nil
                    :background (face-background 'default)
                    :extend t)
(set-face-attribute 'mu4e-folding-child-unfolded-face nil
                    :background (face-background 'default)
                    :extend t)

Layout for mu4e

Headers

(setq mu4e-headers-fields
      '((:my/date       . 12)
        (:my/flags      . 1)
        (:my/from       . 28)
        (:my/subject    . nil)))

(setq mu4e-headers-thread-root-prefix          '(""   . "")
      mu4e-headers-thread-first-child-prefix   '(""   . "")
      mu4e-headers-thread-child-prefix         '(""   . "")
      mu4e-headers-thread-last-child-prefix    '(""   . "")
      mu4e-headers-thread-connection-prefix    '("| " . "| ")
      mu4e-headers-thread-blank-prefix         '(""   . "")
      mu4e-headers-thread-orphan-prefix        '(""   . "")
      mu4e-headers-thread-single-orphan-prefix '(""   . "")
      mu4e-headers-thread-duplicate-prefix     '("="  . "="))

(plist-put (cdr (assq 'refile   mu4e-marks)) :char "")
(plist-put (cdr (assq 'trash    mu4e-marks)) :char "")
(plist-put (cdr (assq 'action   mu4e-marks)) :char "")
(plist-put (cdr (assq 'untrash  mu4e-marks)) :char "")
(plist-put (cdr (assq 'delete   mu4e-marks)) :char "×")
(plist-put (cdr (assq 'flag     mu4e-marks)) :char "")
(plist-put (cdr (assq 'unflag   mu4e-marks)) :char "")
(plist-put (cdr (assq 'move     mu4e-marks)) :char "")
(plist-put (cdr (assq 'tag      mu4e-marks)) :char "")

Colorization of recent emails

(defun my/mu4e-headers-colorize (item msg)
  "Colorize item depending on whe msg was received"
  
  (let* ((recent (* 5 60)) ;; 5 minutes
         (now (current-time))
         (unread (mu4e-message-field msg :unread))
         (date (mu4e-message-field msg :date))
         (delta (float-time (time-subtract now date))))
    (if (and (< delta recent) unread)
        (propertize item 'face 'nano-salient)
      item)))

Date (custom info)

(require 'relative-date)

(defun my/mu4e-headers-date (msg)
  (let* ((date (mu4e-message-field msg :date)))
    (my/mu4e-headers-colorize
       (format "%12s" (relative-date date)) msg)))

(add-to-list 'mu4e-header-info-custom
                 '(:my/date . (:name "my/date"
                               :shortname "D"
                               :function my/mu4e-headers-date)))

From (custom info)

(defun my/mu4e-headers-from (msg)
  (let* ((from        (my/mu4e-get-sender msg))
         (meta        (when msg (mu4e-message-field msg :meta)))
         (root        (when meta (plist-get meta :root)))
         (orphan      (when meta (plist-get meta :orphan)))
         (first-child (when meta (plist-get meta :first-child)))
         (has-child   (when meta (plist-get meta :has-child)))
         (root        (when meta (plist-get meta :root)))
         (from        (cond ((and root has-child)       (concat "" from))
                            ((and orphan first-child)   (concat "" from))
                            ((and root (not has-child)) (concat "" from))
                            (t                          (concat "" from)))))
    (my/mu4e-headers-colorize from msg)))

(add-to-list 'mu4e-header-info-custom
             '(:my/from . (:name "my/from"
                           :shortname "F"
                           :function my/mu4e-headers-from)))

Flags (custom info)

(defun my/mu4e-headers-flags (msg)
  (let* ((size (mu4e-message-field msg :size))
         (flags (cond ((memq 'flagged   (mu4e-message-field msg :flags)) "")
                     ((and (> size 256000)
                           (memq 'attach    (mu4e-message-field msg :flags))) "")
                     ((memq 'replied   (mu4e-message-field msg :flags)) "")
                     ((memq 'draft     (mu4e-message-field msg :flags)) "")
                     ((memq 'trashed   (mu4e-message-field msg :flags)) "")
                     ((memq 'encrypted (mu4e-message-field msg :flags)) "")
                     (t ""))))
    (my/mu4e-headers-colorize flags msg)))

(add-to-list 'mu4e-header-info-custom
             '(:my/flags . (:name "my/flags"
                            :shortname "F"
                            :function my/mu4e-headers-flags)))

Tags (custom info)

(defface mu4e-tag-face
  '((t :inherit (nano-popout nano-strong)))
  "Face for message tags"
  :group 'mu4e-faces)

(defun my/mu4e-headers-tags (msg)
  (let* ((tags (mu4e-message-field msg :tags)))
    (if tags
        (propertize (format "[%s]" (mapconcat #'identity tags ","))
                    'face 'mu4e-tag-face)
      "")))
(add-to-list 'mu4e-header-info-custom
             '(:my/tags . (:name "my/tags"
                           :shortname "T"
                           :function my/mu4e-headers-tags)))

Thread count (custom info)

This is an empty info field (4 non-breaking spaces) that will be overwritten by folding mode.

(defun my/mu4e-headers-thread-counter (msg)
  "    ") ;; NON-BREAKING spaces
(add-to-list 'mu4e-header-info-custom
             '(:my/counter . (:name "my/counter"
                              :shortname "#"
                              :function my/mu4e-headers-thread-counter)))

Subject (custom info)

(defun my/mu4e-headers-subject (msg)
  (let* ((thread (mu4e-message-field msg :meta))
         (prefix (mu4e~headers-thread-prefix thread))
         (subject (mu4e-message-field msg :subject))
         (subject (concat prefix subject " " (my/mu4e-headers-tags msg))))
    (my/mu4e-headers-colorize subject msg)))

(add-to-list 'mu4e-header-info-custom
             '(:my/subject . (:name "my/subject"
                              :shortname "S"
                              :function my/mu4e-headers-subject)))

Folding

(require 'mu4e-folding)

;; (defun mu4e-folding--make-root-overlay (beg end)
;;   "Create the root overlay."
  
;;   (let* ((buffer-read-only)
;;          (overlay (car (mu4e-folding--children-overlay)))
;;          (count (if overlay
;;                     (overlay-get overlay 'mu4e-folding-children-count)
;;                   0)))
;;     (save-excursion
;;       (goto-char beg)
;;       (when (search-forward "" end t)
;;         (put-text-property (- (point) 1) (point)
;;                            'display (svg-lib-tag (format "%d" count) nil
;;                                                  :ascent 'center)))))
;;   (save-excursion
;;     (let* ((match (search-forward "  " end t)))
;;       (if match
;;           (make-overlay (- match 2) end)
;;         (make-overlay beg end)))))

;; (defun mu4e-folding--make-root-overlay (beg end)
;;   "Create the root overlay."
 
;;   (let* ((buffer-read-only)
;;          (overlay (car (mu4e-folding--children-overlay)))
;;          (count (if overlay
;;                     (overlay-get overlay 'mu4e-folding-children-count)
;;                   0))
;;          (tag (format "%d" count))
;;          (tag (svg-lib-tag tag nil
;;                            :margin (- 4 (length tag))
;;                            :alignment 1.0
;;                            )))
;;     (save-excursion
;;       (goto-char beg)
;;       (when (search-forward "    " end t)
;;         (set-text-properties (- (point) 4) (+ (point) 1)
;;                              `(display ,tag)))))
 
;;   (let ((buffer-read-only)
;;         (overlay (make-overlay beg end)))
;;     overlay))

Threads separation

(defun my/mu4e-headers-is-root ()
  "Check if message at point is the root of a thread"
  (let* ((msg  (get-text-property (point) 'msg))
         (meta (when msg (mu4e-message-field msg :meta)))
         (orphan      (when meta (plist-get meta :orphan)))
         (first-child (when meta (plist-get meta :first-child)))
         (has-child   (when meta (plist-get meta :has-child)))
         (root     (when meta (plist-get meta :root))))

    (or root (and orphan first-child))))

;; my/mu4e-headers-is-root

(defun my/mu4e-headers-separate-root ()
  (save-excursion
    (let ((buffer-read-only))
      (goto-char (point-min))
      (while (not (eobp))
        (when (my/mu4e-headers-is-root)
          (goto-char (line-beginning-position))
          (unless (or (eq (point) 1)
                      (and (> (point) (point-min))
                           (get-text-property (- (point) 1) 'mu4e-separator)))
            (insert (propertize "\n"
                                'mu4e-separator t
                                'face '(:inherit nano-subtle-i
                                        :strike-through t
                                        :extend t)))))
        (forward-line)))))

(add-hook 'mu4e-headers-found-hook #'my/mu4e-headers-separate-root)

Layout for mu4e (1.8, alternative)

A custom multiline headers view for mu4e.

(defun my/mu4e-headers-multiline (msg)
  "A multiline headers mode."
  
  (let* ((sender  (my/mu4e-get-sender msg))
         (date (mu4e-message-field msg :date))
         (date (concat (propertize "" 'display " ")
                       (format "%16s" (my/date-relative date))))
         (subject (mu4e-message-field msg :subject))
         (subject (truncate-string-to-width subject (- (window-width) 16) nil nil ""))
         (flagged   (memq 'flagged   (mu4e-message-field msg :flags)))
         (attach    (memq 'attach    (mu4e-message-field msg :flags)))
         (unread    (memq 'unread    (mu4e-message-field msg :flags)))
         (replied   (memq 'replied   (mu4e-message-field msg :flags)))
         (encrypted (memq 'encrypted (mu4e-message-field msg :flags)))
         (draft     (memq 'draft     (mu4e-message-field msg :flags)))
         (thread (mu4e-message-field msg :meta))
         (related (and thread (plist-get thread :related)))
         (prefix (mu4e~headers-thread-prefix thread))
         (root (plist-get thread :root))
         (orphan (plist-get thread :orphan))
         (first-child (plist-get thread :first-child))
         (has-child (plist-get thread :has-child))
         (level (plist-get thread :level))
         (root (or root (and orphan (or first-child has-child))))
         (child (and thread (not root)))
         (tags      (mu4e-message-field msg :tags))
         (unread-mark (propertize (cond (unread               (propertize "" 'face 'nano-salient))
                                        ((and root has-child) "  ")
                                        (t                    "  "))))
         (one-line (and child mu4e-headers-include-related))
         (face-sender (cond (unread               '(nano-salient nano-strong))
                            ((and root related)   '(nano-strong nano-faded))
                            (root                '(nano-strong nano-default))
                            ((and child related) '(:inherit nano-faded :height 140))
                            (child               '(:inherit nano-default :height 140))
                            (t                   '(nano-default))))
         (face-subject (cond (unread  '(:inherit nano-salient))
                             (related '(:inherit nano-faded))
                             (t       '(:inherit nano-default))))
         (face-tags (cond (related '(:inherit (nano-faded)              :height 120))
                          (t       '(:inherit (nano-popout nano-strong) :height 120))))
         (face-date    (cond (t '(:inherit nano-faded :height 140))))
         
         (icons (string-join
                 `(,@(if draft   `( ,(propertize "" 'face 'nano-faded)))
                   ,@(if attach  `( ,(propertize "" 'face 'nano-faded)))
                   ,@(if flagged `( ,(propertize "" 'face 'nano-salient)))
                   ) " ")))

    (concat

     ;; Separaction line between threads
     (when root
       (concat
        (propertize " "
                    'mu4e-root t
                    'display `((margin left-margin) "  "))
        (propertize "-" 'display "\n"
                    'face '(:extend t
                            :strike-through t
                            :inherit nano-subtle-i))
        "  ")) ;; !! NON-BREAKING SPACE -> will be searched later

     ;; Children are always indented (relatively to root)
     (when (and child one-line)
       (concat 
        (propertize "" 'face 'nano-faded)))

     ;; Unread mark appears in the left margin
     (propertize " " 'face (if unread 'nano-default face-sender)
                     'display `((margin left-margin) ,unread-mark))

     ;; Sender
     (cond (one-line     (propertize (concat prefix sender)
                                     'face face-sender))
           ((and root has-child) (propertize (concat "" sender)
                                             'face face-sender))
           (t (propertize sender 'face face-sender)))
     
     " "
     ;; Replied
     (when replied
         (propertize "" 'face face-sender))

     ;; In one line mode (children), icons are displayed next to sender
     (when one-line
         (concat (propertize icons)
                 " "))

     ;; Tags next to sender
     (when tags
       (concat 
        (propertize "" 'face face-tags)
        (mapconcat #'(lambda (tag)
                       (propertize tag 'face face-tags))
                   tags (propertize "," 'face face-tags))))

     ;; Spacing to have date aligned on the right
     (propertize " " 'display `(space :align-to (- right 1 ,(* 1.0 (length date)))))

     ;; Date
     (propertize date 'face face-date)

     ;; When not a child
     (when (or root (not mu4e-headers-include-related))
       (concat

        ;; Second line. We use a display property such that hl-line-mode works correctly.
        (propertize " " 'display "\n") ;; NON-BREAKING space for later search (see below)

        ;; Blank spaces in the margin (for nice hl-line-mode)
        (propertize " " 'face '(nano-strong nano-salient)
                        'display `((margin left-margin) "  "))

        ;; Indentation (to compensate for the virtual "\n" we introduced before)
        (propertize "  ")

        ;; Align subject and sender when this is a child
        (when (and one-line child)
            (propertize "  " 'face 'nano-faded))

        ;; Subject
        (propertize subject 'face face-subject)

        ;; Spacing to have icons aligned on the right
        (propertize " " 'display `(space :align-to (- right ,(length icons) 1)))

        ;; Icons on the right
        (propertize icons))))))

Then, we redefine thread symbols (we’ll mostly manage ourselves).

(setq mu4e-headers-thread-root-prefix          '(""    . "")
      mu4e-headers-thread-first-child-prefix   '(" "   . " ")
      mu4e-headers-thread-child-prefix         '(" "   . " ")
      mu4e-headers-thread-last-child-prefix    '(" "   . " ")
      mu4e-headers-thread-connection-prefix    '(" |"  . "  ")
      mu4e-headers-thread-blank-prefix         '(""    . "")
      mu4e-headers-thread-orphan-prefix        '(" "   . "")
      mu4e-headers-thread-single-orphan-prefix '(""    . "")
      mu4e-headers-thread-duplicate-prefix     '("="   . "="))

And we install the new header.

;; (add-to-list 'mu4e-header-info-custom
;;              '(:multiline . (:name "multiline"
;;                              :shortname ""
;;                              :function my/mu4e-headers-multiline)))
;; (setq mu4e-headers-fields  '((:multiline . nil)))

Margins

Because the multiline header view uses margin to show new mail, we have to make sure there are always margin in the headers view. We also make the hl-line more salient.

(defun my/mu4e-headers-mode-setup ()

  (with-current-buffer "*mu4e-headers*"
    (set-face-attribute 'mu4e-header-highlight-face nil
                        :inherit 'nano-salient-i)
    (setq-local left-margin-width 2)
    (setq-local cursor-type nil)
    (set-window-buffer nil "*mu4e-headers*")))

(add-hook 'mu4e-headers-found-hook #'my/mu4e-headers-mode-setup)
(add-hook 'mu4e-headers-mode-hook  #'my/mu4e-headers-mode-setup)

Navigation

Since a header can now be displayed over several consecutive lines, we need to remap prev/next line such as to use mu4e prev/next functions (that works properly with multiline headers).

(bind-key [remap next-line] #'mu4e-headers-next mu4e-headers-mode-map)
(bind-key [remap previous-line] #'mu4e-headers-prev mu4e-headers-mode-map)

Highlight

A special highlight function for root headers that takes the multiline root header into account.

(defun my/mu4e-hl-line-move ()

  (save-excursion
    (let* ((beg (line-beginning-position))
           (end (min (line-beginning-position 2) (point-max)))
           (match1 (search-forward "  " end t))
           (match2 (search-forward "" end t)))
      (goto-char beg)
      (cond (match1 (cons (- match1 2) end))
            (match2 (cons (+ match2 1) end))
            (t      (cons beg end))))))

We retrict the hack to mu4e-header-mode

(defun my/mu4e-headers-mode-hl-line-move ()
    (setq-local hl-line-range-function #'my/mu4e-hl-line-move))

(add-hook 'mu4e-headers-mode-hook  #'my/mu4e-headers-mode-hl-line-move)

Folding

A special overlay function for folding that takes the multiline root header into account & insert the number of children at the root level (replacing “” symbol)

(defun mu4e-folding--make-root-overlay (beg end)
  "Create the root overlay."
  
  (let* ((buffer-read-only)
         (overlay (car (mu4e-folding--children-overlay)))
         (count (if overlay
                    (overlay-get overlay 'mu4e-folding-children-count)
                  0)))
    (save-excursion
      (goto-char beg)
      (when (search-forward "" end t)
        (put-text-property (- (point) 1) (point)
                           'display (svg-lib-tag (format "%d" count) nil
                                                 :ascent 'center)))))
  (save-excursion
    (let* ((match (search-forward "  " end t)))
      (if match
          (make-overlay (- match 2) end)
        (make-overlay beg end)))))

(defun my/mu4e-move-mark-overlay ()
   (interactive)
   (let* ((beg (line-beginning-position))
          (end (line-beginning-position 2))
          (overlays (overlays-in beg end)))
     (dolist (overlay overlays)
       (when (overlay-get overlay 'mu4e-mark)
         (move-overlay overlay
                       (overlay-start overlay)
                       (+ (overlay-start overlay) 2))))))

Dashboard

Provide a mu4e-dashboard command that opens the mu4e dashboard on the left side.

(require 'mu4e-dashboard)
(require 'svg-lib)

(setq mu4e-dashboard-propagate-keymap nil)

(defun mu4e-dashboard ()
  "Open the mu4e dashboard on the left side."
  
  (interactive)
  (with-selected-window
      (split-window (selected-window) -34 'left)

    (find-file (expand-file-name "mu4e-dashboard.org" user-emacs-directory))
    (mu4e-dashboard-mode)
    (hl-line-mode)
    (set-window-dedicated-p nil t)
    (defvar svg-font-lock-keywords
      `(("\\!\\([\\ 0-9]+\\)\\!"
         (0 (list 'face nil 'display (svg-font-lock-tag (match-string 1)))))))
    (defun svg-font-lock-tag (label)
      (svg-lib-tag label nil
                   :stroke 0 :margin 1 :font-weight 'bold
                   :padding (max 0 (- 3 (length label)))
                   :foreground (face-foreground 'nano-popout-i)
                   :background (face-background 'nano-popout-i)))
    (push 'display font-lock-extra-managed-props)
    (font-lock-add-keywords nil svg-font-lock-keywords)
    (font-lock-flush (point-min) (point-max))))

Benchmark

(my/report-time "Mail")

Bibliography

Bibtex

Settings for bibtex

(setq bibtex-autokey-titleword-length 0
      bibtex-autokey-name-year-separator ":"
      bibtex-autokey-name-case-convert-function 'capitalize
      bibtex-autokey-year-length 4
      bibtex-autokey-names 1
      bibtex-autokey-titleword-separator ""
      bibtex-autokey-year-title-separator ""
      bibtex-autokey-edit-before-use nil
      imenu-list-position 'left
      imenu-list-size 100
      org-imenu-depth 2
      org-image-actual-width `( ,(truncate (* (frame-pixel-width) 0.85)))
      org-startup-with-inline-images t)

Org bib mode

Dedicated header line for org-bib-mode

(defun my/org-bib-mode-hook ()

  (with-current-buffer "*Ilist*"
    (setq header-line-format
        '(:eval
          (nano-modeline-render nil
                                (buffer-name imenu-list--displayed-buffer)
                                (format "(view mode: %s, filter: %s)"
                                        (if (eq org-bib--view-mode-current 'none)
                                            "-"
                                          org-bib--view-mode-current)
                                        (if (eq org-imenu-filter-string "*")
                                            "-"
                                          org-imenu-filter-string))
                                "")))
    (face-remap-add-relative 'hl-line :inherit 'nano-strong-i)))

(add-hook 'org-bib-mode-hook #'my/org-bib-mode-hook)

A shortcut to edit bibliography

(defun my/biblio ()
  "Create a new frame for editing bibliography"
  
  (interactive)
  (require 'org-bib)
  (setq imenu-list-position 'left
        imenu-list-size 100
        org-imenu-depth 2)

  (select-frame (make-frame '((name . "my/biblio")
                              (width . 180)
                              (height . 45))))
  (find-file "~/Documents/Papers/papers.org")
  (org-bib-mode))

An autoload function for my/config (that will load org mode).

(autoload 'my/biblio
  (expand-file-name "init.el" user-emacs-directory)
  "Autoloaded my/config command."
  t)

Org

(setq my/section-start-time (current-time))

General

(setq-default org-directory "~/Documents/org"
              org-ellipsis ""              ; Nicer ellipsis
              org-tags-column 1              ; Tags next to header title
              org-hide-emphasis-markers t    ; Hide markers
              org-cycle-separator-lines 2    ; Number of empty lines between sections
              org-use-tag-inheritance nil    ; Tags ARE NOT inherited 
              org-use-property-inheritance t ; Properties ARE inherited
              org-indent-indentation-per-level 2 ; Indentation per level
              org-link-use-indirect-buffer-for-internals t ; Indirect buffer for internal links
              org-fontify-quote-and-verse-blocks t ; Specific face for quote and verse blocks
              org-return-follows-link nil    ; Follow links when hitting return
              org-image-actual-width nil     ; Resize image to window width
              org-indirect-buffer-display 'other-window ; Tab on a task expand it in a new window
              org-outline-path-complete-in-steps nil) ; No steps in path display

Better latex preview (see https://stackoverflow.com/questions/30151338)

(setq org-latex-create-formula-image-program 'dvisvgm)

We adapt fill functions according to the indent level.

(defun my/calc-offset-on-org-level ()
  "Calculate offset (in chars) on current level in org mode file."

  (* (or (org-current-level) 0) org-indent-indentation-per-level))

(defun my/org-fill-paragraph (&optional justify region)
  "Calculate apt fill-column value and fill paragraph."
 
  (let* ((fill-column (- fill-column (my/calc-offset-on-org-level))))
    (org-fill-paragraph justify region)))

(defun my/org-auto-fill-function ()
  "Calculate apt fill-column value and do auto-fill"
 
  (let* ((fill-column (- fill-column (my/calc-offset-on-org-level))))
    (org-auto-fill-function)))

(defun my/org-mode-hook ()
  (setq fill-paragraph-function #'my/org-fill-paragraph
        normal-auto-fill-function #'my/org-auto-fill-function))

(add-hook 'org-load-hook 'my/org-mode-hook)
(add-hook 'org-mode-hook 'my/org-mode-hook)

A shortcut for emacs-lisp source blocks. Type “<S” (in org-mode) then press tab.

(require 'org-tempo)
(add-to-list 'org-structure-template-alist
             '("S" . "src emacs-lisp"))

Babel

(setq-default org-src-fontify-natively t         ; Fontify code in code blocks.
              org-adapt-indentation nil          ; Adaptive indentation
              org-src-tab-acts-natively t        ; Tab acts as in source editing
              org-confirm-babel-evaluate nil     ; No confirmation before executing code
              org-edit-src-content-indentation 0 ; No relative indentation for code blocks
              org-fontify-whole-block-delimiter-line t) ; Fontify whole block

Benchmark

(my/report-time "Org")

Agenda

(setq my/section-start-time (current-time))

General

Load libraries.

(require 'org-agenda)
(require 'org-agenda-property)

Open agenda(s)

(bind-key "C-c a" #'org-agenda)

Files

(setq org-agenda-files (list "~/Documents/org/agenda.org"
                             "~/Documents/org/students.org"
                             "~/Documents/org/todo.org"
                             "~/Documents/org/inbox.org")
      org-agenda-diary-file (expand-file-name "diary" user-emacs-directory))

Settings

(setq org-agenda-window-setup 'current-window
      org-agenda-restore-windows-after-quit t
      org-agenda-show-all-dates nil
      org-agenda-time-in-grid t
      org-agenda-show-current-time-in-grid t
      org-agenda-start-on-weekday 1
      org-agenda-span 7
      org-agenda-hide-tags-regexp "." ; No tags
    ; org-agenda-hide-tags-regexp nil) ; All tags
      org-agenda-tags-column 0
    ; org-agenda-tags-column -79)      ; Left aling
      org-agenda-block-separator nil
      org-agenda-category-icon-alist nil
      org-agenda-skip-deadline-if-done t
      org-agenda-skip-scheduled-if-done t
      org-agenda-sticky t)

Prefix format

(setq org-agenda-prefix-format
      '((agenda . "%i %?-12t%s")
        (todo .   "%i")
        (tags .   "%i")
        (search . "%i")))

Sorting strategy

(setq org-agenda-sorting-strategy
      '((agenda deadline-down scheduled-down todo-state-up time-up
                habit-down priority-down category-keep)
        (todo   priority-down category-keep)
        (tags   timestamp-up priority-down category-keep)
        (search category-keep)))

Minimal time grid

(setq org-agenda-time-grid
      '((daily today require-timed)
        ()
        "......" "----------------"))

(setq org-agenda-current-time-string "   now")

A small function to cancel a meeting

(defun my/org-cancel-meeting ()
  (interactive)
  (org-entry-put (point) "CATEGORY" "cancelled")
  (org-entry-put (point) "NOTE" "Cancelled")
  (org-set-tags ":CANCELLED:"))

Holidays

(require 'cal-iso)
(require 'holidays)

(defvar french-holidays nil
  "French holidays")

(setq french-holidays
      `((holiday-fixed 1 1 "New year's Day")
	    (holiday-fixed 5 1 "Labour Day")
	    (holiday-fixed 5 8 "Victory in Europe Day")
	    (holiday-fixed 7 14 "Bastille day")
	    (holiday-fixed 8 15 "Assumption of Mary")
	    (holiday-fixed 11 11 "Armistice 1918")
	    (holiday-fixed 11 1 "All Saints' Day")
	    (holiday-fixed 12 25 "Christmas Day")
	    (holiday-easter-etc 0 "Easter Sunday")
        (holiday-easter-etc 1 "Easter Monday")
        (holiday-easter-etc 39 "Ascension Day")
        (holiday-easter-etc 50 "Whit Monday")
        (holiday-sexp
         '(if (equal
               (holiday-easter-etc 49)
               (holiday-float 5 0 -1 nil))
              (car (car (holiday-float 6 0 1 nil)))
            (car (car (holiday-float 5 0 -1 nil))))
         "Mother's Day")))

(setq calendar-holidays french-holidays     ; French holidays
      calendar-week-start-day 1             ; Week starts on Monday
      calendar-mark-diary-entries-flag nil) ; Do not show diary entries

; Mark today in calendar
(add-hook 'calendar-today-visible-hook  #'calendar-mark-today)

Week day name with holidays

(defun my/org-agenda-format-date (date)
  "Org agenda date format displaying holidays"
  (let* ((dayname (calendar-day-name date))
	     (day (cadr date))
	     (month (car date))
	     (monthname (calendar-month-name month))
	     (year (nth 2 date))
         (holidays (calendar-check-holidays date)))
    (concat "\n"
            dayname " "
            (format "%d " day)
            monthname " "
            (format "%d" year)
            (if holidays (format " (%s)" (nth 0 holidays)))
            "\n")))

(setq org-agenda-format-date #'my/org-agenda-format-date)

Daily agenda

The daily agenda

(add-to-list 'org-agenda-custom-commands
             '("a" "Agenda"
               ((agenda "Agenda"
                        ((org-agenda-todo-keyword-format "%s")
                         (org-agenda-skip-deadline-if-done nil)
                         (org-deadline-warning-days 3)
                         (org-agenda-overriding-header nil))))))

Some decorations for the agenda

(defun my/org-agenda-highlight-todo (x)
  (let* ((done (string-match-p (regexp-quote ":DONE:") x))
         (canceled (string-match-p (regexp-quote "~") x))
         (x (replace-regexp-in-string ":TODO:" "" x))
         (x (replace-regexp-in-string ":DONE:" "" x))
         (x (replace-regexp-in-string "~" "" x))
         (x (if (and (boundp 'org-agenda-dim) org-agenda-dim)
                (propertize x 'face 'nano-faded) x))
         (x (if done (propertize x 'face 'nano-faded) x))
         (x (if canceled (propertize x 'face 'nano-faded) x)))
    x))

(advice-add 'org-agenda-highlight-todo
            :filter-return #'my/org-agenda-highlight-todo)

Timestamp tags for the agenda (bold means inverse video below):

now -> now 9:00 -> 9h00 9:30-10:00 -> 9h30 | 30mn -> ANYTIME

(require 'svg-lib)
(require 'svg-tag-mode)
 
(defun my/svg-tag-timestamp (&rest args)
  "Create a timestamp SVG tag for the time at point."
  
  (interactive)
  (let ((inhibit-read-only t))

    (goto-char (point-min))
    (while (search-forward-regexp
            "\\(\([0-9]/[0-9]\):\\)" nil t)
              (set-text-properties (match-beginning 1) (match-end 1)
                             `(display ,(svg-tag-make "ANYTIME"
                                                      :face 'nano-faded
                                                      :inverse nil
                                                      :padding 3 :alignment 0))))

    (goto-char (point-min))
    (while (search-forward-regexp
            "\\([0-9]+:[0-9]+\\)\\(\\.+\\)" nil t)

              (set-text-properties (match-beginning 1) (match-end 2)
                             `(display ,(svg-tag-make (match-string 1)
                                                       :face 'nano-faded
                                                       :margin 4 :alignment 0))))

    (goto-char (point-min))
    (while (search-forward-regexp
            "\\([0-9]+:[0-9]+\\)\\(\\.*\\)" nil t)

              (set-text-properties (match-beginning 1) (match-end 2)
                             `(display ,(svg-tag-make (match-string 1)
                                                      :face 'nano-default
                                                      :inverse t
                                                      :margin 4 :alignment 0))))
    (goto-char (point-min))
    (while (search-forward-regexp
            "\\([0-9]+:[0-9]+\\)\\(-[0-9]+:[0-9]+\\)" nil t)
      (let* ((t1 (parse-time-string (match-string 1)))
             (t2 (parse-time-string (substring (match-string 2) 1)))
             (t1 (+ (* (nth 2 t1) 60) (nth 1 t1)))
             (t2 (+ (* (nth 2 t2) 60) (nth 1 t2)))
             (d  (- t2 t1)))

        (set-text-properties (match-beginning 1) (match-end 1)
                                `(display ,(svg-tag-make (match-string 1)
                                                         :face 'nano-faded
                                                         :crop-right t)))
        ;; 15m: ¼, 30m:½, 45m:¾
        (if (< d 60)
             (set-text-properties (match-beginning 2) (match-end 2)
                                  `(display ,(svg-tag-make (format "%2dm" d)
                                                           :face 'nano-faded
                                                           :crop-left t :inverse t)))
           (set-text-properties (match-beginning 2) (match-end 2)
                                `(display ,(svg-tag-make (format "%1dH" (/ d 60))
                                                         :face 'nano-faded
                                                         :crop-left t :inverse t
                                                         :padding 2 :alignment 0))))))))
(add-hook 'org-agenda-mode-hook #'my/svg-tag-timestamp)
(advice-add 'org-agenda-redo :after #'my/svg-tag-timestamp)

Tasks agenda

A custom date format function using svg tags (progress pies) for the task agenda.

(defun my/org-agenda-custom-date ()
  (interactive)
  (let* ((timestamp (org-entry-get nil "TIMESTAMP"))
         (timestamp (or timestamp (org-entry-get nil "DEADLINE"))))
    (if timestamp
        (let* ((delta (- (org-time-string-to-absolute (org-read-date nil nil timestamp))
                         (org-time-string-to-absolute (org-read-date nil nil ""))))
               (delta (/ (+ 1 delta) 30.0))
               (face (cond ;; ((< delta 0.25) 'nano-popout)
                           ;; ((< delta 0.50) 'nano-salient)
                           ((< delta 1.00) 'nano-default)
                           (t 'nano-faded))))
          (concat
           (propertize " " 'face nil
                       'display (svg-lib-progress-pie
                                 delta nil
                                 :background (face-background face nil 'default)
                                 :foreground (face-foreground face)
                                 :margin 0 :stroke 2 :padding 1))
           " "
           (propertize
            (format-time-string "%d/%m" (org-time-string-to-time timestamp))
            'face 'nano-popout)))
      "     ")))

The task agenda

(add-to-list 'org-agenda-custom-commands
        '("x" "Tasks"
          ((todo "TODO" ;; "PROJECT"
                 ( (org-agenda-todo-keyword-format ":%s:")
                   (org-agenda-prefix-format '((todo   . " ")))
                   (org-agenda-skip-function '(org-agenda-skip-entry-if 'timestamp))
                   (org-agenda-overriding-header (propertize " Todo \n" 'face 'nano-strong))))

           (tags "+TALK+TIMESTAMP>=\"<now>\""
                 ((org-agenda-span 90)
                  (org-agenda-max-tags 5)
                  (org-agenda-prefix-format '((tags   . " %(my/org-agenda-custom-date) ")))
                  (org-agenda-overriding-header "\n Upcoming talks\n")))

           (tags "TEACHING+TIMESTAMP>=\"<now>\""
                 ((org-agenda-span 90)
                  (org-agenda-max-tags 5)
                  (org-agenda-prefix-format '((tags   . " %(my/org-agenda-custom-date) ")))
                  (org-agenda-overriding-header "\n Upcoming lessons\n")))

           (tags "TRAVEL+TIMESTAMP>=\"<now>\""
                 ((org-agenda-span 90)
                  (org-agenda-max-tags 5)
                  (org-agenda-prefix-format '((tags .  " %(my/org-agenda-custom-date) ")))
                  (org-agenda-overriding-header "\n Upcoming travels\n")))

           (tags "DEADLINE>=\"<today>\""
                  ((org-agenda-span 90)
                   (org-agenda-max-tags 5)
                   (org-agenda-prefix-format '((tags .  " %(my/org-agenda-custom-date) ")))
                   (org-agenda-overriding-header "\n Upcoming deadlines\n"))))))

Update

We install a time to refresh the daily agenda (a) at regular intervals such that the current time is up to date.

(defvar my/org-agenda-update-delay 60)
(defvar my/org-agenda-update-timer nil)

(defun my/org-agenda-update ()
  "Refresh daily agenda view"
  
  (when my/org-agenda-update-timer
    (cancel-timer my/org-agenda-update-timer))
  
  (let ((window (get-buffer-window "*Org Agenda(a)*" t)))
    (when window
      (with-selected-window window
        (let ((inhibit-message t))
          (org-agenda-redo)))))

  (setq my/org-agenda-update-timer
    (run-with-idle-timer
     (time-add (current-idle-time) my/org-agenda-update-delay)
     nil
     'my/org-agenda-update)))

(run-with-idle-timer my/org-agenda-update-delay t 'my/org-agenda-update)

Refile

Set refile targets according to my setup

(setq org-outline-path-complete-in-steps nil)
(setq org-refile-use-outline-path nil)
(setq org-refile-targets
      '(
        ("~/Documents/org/agenda.org"    :maxlevel . 2)
        ("~/Documents/org/inbox.org"     :maxlevel . 1)
;;        ("~/Documents/org/organizer.org" :tag      . "tasks")
;;        ("~/Documents/org/organizer.org" :tag      . "notes")
;;        ("~/Documents/org/organizer.org" :tag      . "mails")
;;        ("~/Documents/org/organizer.org" :tag      . "meetings")
        ;;("~/Documents/org/archives.org" :maxlevel . 1)
        ))

Capture

(setq org-capture-templates
      '(("i" "Inbox"  entry   (file "inbox.org")
         "* TODO %?Task :INBOX:\n")

        ("m" "Meeting" entry  (file+headline "agenda.org" "Future")
         "* %?Meeting   <%<%Y-%m-%d %a 12:00-13:00>>"
         :empty-lines-after 1)))
      

Assign key

(bind-key "C-c c" #'org-capture)

(defun my/org-capture-meeting ()
  (interactive)
  (org-capture nil "m"))

(bind-key "C-c m" #'my/org-capture-meeting)

(defun my/org-capture-inbox ()
  (interactive)
  (org-capture nil "i"))

(bind-key "C-c i" #'my/org-capture-inbox)

Here we orverwrite the org-capture-place-template to have the capture window below the current one. There must be a better way to do that but I did not find it yet. Since we are in org-agenda deferred mode, we need to cancel epiloque/prologue.

(with-eval-after-load 'org-capture

  (defun org-capture-place-template (&optional inhibit-wconf-store)
    "Insert the template at the target location, and display the buffer.
when `inhibit-wconf-store', don't store the window configuration, as it
may have been stored before."
    (unless inhibit-wconf-store
      (org-capture-put :return-to-wconf (current-window-configuration)))
    ;; (delete-other-windows)
    ;; (org-switch-to-buffer-other-window
    ;;  (org-capture-get-indirect-buffer (org-capture-get :buffer) "CAPTURE"))
    (select-window (split-window-below -6))
    (switch-to-buffer
     (org-capture-get-indirect-buffer (org-capture-get :buffer) "CAPTURE"))
    (widen)
    (org-show-all)
    (goto-char (org-capture-get :pos))
    (setq-local outline-level 'org-outline-level)
    (pcase (org-capture-get :type)
      ((or `nil `entry) (org-capture-place-entry))
      (`table-line (org-capture-place-table-line))
      (`plain (org-capture-place-plain-text))
      (`item (org-capture-place-item))
      (`checkitem (org-capture-place-item)))
    (setq-local org-capture-current-plist org-capture-plist)
    (org-capture-mode 1)))

Capture 2

(defun my/org-capture-frame (orig-fun &optional goto keys)
  (if (not (org-capture-get :description))
      (funcall orig-fun goto keys))
  (progn
    (select-frame (my/mini-frame 8))
    (let ((split-width-threshold nil)
          (split-height-threshold 0))
      (funcall orig-fun goto keys))
    (delete-other-windows)
    (setq word-wrap nil)
    (setq truncate-lines nil)
    (nano-modeline--update-selected-window)
    (let* ((left (concat (propertize " "
                                     'face '(nano-subtle)
                                     'display '(raise +0.20))
                         (propertize " Capture"
                                     'face '(nano-strong nano-subtle))
                         (propertize " "
                                     'face 'nano-subtle
                                     'display '(raise -0.30))
                         (propertize (org-capture-get :description)
                                     'face 'nano-default)))
                 (right (propertize "C-c C-c: capture, C-c C-k: abort "
                                    'face '(:inherit (nano-faded nano-subtle)
                                            :weight light)))
         (spacer (propertize (make-string (- (window-width)
                                             (length left)
                                             (length right)
                                             0) ?\ )
                             'face 'nano-subtle))
         (header (concat left spacer right " ")))
    (setq-local header-line-format header))
    
    (set-window-dedicated-p nil t)
    (set-frame-parameter (selected-frame) 'height 5)
    (face-remap-add-relative 'mode-line :background (face-background 'highlight))
    (save-excursion
      (goto-char (point-min))
      (let ((overlay (make-overlay
                      (line-beginning-position) (line-end-position))))
        (overlay-put overlay 'display '(raise -0.5))))))
(defun my/org-capture (goto key)
  (interactive)
  (unwind-protect
      (progn
        (advice-add 'org-capture :around #'my/org-capture-frame)
        (org-capture goto key))
    (advice-remove 'org-capture #'my/org-capture-frame)))

(require 'org-capture)
  
;; (bind-key "C-c m" #'(lambda ()
;;                       (interactive)
;;                       (my/org-capture nil "m")))

;; (bind-key "C-c i" #'(lambda ()
;;                       (interactive)
;;                       (my/org-capture nil "i")))

;; (bind-key "C-g" #'org-capture-kill 'org-capture-mode-map)

Navigation

The default org-agenda-goto, which is used when tab key is pressed or when follow mode (F) is active open a window at a non specific place. Here, we make sure the window is opened where we want. In this case, this is below the agenda window.

(defun my/org-agenda-goto (buffer args)
  "Open a headline in a window below the current window"
  
  (setq-local mode-line-format nil)
  (select-window (or (window-in-direction 'below (selected-window))
                     (split-window nil -6 'below)))
  (switch-to-buffer buffer)
  (setq-local header-line-format
              '((:eval
                 (let ((nano-modeline-prefix 'none)
                       (nano-modeline-prefix-padding 0)
                       (outline-path (org-with-point-at (org-get-at-bol'org-marker)
                                       (org-display-outline-path nil nil " » " t))))
                   (nano-modeline-render
                    ""
                    (file-name-nondirectory
                     (buffer-file-name (buffer-base-buffer)))
                    (format "/ %s" (substring-no-properties outline-path))
                    "")))))
  (selected-window))

Install the new function as an advice around org-agenda-goto and apply a narrow to subtree after

(define-advice org-agenda-goto (:around (orig-fn &rest args) "my/org-agenda-goto")
  (let ((display-buffer-overriding-action '(my/org-agenda-goto)))
    (apply orig-fn args)
    (org-narrow-to-subtree)))

Finally, we disable org-agenda-show-outline-path since this is now redundant with the header line.

(setq org-agenda-show-outline-path nil)

Nano agenda

(bind-key "C-c n" #'nano-agenda)

Benchmark

(my/report-time "Agenda")

Versioning

(setq my/section-start-time (current-time))

Magit

Prevent magit from writing in the header line.

(advice-add 'magit-set-header-line-format :override #'ignore)

Add fringe on the left side of magit windows such that we can highlight region using the fringe.

(add-hook 'magit-mode-setup-hook
          #'(lambda ()
              (interactive)
              (set-window-fringes nil (* 2 (window-font-width)) 0)))

Benchmark

(my/report-time "Versioning")

News

(setq my/section-start-time (current-time))

Read

Alternative print entry function using svg-tag-mode.

(require 'elfeed)
(require 'svg-tag-mode)

(defun my/elfeed-search-print-entry (entry)
  "Alternative printing of elfeed entries using SVG tags."
  
  (let* ((date (elfeed-search-format-date (elfeed-entry-date entry)))
         (title (or (elfeed-meta entry :title)
                    (elfeed-entry-title entry) ""))
         (unread (member 'unread (elfeed-entry-tags entry)))
         (feed (elfeed-entry-feed entry))
         (feed-title (when feed
                       (or (elfeed-meta feed :title)
                           (elfeed-feed-title feed))))
         (title-face (if unread 'nano-default 'nano-faded))
         (date-face (if unread 'nano-salient 'nano-faded))
         (feed-title-face (if unread 'nano-strong '(nano-strong nano-faded)))
         (tag-face (if unread 'nano-popout 'nano-faded))
         (tags (mapcar #'symbol-name (elfeed-entry-tags entry)))
         (tags (delete "unread" tags))
         (tags-svg (mapconcat
                    (lambda (s)
                      (propertize (concat (upcase s) " ")
                                  'display (svg-tag-make (upcase s)
                                                         :margin 0
                                                         :padding 1
                                                         :inverse 1
                                                         :face tag-face)))
                    tags " "))
         (left (concat
                (when feed-title
                  (propertize feed-title 'face feed-title-face))
                " " tags-svg " "
                (propertize title 'face title-face 'kbd-help title)))
         (right (propertize date 'face date-face)))
    (insert (my/string-join -1 left right))))

Elfeed setup.

(setq elfeed-search-title-max-width 80    ; Maximum titles width
      elfeed-search-title-min-width 40    ; Minimum titles width
      elfeed-search-trailing-width 24     ; Space reserved for feed & tag
      elfeed-search-filter                ; Default filter
          "@1-weeks-ago +unread"          
      elfeed-search-print-entry-function  ; Alternative print function
           #'my/elfeed-search-print-entry)


; Bind "U" to update feeds on main screen
(bind-key "U" #'elfeed-update elfeed-search-mode-map)

Hook on elfeed main screen (hl-line mode).

(defun my/elfeed-search-mode-hook ()
  (hl-line-mode t)
  (face-remap-add-relative 'hl-line :inherit 'nano-subtle)
  (set-window-fringes nil 0 1) ; One pixel right fringe to avoid ellipsis
  (setq cursor-type nil))

(add-hook 'elfeed-search-mode-hook  #'my/elfeed-search-mode-hook)

Hook on elfeed post screen (visual mode).

(defun my/elfeed-show-mode-hook ()
  (visual-line-mode)
;;  (setq truncate-lines t)

  (let ((inhibit-read-only t)
        (inhibit-modification-hooks t))
    (setq-local truncate-lines nil)
    (setq-local shr-width 79)
    (set-buffer-modified-p nil)))

(add-hook 'elfeed-show-mode-hook #'my/elfeed-show-mode-hook)

Bookmarks

Setup bookmarlks (elfeed.org) using elfeed-org. It is important to load it after elfeed such as to not load org immediately.

(with-eval-after-load 'elfeed
  (require 'elfeed-org)
  (setq rmh-elfeed-org-files (list (expand-file-name "elfeed.org" user-emacs-directory)))
  (elfeed-org))
  

Benchmark

(my/report-time "News")

Notes (denote)

(setq my/section-start-time (current-time))

Configuration

Samplet configuration from https://protesilaos.com/emacs/denote

(require 'denote)

;; Remember to check the doc strings of those variables.
(setq denote-directory (expand-file-name "~/Documents/Denote/"))
(setq denote-known-keywords
      '("emacs" "research" "visualization"))
(setq denote-infer-keywords t)
(setq denote-sort-keywords t)
(setq denote-file-type nil) ; Org is the default, set others here

;; We allow multi-word keywords by default.  The author's personal
;; preference is for single-word keywords for a more rigid workflow.
(setq denote-allow-multi-word-keywords t)

(setq denote-front-matter-date-format nil) ; change this to `org-timestamp' or custom string

;; You will not need to `require' all those individually once the
;; package is available.
(require 'denote-retrieve)
(require 'denote-link)
(require 'denote-dired)
(setq denote-dired-rename-expert nil)

;; We use different ways to specify a path for demo purposes.
(setq denote-dired-directories
      (list denote-directory
            (thread-last denote-directory (expand-file-name "attachments"))
            (expand-file-name "~/Documents/vlog")))

;; Generic:
;; (add-hook 'dired-mode-hook #'denote-dired-mode)
;;
;; OR better:
(add-hook 'dired-mode-hook #'denote-dired-mode-in-directories)

;; Here is a custom, user-level command from one of the examples we
;; showed in this manual.  We define it here and add it to a key binding
;; below.
(defun my/denote-journal ()
  "Create an entry tagged 'journal', while prompting for a title."
  (interactive)
  (denote
   (denote--title-prompt)
   "journal"))

;; Denote does not define any key bindings.  This is for the user to
;; decide.  For example:
(let ((map global-map))
  (define-key map (kbd "C-c n j") #'my/denote-journal) ; our custom command
  (define-key map (kbd "C-c n n") #'denote)
  (define-key map (kbd "C-c n N") #'denote-type)
  (define-key map (kbd "C-c n d") #'denote-date)
  ;; If you intend to use Denote with a variety of file types, it is
  ;; easier to bind the link-related commands to the `global-map', as
  ;; shown here.  Otherwise follow the same pattern for `org-mode-map',
  ;; `markdown-mode-map', and/or `text-mode-map'.
  (define-key map (kbd "C-c n i") #'denote-link) ; "insert" mnemonic
  (define-key map (kbd "C-c n I") #'denote-link-add-links)
  (define-key map (kbd "C-c n l") #'denote-link-find-file) ; "list" links
  (define-key map (kbd "C-c n b") #'denote-link-backlinks)
  ;; Note that `denote-dired-rename-file' can work from any context, not
  ;; just Dired bufffers.  That is why we bind it here to the
  ;; `global-map'.
  (define-key map (kbd "C-c n r") #'denote-dired-rename-file))

(with-eval-after-load 'org-capture
  (require 'denote-org-capture)
  (setq denote-org-capture-specifiers "%l\n%i\n%?")
  (add-to-list 'org-capture-templates
               '("n" "New note (with denote.el)" plain
                 (file denote-last-path)
                 #'denote-org-capture
                 :no-save t
                 :immediate-finish nil
                 :kill-buffer t
                 :jump-to-captured t)))

Benchmark

(my/report-time "Notes")

Notes (deft)

(setq my/section-start-time (current-time))

Read

Deft setup

(setq deft-default-extension "org"
      deft-extensions '("org")
      deft-recursive nil
      deft-use-filename-as-title nil
      deft-use-filter-string-for-filename t
      deft-file-naming-rules '((noslash . "-")
                               (nospace . "-")
                               (case-fn . downcase))
      deft-separator " "
      deft-time-format " %d %b %Y")

Rewrite the deft-print-header function to get rid of “Deft\n”

(defun deft-print-header ()
  (force-mode-line-update))

Bug fix (see jrblevin/deft#73)

(defun org-open-file-with-emacs (path)
  (org-open-file path t))

A small bugfix for header that are too long by one character.

(defun deft-setup ()

  (face-remap-add-relative 'hl-line :inherit 'nano-salient-i)
  (set-window-fringes nil 0 1)
  (set-default 'truncate-lines t))

(add-hook 'deft-mode-hook #'deft-setup)

Keywords

(defun my/deft-parse-summary (orig-fun contents title)
  "Filter deft summary in order to extract the first dot
terminated sentence and add tags if any."

  (let ((summary (apply orig-fun (list contents title)))
        (tags nil))
    (when (and (stringp contents)
               (string-match "#\\+TAGS:\\(.*\\)$" contents))
      (setq tags (split-string (string-trim (match-string 1 contents))
                                 "[ ,]")))
    (if (and (stringp summary)
             (string-match "\\(.*?\\)\\. " summary))
        (concat
         (when tags
           (concat (propertize (car tags)
                               'display (svg-tag-make (car tags)
                                                      :face 'nano-popout
                                                      :inverse t))
                   " "))
         (match-string 1 summary))
      summary)))

(advice-add 'deft-parse-summary :around #'my/deft-parse-summary)
(defun deft-note-toggle-keywords ()
  "Toggle visibility of all keywords."
  
  (interactive)
  (save-excursion
    (goto-char (point-min))
    (re-search-forward "^\\(#\\+.*\\)$" nil t)
    (if (get-text-property (match-beginning 1) 'display)
        (deft-note-show-keywords)
      (deft-note-hide-keywords))))
        
(defun deft-note-hide-keywords ()
  "Hide all keywords."
  
  (interactive)
  (save-excursion
    (goto-char (point-min))
    (while (re-search-forward "^\\(#\\+.*\\)$" nil t)
      ;; (message (format "Hiding keyword %s" (match-string 1)))
      (put-text-property
        (match-beginning 1) (+ (match-end 1) 1) 'display ""))))

(defun deft-note-show-keywords ()
  "Show all keywords."
  
  (interactive)
  (save-excursion
    (goto-char (point-min))
    (while (re-search-forward "^\\(#\\+.*\\)$" nil t)
      ;; (message (format "Showing keyword %s" (match-string 1)))
      (remove-text-properties
        (match-beginning 1) (+ (match-end 1) 1) '(display)))))

(defun deft-note-get-keyword (keyword)
  "Get the value of a KEYWORD"
  
  (interactive)
  (let ((case-fold-search t)
        (re (format "^#\\+%s:[ \t]+\\([^\t\n]+\\)" keyword)))
    (if (save-excursion (or (re-search-forward re nil t)
                            (re-search-backward re nil t)))
        (substring-no-properties (match-string 1)))))

(defun deft-note-set-keyword (keyword value)
  "Set the VALUE of KEYWORD, creates it if absent."
  (interactive)
  (save-excursion
    (goto-char (point-min))
    (if (deft-note-get-keyword keyword)
        (replace-match value t nil nil 1)
      (insert (format "#+%s: %s\n" keyword value)))))

Write

Setup note modes and ask for a title if the file does not exist

(defun my/deft-open-file ()
  "Setup note modes and ask for a title if the file does not exist."
  
  (when (= (buffer-size (current-buffer)) 0)
    (setq title (read-from-minibuffer "Note title: "))
    (deft-note-set-keyword "DATE" (format-time-string "[%Y-%m-%d %a]"))
    (deft-note-set-keyword "TITLE" (if (> (length title) 0)
                                       title
                                     "New note"))
    (org-mode)
    (org-indent-mode)
    (visual-line-mode)))

(add-hook 'deft-open-file-hook 'my/deft-open-file)

Benchmark

(my/report-time "Notes")

System

(setq my/section-start-time (current-time))

Term & shell

Set default shell (zsh)

(setq-default shell-file-name          "/bin/zsh"
              explicit-shell-file-name "/bin/zsh")

Make sure our environment variables are set properly

;; (require 'exec-path-from-shell)
;; (exec-path-from-shell-copy-envs '("LANG" "LC_ALL" "LC_CTYPES"))

Kill term buffer when exiting.

(defun my/term-handle-exit (&optional proc msg)
  "Kill term buffer (advice)."
  
  (message "%s | %s" proc msg)
  (kill-buffer (current-buffer)))

(advice-add 'term-handle-exit :after 'my/term-handle-exit)

OSX

Open an iterm (OSX) and go to the curent directory

(defun my/iterm-here ()
  (interactive)

  (shell-command "open -a iTerm $PWD" nil nil))

Benchmark

(my/report-time "System")

Profiling

Final report

(let ((init-time (float-time (time-subtract (current-time) my/init-start-time)))
      (total-time (string-to-number (emacs-init-time "%f"))))

  (message "---------------------------------------------------------------")
  (message "Initialization time:                 %.2fs (+ %.2f system time)"
           init-time (- total-time init-time)))
  (message "---------------------------------------------------------------")