/ mrb.org
mrb.org
   1  #+SETUPFILE: "org-config.org"
   2  #+TITLE: My org-babel based emacs configuration
   3  #+LANGUAGE: en
   4  #+OPTIONS:  H:5 toc:nil creator:nil email:nil author:t timestamp:t tags:nil tex:verbatim
   5  #+PROPERTY: header-args :results silent :noweb no-export
   6  
   7  Last update: call_last_modified()
   8  
   9  This is my emacs configuration file that is loaded with =org-babel-load-file= in the Emacs init file. The intent is to have as much of my Emacs configuration in here as possible. The system works as a literal programming system where with a tangle the elisp code that actually makes up my configuration is extracted automatically and loaded.
  10  
  11  This document is irregularly published and lives in different locations:
  12  
  13  - [[https://codeberg.org/mrb/emacs-config][config sources are available on codeberg]]
  14  - [[https://qua.name/mrb/an-org-babel-based-emacs-configuration][published on my writefreely instance qua.name]]
  15  - [[rad:z4Hkna53NPf7wFn6QJ8BcajVvG9do][published as a radicle repository]]
  16  
  17  #+TOC: headlines 4
  18  * Files for configuration
  19  Next to this main configuration file (=mrb.org=), two other files are involved in the configuration:
  20  
  21  1. =early-init.el= emacs' early initialization file;
  22     #+INCLUDE: early-init.el src emacs-lisp
  23  
  24  2. =init.el= the main init file, which is used to generate =mrb.el= from =mrb.org=.
  25     #+INCLUDE: init.el src emacs-lisp
  26  
  27  Ideally these files should be produced by org-babel as well, just like the main configuration, but that generates a dependency because these two files need to be available from the beginning.
  28  
  29  Small snippet to have last modified timestamp inserted when exporting this file (see line above which has the call to =last_modified=)
  30  
  31  #+NAME: last_modified
  32  #+BEGIN_SRC sh :var emacs_dir=(expand-file-name user-emacs-directory)
  33    git log -1 --pretty="format:%ci" "$emacs_dir/mrb.org"
  34  #+END_SRC
  35  
  36  * Preparing for lift-off
  37  The first thing I want to take care of is to make customization possible and stored in a place to my liking. I want to load this first so anything in the configuration I define explicitly overrides it. The custom file is really only for =safe-local-variables= support; variables that get set automtically by reading a =.dir-locals.el= file for example. All actual persistent configuration goes in this file only.
  38  
  39  #+begin_src emacs-lisp
  40    (defun mrb/org-file (relative-path)
  41      "Expand RELATIVE-PATH relative to org-directory."
  42      (expand-file-name relative-path org-directory))
  43  
  44    (defun mrb/emacs-file (relative-path)
  45      "Expand RELATIVE-PATH relative to user-emacs-directory."
  46      (expand-file-name relative-path user-emacs-directory))
  47  
  48    (defun mrb/home-file (relative-path)
  49      "Expand RELATIVE-PATH relative to the user's home directory."
  50      (expand-file-name relative-path (getenv "HOME")))
  51  
  52    (setq custom-file (mrb/emacs-file "custom.el"))
  53    (load custom-file)
  54  
  55    (defun mrb/clean-custom-file (custom-file-path)
  56      "Clean CUSTOM-FILE-PATH by removing auto-generated variables, keeping safe-local-variable-values.
  57  Parses the file as Lisp, extracts the safe-local-variable-values entry,
  58  and writes it back (or a fallback if not found). Skips cleanup on parse error."
  59      (when (file-exists-p custom-file-path)
  60        (let ((safe-entry nil)
  61              (parse-ok t))
  62          ;; Parse custom.el as Lisp and extract safe-local-variable-values
  63          (with-temp-buffer
  64            (insert-file-contents custom-file-path)
  65            (goto-char (point-min))
  66            (let (forms)
  67              (condition-case err
  68                  (while t (push (read (current-buffer)) forms))
  69                (end-of-file nil)
  70                (error
  71                 (lwarn 'mrb :warning "custom.el parse error: %S — skipping cleanup" err)
  72                 (setq parse-ok nil)))
  73              (when parse-ok
  74                (let* ((cvars-form (cl-find 'custom-set-variables forms :key #'car))
  75                       (entries (cdr cvars-form)))
  76                  (setq safe-entry
  77                        (cl-find 'safe-local-variable-values entries
  78                                 :key (lambda (e) (caadr e))))))))
  79          ;; Write back only safe-local-variable-values
  80          (when parse-ok
  81            (with-temp-file custom-file-path
  82              (insert ";;;; Custom file for safe-local-variable approvals only\n")
  83              (insert ";; Auto-generated variables removed by mrb/clean-custom-file.\n")
  84              (insert ";; All face and configuration customizations are in mrb.org.\n\n")
  85              (insert "(custom-set-variables\n ")
  86              (insert (if safe-entry
  87                          (pp-to-string safe-entry)
  88                        "'(safe-local-variable-values\n   '())"))
  89              (insert ")\n"))))))
  90  
  91    (add-hook 'after-init-hook (lambda () (mrb/clean-custom-file custom-file)))
  92  #+end_src
  93  
  94  * X Resources
  95  X resource settings for Emacs are generated and loaded here. This keeps display configuration
  96  in sync with the elisp configuration (fonts, colors, widget styling).
  97  
  98  #+begin_src fundamental :tangle ~/.Xdefaults.d/emacs :exports code
  99  ! Emacs X Resources
 100  ! Generated from mrb.org - do not edit manually
 101  ! Reload with: xrdb -merge ~/.Xdefaults.d/emacs
 102  
 103  ! Font configuration (synced with base16-adjustments default face)
 104  ! Height 112 at 120 DPI ≈ pixelsize=19
 105  Emacs.font: Hack-11:pixelsize=19
 106  
 107  ! Menu bar and tool bar
 108  Emacs.menuBar: off
 109  Emacs.toolBar: off
 110  
 111  ! Menu and dialog styling (Nord theme colors)
 112  Emacs.menu*.background: #5E81AC
 113  Emacs.menu*.foreground: #D8DEE9
 114  Emacs.dialog*.background: #5E81AC
 115  Emacs.dialog*.foreground: #D8DEE9
 116  Emacs.pane.menubar.background: #5E81AC
 117  Emacs.pane.menubar.foreground: #D8DEE9
 118  
 119  ! Widget appearance
 120  Emacs*shadowThickness: 1
 121  #+end_src
 122  
 123  After tangling, load the generated file:
 124  
 125  #+begin_src emacs-lisp
 126    (when (display-graphic-p)
 127      (call-process "xrdb" nil nil nil "-merge"
 128                    (mrb/home-file ".Xdefaults.d/emacs")))
 129  #+end_src
 130  
 131  The config file =mrb.org= gets opened *a lot* because I tend to fiddle with this config at least once a day. So, it warrants it's own keybinding, but it will have to wait until [[Key bindings]] before we can use the =bind-key= package.
 132  
 133  #+begin_src emacs-lisp
 134    (defun mrb/open-config ()
 135      (interactive)
 136      (find-file config-file))
 137  #+end_src
 138  
 139  Also, it helps to have a config-reload option sometimes.
 140  
 141  #+begin_src emacs-lisp
 142    (defun mrb/reload-config ()
 143      "Reload init file, which will effectively reload everything"
 144      (interactive)
 145      (load-file (mrb/emacs-file "init.el")))
 146  
 147    (defun mrb/check-emacs-version-support (required-version message)
 148      "Warn if current Emacs version is below REQUIRED-VERSION (e.g., \"29.1\").
 149  MESSAGE describes the unsupported feature. Supports formats: x, x.y, x.y.z.
 150  REQUIRED-VERSION can be a string or integer; integers are converted to strings."
 151      (let ((version-str (if (stringp required-version)
 152                             required-version
 153                           (number-to-string required-version))))
 154        (when (version< emacs-version version-str)
 155          (warn "Emacs %s: %s (requires Emacs %s+)"
 156                emacs-version message version-str))))
 157  #+end_src
 158  
 159  Most of the time the editing process starts at the command line. Because I use emacs in server mode, this needs a little script.  The script does two things:
 160  1. check if we already have the emacs server, if not, start it;
 161  2. treat input from stdin a little bit special.
 162  
 163  While we are here, define a snippet which should go in shell scripts. We will repeat this per language at some point.
 164  
 165  #+name: tangle-header
 166  #+begin_src sh :tangle no
 167    [ Org-mode generated this file from a code block, changes will be overwritten ]
 168  #+end_src
 169  
 170  And edit the file obviously.
 171  
 172  #+begin_src sh :exports code :tangle ~/bin/edit :shebang #!/bin/bash
 173    # <<tangle-header>>
 174    # Wrapper for emacs-client usage to handle specific cases:
 175    # - no filename given
 176    # - stdin editing
 177    # - force tty
 178    # - adjustment to systemd invocation
 179  
 180    # Set to false when not debugging
 181    debug() { false $@; }
 182  
 183    ME=$(basename $0)
 184    SNAME=server
 185    SFILE=$XDG_RUNTIME_DIR/emacs/$SNAME
 186  
 187    # Default argument needs our socket in any case
 188    ARGS="--socket-name=$SNAME --suppress-output"
 189  
 190    # Ensure availability of commands
 191    ensure() {
 192        if ! command -v $1 >/dev/null 2>&1; then
 193            echo "ERROR: command '$1' is needed"
 194            exit 1
 195        fi
 196    }
 197  
 198    ensure notify-send
 199    ensure cat
 200    ensure rm
 201    ensure systemctl
 202    ensure emacsclient
 203    ensure emacs
 204  
 205    # Get full paths to emacs commands via command lookup
 206    EC=$(command -v emacsclient)
 207    EM=$(command -v emacs)
 208  
 209    # Do we already have an emacs?
 210    # I could let emacsclient do this automatically, but it
 211    # is hard-coded to exec emacs --daemon (without socket name)
 212  
 213    # This goes along an emacs.service file obviously
 214  
 215    # Diagnostic: if systemctl says emacs is active, socket MUST exist
 216    if systemctl -q --user is-active emacs && [ ! -S $SFILE ]; then
 217        echo "ERROR: Emacs service is active but socket not found at $SFILE"
 218        echo "This indicates a problem with the Emacs service configuration"
 219        exit 1
 220    fi
 221  
 222    # If socket missing, start the service
 223    if [ ! -S $SFILE ]; then
 224        debug "Socket file not found, starting Emacs service..."
 225        notify-send "Starting Emacs server, this might take a while....";
 226        #$EM --bg-daemon=$SFILE
 227        systemctl --user start emacs
 228    fi
 229  
 230    # Final assertion: socket must exist now
 231    [ -S $SFILE ] || { echo "ERROR: Socket file not found at $SFILE"; exit 1; }
 232  
 233    # Handle special 'stdin' case
 234    if [ $# -ge 1 ] && [ "$1" == "-" ]; then
 235        TMPSTDIN="$(mktemp /tmp/emacsstdinXXX)";
 236        cat >"$TMPSTDIN";
 237        # Recurse or call with flags below
 238        $ME $TMPSTDIN;
 239        rm $TMPSTDIN;
 240        exit 0;
 241    fi
 242  
 243    ## If we explicitly get asked to start terminal, do so
 244    if [[ $@ =~ (^|[[:space:]])(-nw|-tty|-t|--no-window-system)($|[[:space:]]) ]]; then
 245        # We were forcing terminal, skip the whole wait, frame, no-frame stuff
 246        debug "Forced tty startup"
 247        # FIXME this may create twice the -tty argument to start terminal version, but it's harmless
 248        ARGS=$ARGS' -tty'
 249    else
 250        # We're basically expecting X windows, but if not, we good
 251        # force tty: call this like 'DISPLAY="" edit ....' see my aliases
 252        if [[ $DISPLAY ]]; then
 253            debug "X-display value seen, using GUI"
 254            case $ME in
 255                "edit")
 256                    ARGS=$ARGS' --no-wait'
 257                    ;;
 258                "edit-wait")
 259                    ARGS=$ARGS' '
 260                    ;;
 261                "edit-frame")
 262                    ARGS=$ARGS' --no-wait --create-frame'
 263                    ;;
 264                "edit-wait-frame")
 265                    ARGS=$ARGS' --create-frame'
 266                    ;;
 267                ,*)
 268                    echo "Wrong calling name..."
 269                    ;;
 270            esac
 271        else
 272            # tty always waits, as it is in the terminal, create-frame has no meaning
 273            debug "Starting tty emacs"
 274            ARGS=$ARGS' -tty'
 275        fi
 276    fi
 277  
 278    # Go with the original arguments
 279    debug $EC ${ARGS} "$@"
 280    exec $EC ${ARGS} "$@"
 281  #+end_src
 282  
 283  The above script is the result of trying to get the server based setup of Emacs a little better. The thoughts leading to the above script are included below.
 284  
 285  Emacs can run as a /server/ meaning that its process is always active and a specific /client/ program called =emacsclient= can be used to connect to it to do the actual user interaction and let me edit and use the lisp interpreter.
 286  
 287  It's far from trivial, for me at least, on how to properly configure this, assuming you have some special wishes. I've certainly spent my time on trying to get it right.
 288  
 289  *** Things that I'm sure of I want
 290  Here's what I know for sure on what I want:
 291  
 292  1. I want to have at least one server process for 'default' editing so emacs does not have to start a process for every file; the goal here is to have fast opening of files to edit. This is critical. I'm assuming that for this a server-like-setup is needed.
 293  2. Given that I tweak emacs a lot, it must be very easy to either disable the server temporarily or have it restart easily so I can test new changes without shutting me out of emacs completely.
 294  3. The previous item also implies that I want to run multiple independent emacs-en next to each other, regardless if they are run as a daemon or not.
 295  4. Emacs must work on a non X-windows screen, but extra effort to get that is OK.
 296  5. Be able to use '-' as placeholder for =stdin= to be able to pipe information directly into editor
 297  6. allow specific handlers, like =mailto:=
 298  
 299  *** Things I see potential for, but not sure of
 300  Having a server/servers opens a couple of options which I may like, but not sure on how to do or even if I understand them properly
 301  
 302  - given that the socket can be a TCP socket, does that mean I can use emacsclient -some-arg <the IP of the socket> on one machine to use emacs on another machine? How well would that work? This would be awesome to have a small computer which doesn't need an emacs config to use my emacs on another machine, but I suspect I misunderstand how this works
 303  
 304  - having multiple emacs servers could help with specific scenario's for mu4e for example. I'd love to have the mu-server in its own process and just use that from other emacs servers Similarly for long running language servers and or other processes which shouldn't be affected by me clawing words into my keyboard.
 305  
 306  * Package Management
 307  Package handling, do this early so emacs knows where to find things. Package handling basically means two things:
 308  
 309  1. getting and installing packages, by default handled by package.el;
 310  2. configuring packages, just elisp, but I use the =use-package= macro typically.
 311  
 312  As I want to have version controlled package installation that is easily reproducible, =straight.el= seem a logical choice, so let's start by bootstrapping that.
 313  
 314  #+begin_src emacs-lisp
 315    (defvar bootstrap-version)
 316    (setq straight-repository-branch "develop") ; Need this for new org-contrib location
 317    (let ((bootstrap-file
 318           (mrb/emacs-file "straight/repos/straight.el/bootstrap.el"))
 319          (bootstrap-version 5))
 320      (unless (file-exists-p bootstrap-file)
 321        (with-current-buffer
 322            (url-retrieve-synchronously
 323             "https://raw.githubusercontent.com/raxod502/straight.el/develop/install.el"
 324             'silent 'inhibit-cookies)
 325          (goto-char (point-max))
 326          (eval-print-last-sexp)))
 327      (load bootstrap-file nil 'nomessage))
 328  #+end_src
 329  
 330  Now that we have =straight= we will use it from the =use-package= macro.
 331  
 332  #+begin_src emacs-lisp
 333    (setq use-package-always-ensure nil   ; Make sure this is nil, so we do not use package.el
 334          use-package-verbose nil         ; disabled to reduce startup/reload overhead
 335          )
 336    ;; From this point on we should be able to use `use-package
 337    (use-package straight
 338      :custom
 339      ;; straight-host-usernames is set in personal.el
 340      (straight-use-package-by-default t)
 341      ;; Make sure packages do not pull in internal org, we pregister org from straight.el
 342      (straight-register-package 'org)
 343      (straight-register-package 'org-contrib))
 344  
 345    ;; FROM THIS POINT use-package should work as intended, i.e. using straight.
 346  
 347    ;; Need to install dependencies of use-package manually, why??
 348    (use-package diminish)
 349    (use-package buttercup)
 350  #+end_src
 351  
 352  I want to delegate garbage collection to someone who knows more than I do and made a package for it. The idea is still similar to what I had for years, i.e. large threshold on an idle-timer to do garbage collection. The package just adds some convenience, so I dont have to think about it other than making the threshold large on startup.
 353  
 354  #+begin_src emacs-lisp
 355    (use-package gcmh
 356      :custom
 357      (gcmh-verbose 1)
 358      (gcmh-high-cons-threshold (* 16 1024 1024 1024))
 359      :config
 360      (diminish 'gcmh-mode)
 361      (gcmh-mode 1))
 362  #+end_src
 363  
 364  
 365  As emacs lives inside the systemd environment it is not automatically the case that my shell environment is inherited. In many cases, this is not a problem, but for those cases that expect the same environment, I'm making explicit that the enviroment needs to be from the shell. We should do this a soon as =use-package= is useable.
 366  
 367  #+begin_src emacs-lisp
 368    (use-package exec-path-from-shell
 369      :demand t
 370      :custom
 371      (exec-path-from-shell-arguments '("-l"))  ; Login shell only (no interactive flag)
 372      :config
 373      (exec-path-from-shell-initialize))
 374  #+end_src
 375  
 376  This makes sure that things like TeX and git and other programs run in the same context as they would in my shell.
 377  
 378  * Personal information
 379  I'd like to arrange 3 things related to personal information:
 380  1. register the proper identification to use;
 381  2. Make sure the proper authentication information is stored;
 382  3. Store this information privately.
 383  
 384  So, identification, authorization and encryption.
 385  
 386  Seems like a good idea to start configuration with some details about me. The idea being that anything personal goes here and nowhere else. For one, this means my name only appears in this section in this document. Most of the variables I just made up, but some are used in other places too.
 387  
 388  Personal variables are loaded from personal.el (see personal.example.el for template).
 389  
 390  ** Authorization
 391  Several things I use within Emacs need authorization, such as tramp, jabber, erc etc. The authorization can come from several sources; ideally as few as possible. Many packages in Emacs have support for a =.netrc= like mechanism, others want to use the key ring in GNOME. The variables =auth-sources= defines the sources available.
 392  
 393  I want to use systems which are desktop independent, so things like the gnome key ring are out because they depend on the gnome environment being present, which I can not guarantee, nor want to related to authentication. The situation which I want to prevent is that if gnome is broken, I can't authenticate to services I need.
 394  
 395  I have a gpg-agent daemon configured which manages gpg and ssh keys, protected by a hardware key. Let's make the system as simple as we can for now and just store passwords in the gpg protected store only, i.e. the password-store program.
 396  
 397  #+begin_src emacs-lisp
 398    ;; Use only password-store
 399    (use-package password-store)
 400    (use-package auth-source-pass
 401      :straight (:type built-in)
 402      :init
 403      (auth-source-pass-enable)
 404      :after password-store
 405      :custom
 406      ;; Make sure it's the only mechanism
 407      (auth-sources '(password-store))
 408      (auth-source-gpg-encrypt-to (list user-mail-address)))
 409  
 410    ;; I like the pass interface, so install that too
 411    (use-package pass
 412      :after password-store)
 413  #+end_src
 414  
 415  ** Encrypting information
 416  I need a way to store some sensitive information without that being published, should I decide some day to push this config somewhere.
 417  
 418  When needed, the gpg key is used to encrypt information.
 419  
 420  #+begin_src emacs-lisp
 421    ;; Use my email-address for encryption
 422    (setq-default epa-file-encrypt-to user-mail-address)
 423    ;; Make sure we always use this
 424    (setq-default epa-file-select-keys nil)
 425  #+end_src
 426  
 427  For org-mode, there is a way to encrypt sections separately. See [[Encrypting information in org-mode]] for the details on the settings for this.
 428  
 429  Next to inline content in org that needs encryption, there is also content that needs encrypting which is more suitable to store in a separate file for several reasons.
 430  
 431  * Global Generic settings
 432  Section contains global settings which are either generic or I haven't figured out how to classify it under a better section.
 433  
 434  Let's begin with the setup of some fundamentals like if we want tabs or spaces, how to select stuff and what system facilities to use.
 435  
 436  UI and display settings:
 437  #+begin_src emacs-lisp
 438    (setq inhibit-startup-message t         ; suppress startup screen
 439          frame-resize-pixelwise t)          ; pixel-precise frame resizing
 440  #+end_src
 441  
 442  Native compilation settings:
 443  #+begin_src emacs-lisp
 444    (setq package-native-compile t                                    ; compile packages natively
 445          native-comp-async-report-warnings-errors 'silent            ; suppress native-comp warnings
 446          comp-deferred-compilation t)                                ; defer compilation
 447  #+end_src
 448  
 449  I'm of the /spaces-no-tabs religion/:
 450  #+begin_src emacs-lisp
 451    (setq-default indent-tabs-mode nil)     ; do not insert tabs when indenting by default
 452    (setq tab-width 4)                      ; 4 spaces by default
 453  #+end_src
 454  
 455  
 456  I want to prevent producing files which have trailing spaces, because they serve no purpose at all. Removing these spaces is less trivial than it seems and I am not pretending to know what the best strategy is. The [[https://github.com/jamescherti/stripspace.el][stripspace]] package seems to be exactly made for this.
 457  
 458  #+begin_src emacs-lisp
 459    (use-package stripspace
 460      :diminish stripspace-local-mode
 461      :hook ((prog-mode . stripspace-local-mode)
 462             (text-mode . stripspace-local-mode)
 463             (conf-mode . stripspace-local-mode))
 464      :custom
 465      ;; We do now want diffs that are to verbose on unclean files
 466      (stripspace-only-if-initially-clean t)
 467      ;; make sure we keep in the right position after stripping
 468      (stripspace-restore-column t))
 469  
 470    ;; FIXME these settings do not belong here
 471    (setq
 472     delete-by-moving-to-trash t          ; move files to the trash instead of rm
 473     select-enable-clipboard t            ; use the clipboard in addition to kill-ring
 474  
 475     warning-minimum-level 'error
 476     large-file-warning-threshold nil
 477     find-file-use-truenames nil
 478     find-file-compare-truenames t
 479  
 480     minibuffer-max-depth nil
 481     minibuffer-confirm-incomplete t
 482     complex-buffers-menu-p t
 483     next-line-add-newlines nil
 484     kill-whole-line t
 485  
 486     auto-window-vscroll nil
 487  
 488     compilation-scroll-output t
 489     custom-safe-themes t
 490     disabled-command-function #'ignore  ; don't prompt or write to init.el for disabled functions
 491     explicit-shell-file-name "/bin/bash"
 492     font-lock-maximum-size nil
 493     gud-gdb-command-name "gdb --annotate=1"
 494     reb-re-syntax 'string
 495     sgml-xml-mode t
 496     warning-suppress-types '((comp) (undo))
 497     x-select-enable-clipboard-manager nil)
 498  
 499    ;; Map backspace to DEL and delete to C-d
 500    (when (display-graphic-p)
 501      (normal-erase-is-backspace-mode t))
 502  
 503    ;; Let marks be set when shift arrowing, everybody does this
 504    (setq shift-select-mode t)
 505    (delete-selection-mode 1)
 506  
 507    ;; Only require to type 'y' or 'n' instead of 'yes' or 'no' when prompted
 508    (setq use-short-answers t)
 509  
 510    ;; Use auto revert mode globally
 511    ;; This is safe because emacs tracks if the file is saved in the editing buffer
 512    ;; and if so, it will not revert to the saved file.
 513    (use-package autorevert
 514      :straight (:type built-in)
 515      :diminish auto-revert-mode
 516  
 517      :custom
 518      (auto-revert-interval 10) ; increased from 5 to reduce filesystem checks during typing
 519      (global-auto-revert-non-file-buffers t)  ; things like dired buffers
 520  
 521      :config
 522      (global-auto-revert-mode t)
 523  
 524      (defun mrb/org-restore-context-after-revert ()
 525        "Reveal org-mode heading context around point after buffer revert."
 526        (when (derived-mode-p 'org-mode)
 527          (org-show-context 'default)))
 528  
 529      (add-hook 'after-revert-hook #'mrb/org-restore-context-after-revert))
 530  
 531    ;; Should this be here?
 532    ;; Try to have urls and mailto links clickable everywhere
 533    (setq goto-address-url-mouse-face 'default)
 534    (define-global-minor-mode global-goto-address-mode
 535      goto-address-mode
 536      (lambda ()
 537        (goto-address-mode 1)))
 538    (global-goto-address-mode t)
 539  
 540    ;; Shorten urls for mode specific syntax
 541    (defun mrb/shortenurl-at-point ()
 542      (interactive)
 543      (let ((url (thing-at-point 'url))
 544            (bounds (bounds-of-thing-at-point 'url)))
 545        (kill-region (car bounds) (cdr bounds))
 546  
 547                                            ; Leave url as is, unless mode has specific link syntax
 548        (insert (format (cond ((eq major-mode 'org-mode)         "[[%1s][%2s]]")
 549                              ((eq major-mode 'markdown-ts-mode) "[[%2s][%1s]]")
 550                              (t "%1s"))
 551                        url
 552                        (truncate-string-to-width url 40 nil nil "…")))))
 553    (bind-key "C-c s" 'mrb/shortenurl-at-point)
 554  #+end_src
 555  
 556  Make life easier if we have sudo, so we can just edit the files and be done with them if possible
 557  
 558  #+begin_src emacs-lisp
 559    (use-package sudo-save)
 560  #+end_src
 561  
 562  Using pdf-tools for PDF handling. This is a lot better than docview. I'm a bit annoyed that they are not one package, they are very similar.
 563  
 564  #+begin_src emacs-lisp
 565    (use-package pdf-tools
 566      :demand t                             ; prevent configuring while opening pdf files
 567      :after (fullframe)
 568      :magic ("%PDF" . pdf-view-mode)
 569      :hook ((pdf-view-mode . (lambda () (cua-mode 0))))
 570      :commands (pdf-info-getannots mrb/pdf-extract-info)
 571      :bind ( :map pdf-view-mode-map
 572              ("h"   . 'pdf-annot-add-highlight-markup-annotation)
 573              ("t"   . 'pdf-annot-add-text-annotation)
 574              ("D"   . 'pdf-annot-delete)
 575              ("C-s" . 'isearch-forward)
 576              ("s-c". 'pdf-view-kill-ring-save)
 577              ("m"   . 'mrb/mailfile)
 578              ("q"   . 'kill-current-buffer)
 579              ([remap pdf-misc-print-document] . 'mrb/pdf-misc-print-pages)
 580              )
 581  
 582      :custom
 583      (pdf-annot-activate-created-annotations t) ; automatically annotate highlights
 584      (pdf-view-resize-factor 1.1)              ; more fine-grained zooming
 585      (pdf-view-midnight-colors '("#DCDCCC" . "#383838")) ; Not sure what this is
 586      (pdf-misc-print-program-executable "/usr/bin/lp")
 587      (pdf-view-display-size 'fit-page)        ; scale to fit page by default
 588  
 589      :config
 590      (add-to-list 'pdf-view-incompatible-modes 'display-line-numbers-mode) ; disable line numbers in PDF view
 591      (setq pdf-info-epdfinfo-program (mrb/emacs-file "straight/repos/pdf-tools/server/epdfinfo"))
 592      ;;(pdf-tools-install) ; autobuild should not happen now, so enable the question if it does
 593      (require 'pdf-annot)
 594      (define-key pdf-annot-edit-contents-minor-mode-map (kbd "<return>")   #'pdf-annot-edit-contents-commit)
 595      (define-key pdf-annot-edit-contents-minor-mode-map (kbd "<S-return>") #'newline)
 596  
 597      ;; Some settings from http://pragmaticemacs.com/emacs/even-more-pdf-tools-tweaks/
 598      (fullframe pdf-view-mode kill-this-buffer)
 599  
 600      (add-to-list 'revert-without-query ".+\\.pdf") ; pdf is always a reflection of disk file
 601  
 602      (defun mrb/pdf-misc-print-pages(filename pages &optional interactive-p)
 603        "Wrapper for `pdf-misc-print-document` to add page selection support"
 604        (interactive (list (pdf-view-buffer-file-name)
 605                           (read-string "Page range (empty for all pages): "
 606                                        (number-to-string (pdf-view-current-page)))
 607                           t) pdf-view-mode)
 608        (let ((pdf-misc-print-program-args
 609               (if (not (string-blank-p pages))
 610                   (cons (concat "-P " pages) pdf-misc-print-program-args)
 611                 pdf-misc-print-program-args)))
 612          (pdf-misc-print-document filename))))
 613  #+end_src
 614  
 615  Integrate this with orgmode as well. =org-pdftools= provides a link type, so we can link /into/ pdf documents. =pdf-tools-org= provides utility functions to help export pdf annotations into org mode and vice versa
 616  
 617  #+begin_src emacs-lisp
 618    (use-package org-pdftools               ; this brings in org-noter as well
 619      :after (org pdf-tools)
 620      :hook (org-mode . org-pdftools-setup-link))
 621  
 622    (use-package pdf-tools-org
 623      :demand t  ; when should this be loaded? goes on pdftools load now
 624      :after (org pdf-tools)
 625      :straight (:host github :repo "machc/pdf-tools-org")
 626      )
 627  #+end_src
 628  
 629  I tend to compile Emacs with ImageMagick support, let's make sure we use it too.
 630  
 631  #+begin_src emacs-lisp
 632    (when (fboundp 'imagemagick-register-types)
 633      (imagemagick-register-types))
 634  #+end_src
 635  
 636  Quick install of package that exports =qrencode-region= function. I use this mainly to send links or package tracking codes to my mobile.
 637  
 638  #+begin_src emacs-lisp
 639    (use-package qrencode)
 640  #+end_src
 641  
 642  * Internationalization and multi-language features
 643  If anything multi-language should work, UTF-8 encoding is a must, so let's make sure we try to use that everywhere
 644  
 645  #+begin_src emacs-lisp
 646    (use-package mule
 647      :straight (:type built-in)
 648      :init
 649      (setq default-input-method 'TeX)
 650      (prefer-coding-system 'utf-8)
 651      :config
 652      (set-terminal-coding-system 'utf-8)
 653      (set-keyboard-coding-system 'utf-8))
 654  #+end_src
 655  
 656  For conveniently editing accented characters like 'é' and 'è' there are quite a few options to reach that result. I have dead characters configured as an option in the operating system, but that is far from ideal, especially when programming. As I hardly need those characters outside of emacs, i can leave that option as needed and optimize Emacs to my needs.
 657  
 658  The fallback is C-x 8 C-h which gives specific shortcuts for special characters which are available. For the exotic characters that will do just fine. For the more common characters the C-x 8 prefix is to complex.
 659  
 660  After evaluating some systems, the TeX input method suits me the best. I'd like to enable that globally by default, which needs two things:
 661  1. enable multi-language editing by default (input-method is only
 662     active in a multi-language environment)
 663  2. set the default input-method to tex
 664  
 665  There is however a problem, the TeX input method assumes the first character / string produced will always be the choice I need, without allowing selecting an alternative. This turns out to be due to =quail-define-package= which determines the way the completions work.  The problem is the =DETERMINISTIC= argument of the function, that is set to 't'. (8th argument). While I am at it, I also changed the =FORGET-LAST-SELECTION= (7th argument) to nil, so the last selection is remembered.
 666  
 667  For this to work properly we have to define a whole new input-method based on a copy of latin-ltx.el
 668  
 669  #+begin_src emacs-lisp
 670    ;; No input method will be active by default, so for each mode where
 671    ;; it needs to be active we need to activate it (by a hook for example)
 672    (defun mrb/set-default-input-method()
 673      (interactive)
 674      (activate-input-method "TeX")
 675  
 676      ;; Define a few omissions which I use regularly
 677      (let ((quail-current-package (assoc "TeX" quail-package-alist)))
 678        (quail-define-rules ((append . t))
 679                            ("\\bitcoin" ?฿)
 680                            ("\\cmd" ?⌘)
 681                            ("\\shift" ?⇧)
 682                            ("\\alt" ?⎇)
 683                            ("\\option" ?⌥)
 684                            ("\\return" ?⏎)
 685                            ("\\tab" ?↹)
 686                            ("\\backspace" ?⌫)
 687                            ("\\delete" ?⌦)
 688                            ("\\plusminus" ?±)
 689                            ("\\_1" ?₁)
 690                            ("\\_2" ?₂))))
 691  
 692  
 693    ;; Set the default language environment and make sure the default
 694    ;; input method is activated
 695    (add-hook 'set-language-environment-hook 'mrb/set-default-input-method)
 696    ;; And now we can set it
 697    (set-language-environment "UTF-8")
 698    ;; Make sure our timestamps within emacs are independent of system locale
 699    ;; This is mostly for orgmode I guess.
 700    (setq system-time-locale "nl_NL.UTF-8")
 701  
 702  
 703    ;; Activate it for all text modes
 704    (add-hook 'text-mode-hook 'mrb/set-default-input-method)
 705  #+end_src
 706  
 707  For even more esoteric characters we have to do some extra work. No font provides all Unicode characters. While I previously had a manual solution, I'm now relying on the package =uni-code-fonts= to do the proper mapping. The first run will be very slow, but the information gets cached.
 708  
 709  The problem with applying this function is that we need to be 'done' with our visual initialization or otherwise they'll do nothing (at least that I can see). So, let's group our corrections in a function and call that when we are done with our visual (X) init.
 710  
 711  #+begin_src emacs-lisp
 712    (use-package unicode-fonts
 713      :demand t
 714      )
 715  
 716    (defun mrb/unicode-font-corrections()
 717      "Set up Unicode font mappings with version-aware support."
 718      (interactive)
 719      (mrb/check-emacs-version-support 29 "unicode-fonts caching")
 720      (unicode-fonts-setup)
 721      ;; Add a line like the following for each char displaying improperly
 722  
 723      ;; mu4e uses Ⓟ,Ⓛ and Ⓣ, let's correct all letters
 724      (set-fontset-font t '(?Ⓐ . ?Ⓩ) "Symbola") ; noto line-height too high
 725      )
 726  #+end_src
 727  
 728  So, when characters do not show properly, the steps to take now are:
 729  1. Find a font which has the char
 730  2. Map the character(-range) to that font
 731  3. Optional: define a convenient way to type the character
 732  
 733  To make my life a bit easier, using the =all the icons= package for using special symbols.
 734  
 735  #+begin_src emacs-lisp
 736    (use-package all-the-icons
 737      :demand t)  ; Load unconditionally; package handles display type internally
 738  #+end_src
 739  
 740  The above in itself does nothing. It just makes using the icons easier and consistent. Other packages. With the command =M-x all-the-icons-install-fonts= the latest version of the proper fonts will be installed automaticaly, so run that once in a while.
 741  
 742  * Visual
 743  Many settings have to do with how the screen looks and behaves in a visual way. Things like screen layout, colors, highlighting etc. fall into this category.
 744  
 745  Let's set up some basics first, background dark, some default frame and cursor properties:
 746  
 747  #+begin_src emacs-lisp
 748    (setq mrb/cursor-type 'box)
 749    (setq mrb/cursor-color "DarkOrange")
 750  
 751    (setq-default frame-background-mode 'dark)
 752  
 753    ;; Only show cursor in the active window.
 754    (setq-default cursor-in-non-selected-windows nil)
 755  
 756    ;;Default frame properties frame position, color, etc
 757    (setq default-frame-alist
 758          `((cursor-type . ,mrb/cursor-type )
 759            (cursor-color . ,mrb/cursor-color)
 760            (internal-border-width . 24)
 761            (mouse-color . "white")))
 762  #+end_src
 763  
 764  ** Mode-line and status info
 765  One thing that always bothered me is the repeated mode-line for every buffer with information that only needs to be displayed once. Like the new-mail indicator or time-display for example. This section explores a configuration to scratch that itch.
 766  
 767  *Local information*, that is per buffer or per mode, should be displayed in the =mode-line= at the bottom of each visible buffer. There will be exceptions to this for special buffers where this does not make sense.
 768  
 769  *Global information*, typically at least what =global-mode-string= holds, needs to be displayed only once (per frame). This will be handled by =tab-bar-mode= at the top of each frame, under the menu-bar.
 770  
 771  *** Mode-line
 772  
 773  The format of the mode-line I would like is inspired by nano modeline:
 774  
 775  =[ status name (primary)                               secondary ]=
 776  
 777  where:
 778  - status :: gives status of buffer (RO/RW/**/-- etc.)
 779  - name   :: name or filename to identify the contents of the buffer
 780  - primary :: buffer specific primary information, left aligned
 781  - secondary :: buffer specific secondary information, right aligned
 782  
 783  Both primary and secondary are buffer and/or mode-specific Per mode configuration of the mode-line contents is possible..
 784  
 785  #+begin_src emacs-lisp
 786    (defun mrb/modeline-counters ()
 787      "Display page numbers for pdf-view-mode, line numbers for other modes."
 788      (if (eq major-mode 'pdf-view-mode)
 789          (format "[%d/%d]" (pdf-view-current-page) (pdf-cache-number-of-pages))
 790        (format "[%s/%d]" (format-mode-line "%l") (line-number-at-pos (point-max)))))
 791  
 792    (defun mrb/modeline-right-align ()
 793      "Right-align modeline content with space to the right, accounting for counter width and total window width."
 794      (let* ((counter (mrb/modeline-counters))
 795             (counter-width (string-width counter))
 796             (total-width (window-total-width))
 797             (align-pos (- total-width counter-width 4)))
 798        (propertize " " 'display `(space :align-to ,align-pos))))
 799  
 800    ;; Mmodeline using only built-in Emacs.
 801    (setq-default mode-line-format
 802                  (list
 803                   "%e" mode-line-front-space
 804                   '(:propertize ("" mode-line-mule-info mode-line-client mode-line-modified mode-line-remote)
 805                                 display (min-width (5.0)))
 806                   mode-line-frame-identification
 807                   mode-line-buffer-identification
 808                   '(vc-mode vc-mode)
 809                   "  "
 810                   mode-line-modes
 811                   '(:eval (mrb/modeline-right-align))
 812                   '(:eval (mrb/modeline-counters))))
 813  
 814  #+end_src
 815  
 816  *** Status information in tab-bar
 817  
 818  The format of the status-bar is similar to the mode-line:
 819  
 820  =[ primary                                               secondary ]=
 821  
 822  where:
 823  - primary :: global or mode specific information, *no* buffer specific information, left aligned
 824  - secondary :: global or mode specific information, *no* buffer specific information, right aligned
 825  
 826  One of the elements in the status bar will be a list of buffers which need attention for some reason (irc, compilation etc.). For this the =tracking= package will be used, which is part of circe, but has its own install recipe. I only use it for irc right now.
 827  
 828  #+begin_src emacs-lisp
 829    (use-package tracking
 830      :custom
 831      (tracking-max-mode-line-entries 1)
 832      (tracking-shorten-buffer-names-p nil)
 833  
 834      :config
 835      (tracking-mode 1))
 836  #+end_src
 837  
 838  this fills information in the =global-mode-string= which is used in the tab-bar format later on.
 839  
 840  In the tab-bar we will display messages, like in the echo area at the bottom. Messages in the bottom are sometimes a bit annoying, so we set up a filtering mechanism so we can leave out messages that are often in the way.
 841  
 842  #+begin_src emacs-lisp
 843    ;; The order of this block is important
 844    ;; for handling messages, make sure inhibit message is inserted first
 845    (add-to-list 'set-message-functions 'inhibit-message)
 846  
 847    ;; if that is the case, the inhibit-message regexps start working
 848    ;; Add all messages here that are annoying, they won't get into the message area then
 849    (setq inhibit-message-regexps
 850          '("^\\(Wrote .+\\)$"
 851            "^\\(Saving file .+\\)$"
 852            "^\\(Saving all Org buffers... done\\)$"
 853            "^\\(Garbage collecting.+\\)$"
 854            "^\\(\\[mu4e\\] .+\\)$"
 855            "^\\(Some IDs share title, see M-x org-mem-list-title-collisions\\)$"
 856            ))
 857  
 858    ;; Everything is not a good idea, if there is a question
 859    ;; (setq inhibit-message-regexps
 860    ;;       '("^\\(.+\\)$"))
 861  
 862    ;; but handle the message separately for the tabbar
 863    (setq mrb/tab-bar-message (current-message))
 864    (add-to-list 'set-message-functions
 865                 '(lambda (message) (setq mrb/tab-bar-message (or message ""))))
 866  
 867    #+end_src
 868  
 869   #+begin_src emacs-lisp
 870        ;; Tab bar configuration for global information display
 871        (use-package tab-bar
 872          :after base16-theme
 873          :custom
 874          (tab-bar-format '(tab-bar-format-menu-bar ; easy entry into menu when needed
 875                            tab-bar-format-history  ; only when history mode active, shows arrows then
 876                            mrb/tab-bar-format-primary
 877                            tab-bar-format-align-right
 878                            mrb/tab-bar-format-secondary))
 879          (tab-bar-position t "Position tab-bar under the menu-bar")
 880          (tab-bar-mode t "Enable tab-bar-mode unconditionally")
 881          (global-tab-line-mode nil "Make sure tab-line-mode is disabled")
 882  
 883          :custom-face
 884          ;; TODO tab-bar should ideally be (:inherit mode-line) but the theme's explicit
 885          ;;      :background definition prevents inheritance from overriding. Must use
 886          ;;      explicit colors here to override the theme's tab-bar definition.
 887          (tab-bar ((t (:background "#4c566a" :foreground "#eceff4" :height 1.2
 888                                    :box (:line-width 1 :color "#5e81ac")))))
 889  
 890          :config
 891          (display-time-mode -1) ; make sure it is off, it will appear on right of tab-bar else
 892  
 893          (defun mrb/current-message-tab-bar()
 894            "Show the current message in the tab-bar, after massaging it a bit."
 895            (seq-reduce (lambda (text subst)
 896                          (string-replace (car subst) (cdr subst) text))
 897  
 898                        ;; List of substitution string pairs
 899                        '(("\n" . "⮐") ; display return as character,prevent multiline somewhat
 900                          ("\"" . ""))  ; no need for the quotes here? I think.
 901                        (propertize mrb/tab-bar-message 'face '(warning))))
 902  
 903          (defun mrb/tab-bar-primary-available-chars (inner-width menu-px lead-px sec-px ch-px)
 904            "Compute character count available for the primary tab-bar message.
 905     Pixel arguments: INNER-WIDTH is frame inner width, MENU-PX is the menu-bar
 906     icon width, LEAD-PX is the leading space before the message, SEC-PX is the
 907     secondary content width, CH-PX is the per-character advance width."
 908            (floor (/ (max 0 (- inner-width menu-px lead-px sec-px)) ch-px)))
 909  
 910          (defun mrb/tab-bar-format-primary ()
 911            "Format primary tab-bar element, truncating message to prevent tab-bar overflow."
 912            (let* ((secondary-str (concat
 913                                   " "
 914                                   (format-mode-line tracking-mode-line-buffers)
 915                                   (format-mode-line global-mode-string)))
 916                   (msg-raw (mrb/current-message-tab-bar))
 917                   (available
 918                    (if (and (fboundp 'string-pixel-width) (display-graphic-p))
 919                        ;; GUI: pixel-accurate calculation.
 920                        ;; The tab-bar face uses a larger font (height 1.2x).  The
 921                        ;; per-character advance width differs from the buffer font.
 922                        ;; Measure it as the difference between 2-char and 1-char
 923                        ;; pixel widths to avoid the first-character overhead.
 924                        (let* ((ch-px (max 1 (- (string-pixel-width (propertize "xx" 'face '(warning tab-bar)))
 925                                                (string-pixel-width (propertize "x"  'face '(warning tab-bar))))))
 926                               (lead-px (string-pixel-width (propertize " " 'face 'tab-bar)))
 927                               (sec-s (let ((s (copy-sequence secondary-str)))
 928                                        (add-face-text-property 0 (length s) 'tab-bar t s)
 929                                        s))
 930                               (sec-px (string-pixel-width sec-s))
 931                               (menu-str (let ((items (tab-bar-format-list (list 'tab-bar-format-menu-bar))))
 932                                           (when items (nth 2 (car items)))))
 933                               (menu-px (if menu-str
 934                                            (let ((s (copy-sequence menu-str)))
 935                                              (add-face-text-property 0 (length s) 'tab-bar t s)
 936                                              (string-pixel-width s))
 937                                          0)))
 938                          (mrb/tab-bar-primary-available-chars
 939                           (frame-inner-width) menu-px lead-px sec-px ch-px))
 940                      ;; TTY: character-based fallback
 941                      (max 0 (- (frame-width) 3 (string-width secondary-str)))))
 942                   (msg (if (> available 0)
 943                            (truncate-string-to-width msg-raw available nil nil "…")
 944                          "…")))
 945              (concat " " msg)))
 946  
 947          (defun mrb/tab-bar-format-secondary ()
 948            (concat
 949             (propertize " " 'display `(raise 0.2))
 950             (format-mode-line tracking-mode-line-buffers) ; buffers that need attention
 951             (format-mode-line global-mode-string))))
 952  
 953  #+end_src
 954  
 955  ** Theme
 956  I'm using the [[https://github.com/arcticicestudio/nord][Nord color palette]]. This palette defines 16 colors.
 957  
 958  There is a base16 theme which uses exactly these colors and there are varieties which use the 16 colors and some derivatives of it.
 959  
 960  Although I think the 16 color system is a bit simplistic and, for emacs at least, I will need to customize more later on, the main lure of this system is that I can use the same set for many of my programs I use (emacs, xterm, i3, dunst etc.) which sounds attractive. So, I'm starting with the =base16-nord= theme and see where this leaves me.
 961  
 962  #+begin_src emacs-lisp
 963    (use-package base16-theme
 964      :custom
 965      ;; Do config here and finally load the theme
 966      (base16-theme-distinct-fringe-background nil)
 967      (base16-theme-highlight-mode-line 'contrast)
 968      (base16-theme-256-color-source "colors") ; This gives way better colors with terminal emacs?
 969  
 970      :init
 971      (load-theme 'base16-nord t)
 972  
 973      :custom-face
 974      ;; Adjust only emacs core faces here, the rest should be close to their repective packages
 975  
 976      ;; Default font configuration
 977      (default ((t (:family "Hack" :slant normal :weight normal :height 112 :width normal))))
 978  
 979      (font-lock-comment-face ((t (:foreground "bisque3"))))
 980      (font-lock-doc-face ((t (:inherit font-lock-comment-face))))
 981  
 982      (button ((t (:foreground "#d08770" :weight semi-bold))))
 983  
 984      (show-paren-match ((t (:foreground "#eceff4" :background "#81a1c1" :inherit nil :weight normal))))
 985      (show-paren-mismatch ((t (:foreground "#2e3440" :background "#bf616a" :inherit nil :weight normal))))
 986  
 987      (region ((t (:foreground unspecified :background "#3b4252" :inherit nil :weight normal))))
 988      (fringe ((t (:foreground "#a3be8c"))))
 989  
 990      (hl-line ((t (:foreground unspecified :background "#5e81ac"))))
 991  
 992      (line-number-current-line ((t (:background "#4c566a"))))
 993  
 994      (cursor ((t (:background ,mrb/cursor-color))))
 995      (mouse ((t (:background "white"))))
 996  
 997      (header-line ((t (:foreground "#eceff4" :background "#4c566a"))))
 998      (mode-line ((t (:background "#4c566a" :foreground "#eceff4" :height 1.2
 999                                  :box (:line-width 1 :color "#5e81ac")))))
1000  
1001      (mode-line-inactive ((t (:background "#2e3440" :foreground "#a3be8c" :height 1.2
1002                                           :box (:line-width 1 :color "#434c5e")))))
1003  
1004      (completions-annotations ((t (:inherit (italic font-lock-comment-face))))))
1005  #+end_src
1006  
1007  ** Miscellaneous other visual settings follow.
1008  
1009  #+begin_src emacs-lisp
1010    ;; no splash screen
1011    (setq inhibit-startup-screen  t)
1012    (setq inhibit-startup-message t)
1013    (setq initial-scratch-message nil)
1014  
1015    (use-package mic-paren
1016      :custom
1017      (paren-highlight-at-point nil)
1018  
1019      :config
1020      (paren-activate))
1021  
1022    ;; Font locking has always been the major performance hog for me, so here's the current
1023    ;; state of variables trying to minimize typing delays. The 3 position indication in
1024    ;; comments is the  experience playing with it:
1025    ;;   ? unknown effect
1026    ;;   ! needed can't change
1027    ;;   + minor effect, but noticeable
1028    ;;  ++ major effect, very noticeable
1029    ;; +++ very high effect on performance (mostly subjective, but supported by some light profiling)
1030    (setq font-lock-support-mode 'jit-lock-mode)
1031    (setq jit-lock-defer-time 0.25)   ; defer fontification for 250ms after typing stops
1032    (setq jit-lock-stealth-time nil)  ; disable stealth fontification, use deferred only
1033  
1034  
1035    ;; Make underlining nicer
1036    (setq  underline-minimum-offset 3)
1037  
1038    ;; Show color of '#RRBBGG texts
1039    (use-package rainbow-mode
1040      :diminish
1041      :custom
1042      ;; Enable color name globally
1043      rainbow-r-colors t)
1044  
1045    ;; Give commands the option to display full-screen
1046    (use-package fullframe)
1047  #+end_src
1048  
1049  ** Lines
1050  The most important element of an editor is probably how lines of text are displayed. This warrants its own section.
1051  
1052  The problem is that 'line' needs clarification because there can be a difference between a logical line and a visual line. It's sometimes important that things stay on one logical line but are displayed over multiple lines. This may be different per mode.
1053  
1054  I recently reversed my stance on this and am now enabling visual line mode everywhere (meaning one logical line can be smeared out over multiple display lines. This also enables word-wrap which breaks lines visually between words instead of at random characters.
1055  
1056  To help me see the difference between visual lines and logical lines I let emacs display indicators in the fringe and define a function to help me unfill existing paragraphs.
1057  
1058  #+begin_src emacs-lisp
1059    ;; Set some defaults
1060    (setq-default word-wrap t               ; why would i want to have no wrapping by word?
1061                  fill-column 100
1062                  truncate-lines nil)
1063  
1064    (setq sentence-end-double-space nil)    ; A period ends a sentence, period. relevant for fill
1065  
1066    (use-package visual-fill-column
1067      :commands (turn-on-visual-fill-column-mode))
1068  
1069    ;; Similar to mail messages, use vertical bar for wrapped paragraphs
1070    (setq visual-line-fringe-indicators
1071          '(vertical-bar nil))
1072  
1073    ;; For all text modes use visual-line-mode
1074    (add-hook 'text-mode-hook 'visual-line-mode)
1075  
1076    ;; From:https://www.emacswiki.org/emacs/UnfillParagraph
1077    (defun unfill-paragraph (&optional region)
1078      "Takes a multi-line paragraph and makes it into a single line of text."
1079      (interactive (progn (barf-if-buffer-read-only) '(t)))
1080      (let ((fill-column (point-max))
1081            ;; This would override `fill-column' if it's an integer.
1082            (emacs-lisp-docstring-fill-column t))
1083        (fill-paragraph nil region)))
1084  
1085    ;; Similar to M-q for fill, define M-Q for unfill
1086    (bind-key "M-Q" 'unfill-paragraph)
1087  #+end_src
1088  
1089  Line-numbering is off by default, it tends to slow things down, but if I want it on, I almost always want them relative
1090  
1091  #+begin_src emacs-lisp
1092    (use-package display-line-numbers
1093      :straight (:type built-in)
1094      :config
1095      (setq-default display-line-numbers-mode nil)
1096      :custom
1097      (display-line-numbers-type 'relative))
1098  #+end_src
1099  
1100  ** Client dependent settings
1101  Because most of my usage involves having a long lasting emacs daemon, some settings only come into scope once a client connects to that daemon. Most of these settings have to do with appearance and are related to having X available.  Anyways, some settings need to be moved to the event when a client visits the server, so we can still apply these settings transparently.
1102  
1103  Note that if this code is evaluated any call to =emacsclient= (be that from external or, more importantly Magit) will try to run this code and magit will fail if there's an error in the next section. Take extra care here.
1104  #+begin_src emacs-lisp
1105    ;; Universal settings that work on all frame types (GUI and terminal)
1106    (defun mrb/run-universal-settings ()
1107      "Apply settings that work on both GUI and terminal frames."
1108      (menu-bar-mode -1)   ;; No menu-bar (works in terminal too)
1109      (tab-bar-mode t))    ;; We *do* want the tab-bar
1110  
1111    ;; GUI-specific settings that require graphic display
1112    (defun mrb/run-gui-settings ()
1113      "Apply settings that only work on GUI frames."
1114      (when (display-graphic-p)
1115        (tool-bar-mode -1)   ;; No tool-bar
1116        (scroll-bar-mode -1) ;; No scroll-bar
1117        (tooltip-mode -1)    ;; No tooltips
1118        (setq fringe-mode '(14 . 14)) ;; Fringe, left and right for the continuation characters
1119        (set-fringe-mode fringe-mode)
1120        (setq indicate-buffer-boundaries 'left)
1121        (mrb/unicode-font-corrections)))
1122  
1123    ;; Main dispatcher: runs appropriate settings based on frame type
1124    (defun mrb/run-client-settings (&optional frame)
1125      "Run client settings for FRAME (or current frame if nil).
1126    Applies universal settings to all frames, GUI settings only to graphic frames."
1127      (interactive)
1128      (with-selected-frame (or frame (selected-frame))
1129        (mrb/run-universal-settings)
1130        (mrb/run-gui-settings)))
1131  
1132    ;; Use appropriate hook based on startup mode
1133    (if (daemonp)
1134        ;; Daemon mode: wait for frame to be created by server
1135        (add-hook 'server-after-make-frame-hook 'mrb/run-client-settings)
1136      ;; Non-daemon mode: apply settings to initial frame immediately
1137      (mrb/run-client-settings)
1138      ;; Also apply to any subsequently created frames
1139      (add-hook 'after-make-frame-functions 'mrb/run-client-settings))
1140  #+end_src
1141  ** Context dependent visualization
1142  Next to the information registered in the theme which controls the overall look and feel, there is additional visualization which depend on context dependent factors, such as:
1143  
1144  - the user spells a word wrong
1145  - the syntax of a certain phrase is wrong grammatically, depending on
1146    what language the user is writing in
1147  - the status of a certain region is changing, dependent on certain
1148    rules
1149  - a part of the text could be rendered as non-text, images or formulas for example
1150  
1151  *** Flycheck
1152  Flycheck is a syntax checking package which seems to have better support than flymake, which is built-in. I've no configuration for flymake specifically, but some packages enable it automatically (elpy for example). Where applicable, I gather the necessary disable actions here first.
1153  
1154  Now we can start configuring flycheck
1155  
1156  #+begin_src emacs-lisp
1157    (use-package flycheck-pos-tip)
1158  
1159    (use-package flycheck
1160      ;; Use popup to show flycheck message
1161      :after flycheck-pos-tip
1162      :config
1163      (with-eval-after-load 'flycheck
1164        (flycheck-pos-tip-mode)))
1165  #+end_src
1166  
1167  Currently actively configured are:
1168  - javascript :: eslint with a config file
1169  
1170  Flyspell is similar to flycheck but for text languages. I'm setting some of the flyspell faces to the flycheck faces so they are consistent
1171  
1172  #+begin_src emacs-lisp
1173    (use-package flyspell
1174      :straight (:type built-in)
1175      :custom-face
1176      (flyspell-incorrect ((t (:inherit flycheck-error))))
1177      (flyspell-duplicate ((t (:foreground "#2e3440" :inherit flycheck-warning))))
1178      :custom
1179      (flyspell-mark-duplications-flag nil "Flyspell Duplicates as warning")
1180      (flyspell-issue-message-flag nil)
1181      (ispell-alternate-dictionary "/usr/share/hunspell/nl_NL.dic")
1182      (ispell-use-framepop-p t))
1183  
1184    (use-package guess-language
1185      :custom
1186      (guess-language-languages '(en de nl)))
1187  #+end_src
1188  
1189  Still on the wish-list:
1190  - activate flyspell automatically for all text-modes?
1191  - language detection (English and Dutch mainly)
1192  - check for language pythons, which was troublesome before
1193  - evaluate usage in org source blocks (many checks do not apply)
1194  
1195  *** Math rendering
1196  Mathematics requires some specific rendering. In simple cases, using the proper unicode symbols may suffice, but for anything other than the most basis equations or formulas, some help is needed.  $\LaTeX$ is the defacto standard for this and there's plenty of support for that in emacs.
1197  
1198  For $\TeX$ and $\LaTeX$ fragments I use the =texfrag= package. This allows to insert math fragments inside documents.
1199  
1200  Example:
1201  
1202  $$e^{ix} = cos x + i sinx$$
1203  
1204  #+begin_src emacs-lisp
1205    (use-package texfrag
1206      :disabled t
1207      :diminish
1208  
1209      :custom
1210      (texfrag-prefix "")
1211  
1212      :config
1213      (texfrag-global-mode 1))
1214  #+end_src
1215  *** Highlighting tags and keywords
1216  
1217  In many modes, but especially in orgmode, the use of tags and keywords is abundant. In the set of packages that [[https://github.com/rougier][Nicolas P. Rougier]] has made for emacs, there's also a gem which generates SVG labels from keyword matches.
1218  
1219  The use of that package is simple, just create a list of keyword matches in the =svg-tag-tags= variable and match those up with a command to create a tag.
1220  
1221  I have at least the following usecases in mind:
1222  
1223  1. Render my orgmode keywords as tags;
1224  2. Use in mu4e to render the tags
1225  3. perhaps some TODO keywords in code, I use that quite a bit.
1226  
1227  The first is the easiest, as the format of =org-todo-keyword-faces= is nigh perfect to transform into a list that can be used by svg-tag-mode.
1228  
1229  #+begin_src emacs-lisp
1230    ;; Trickiest here is to know when org-todo-keyword-faces has been filled
1231    (use-package svg-tag-mode
1232      :after (org org-journal base16-theme)
1233      :commands svg-tag-mode
1234  
1235      :config
1236      ;; Only enable in GUI frames where SVG rendering works
1237      (defun mrb/maybe-enable-svg-tag-mode ()
1238        "Enable svg-tag-mode only in graphic displays."
1239        (when (display-graphic-p)
1240          (svg-tag-mode 1)))
1241  
1242      (add-hook 'org-mode-hook #'mrb/maybe-enable-svg-tag-mode)
1243  
1244      ;; Readymade svg-tag-tags generator from org keywords
1245      (defun mrb/gen-svg-tags (kf)
1246        "Generate a `svg-tag-tags element from a `org-todo-keyword-faces element"
1247        (let ((k (concat "\\(" (car kf) "\\) ")) ; pass keyword, not the space
1248              (f (cdr kf)))
1249          `(,k . ((lambda (tag) (svg-tag-make tag :face ',f :inverse t :margin 0 ))))))
1250  
1251      ;; This gets initialized the first time svg-tag-mode is called, so then it must have
1252      ;; the proper keywords in org-todo-keyword-faces
1253      (setq svg-tag-tags (mapcar 'mrb/gen-svg-tags org-todo-keyword-faces))
1254  
1255      ;; See: https://github.com/rougier/svg-tag-mode/issues/27
1256      ;; Org agenda does not use font-lock, so needs separate overlay to render
1257      (defun mrb/org-agenda-show-svg ()
1258        (let* ((case-fold-search nil)
1259               (keywords (mapcar #'svg-tag--build-keywords svg-tag--active-tags))
1260               (keyword (car keywords)))
1261          (while keyword
1262            (save-excursion
1263              (while (re-search-forward (nth 0 keyword) nil t)
1264                (overlay-put (make-overlay
1265                              (match-beginning 0) (match-end 0))
1266                             'display  (nth 3 (eval (nth 2 keyword)))) ))
1267            (pop keywords)
1268            (setq keyword (car keywords)))))
1269      (add-hook 'org-agenda-finalize-hook #'mrb/org-agenda-show-svg))
1270  #+end_src
1271  * Buffers and files
1272  How do I deal with all those buffers and files?
1273  
1274  For starters, make sure that they have unique buffer names so I don't get confused:
1275  
1276  #+begin_src emacs-lisp
1277    (setq uniquify-buffer-name-style 'forward)
1278  #+end_src
1279  
1280  For every file-based buffer, I want auto-save to be on, but not in the same location as the file, as that clutters up everything. For that, I add to the list of file-name transforms to have (local) files auto saved in a designated folder)
1281  
1282  #+begin_src emacs-lisp
1283    (setq auto-save-default t
1284          mrb/auto-save-folder (concat user-emacs-directory "auto-save-list/"))
1285  
1286    (add-to-list 'auto-save-file-name-transforms
1287                 (list "\\(.+/\\)*\\(.*?\\)" (expand-file-name "\\2" mrb/auto-save-folder))
1288                 t)
1289  #+end_src
1290  
1291  Auto save helps to automatically recover to the latest save point, but I need a bit more. This is done through versioned backup file. The recovery process is then manual though.
1292  
1293  #+begin_src emacs-lisp
1294    (setq create-lockfiles nil           ; just clutters for me, no real use?
1295          backup-by-copying t            ; otherwise symlinks galore
1296  
1297          backup-directory-alist '(
1298                                   ("." . "/tmp/emacs/"))
1299  
1300          delete-old-versions t
1301          kept-new-versions 3
1302          kept-old-versions 2
1303          version-control t)
1304  #+end_src
1305  
1306  The auto save helps for the minor disasters, my backups help for the major disasters. What else is needed is a 'normal save' but automatically when I forget to do this.
1307  
1308  What I am aiming for here is to not have to think about explicitly saving for certain files. Typically when typing stuff in org-mode I just want the stuff saved that I have types so far.  For /some/ files, each save is committed if I think the content warrants this (for example if I think going back to an earlier version is a likely event).
1309  
1310  #+begin_src emacs-lisp
1311    (defconst mrb/idle-timer 15
1312      "Time emacs needs to be idle to trigger the save idle timer")
1313  
1314    ;; This function does the actual useful thing
1315    (defun mrb/save-timer-callback()
1316      "Callback function that runs when emacs is idle for `mrb/idle-timer' seconds.
1317  Auto-saves org files unless Claude Code IDE session is active to avoid state conflicts."
1318      (unless (and (fboundp 'mrb/claude-session-active-p)
1319                   (mrb/claude-session-active-p))
1320        (org-save-all-org-buffers)))
1321  
1322    ;; Activate the timer
1323    ;; The # means: evaluate first ( I keep forgetting this stuff )
1324    (run-with-idle-timer mrb/idle-timer 'always #'mrb/save-timer-callback)
1325  #+end_src
1326  
1327  Also, save the location in files between sessions.
1328  
1329  #+begin_src emacs-lisp
1330    ;; Minibuffer prompt is a prompt, don't enter it as text.
1331    (setq minibuffer-prompt-properties '(read-only t point-entered minibuffer-avoid-prompt face minibuffer-prompt))
1332    ;; Don't echo keystrokes in the minibuffer, prefix will be hinted on after 2 seconds or so.
1333    (setq echo-keystrokes 0)
1334  
1335    ;; Save places in buffers between sessions
1336    (use-package saveplace
1337      :straight (:type built-in)
1338      :init
1339      (setq-default save-place-mode t))
1340  #+end_src
1341  
1342  
1343  Bind renaming the visiting file to a new location to =C-x-w= now that we have =rename-visited-file=
1344  
1345  #+begin_src emacs-lisp
1346    (bind-key "C-x C-w" 'rename-visited-file)
1347    (bind-key "C-S-s" 'write-file)
1348  #+end_src
1349  
1350  For file management itself, dired is the defacto standard, but I do not use it much really. The =casual= package is helpfule showing the keyboard bindings.
1351  
1352  #+begin_src emacs-lisp
1353    (use-package dired
1354      :straight (:type built-in)
1355      ;; auto-revert global is only for file buffers, so explicit enable is needed
1356      :hook (dired-mode-hook . auto-revert-mode)
1357      :bind (:map dired-mode-map ("C-o" . 'casual-dired-tmenu)))
1358  #+end_src
1359  
1360  * Modes
1361  Customization setting for specific modes. Most of the modes I use have a separate section, so this section is only for other modes.
1362  
1363  To determine the default major mode; the mode that is started with before all the magic starts is determined by buffer-file-name. If we have it, the normal routine can be followed. If there is no filename yet, the buffer-name is used to determine which mode is needed.
1364  
1365  By looking at the code this may have a side-effect, because the buffer-file-name is given a value. Let's try this and see if it gives any issues.
1366  #+begin_src emacs-lisp
1367    (setq-default major-mode
1368                  (lambda ()
1369                    (if buffer-file-name
1370                        (fundamental-mode)
1371                      (let ((buffer-file-name (buffer-name)))
1372                        (set-auto-mode)))))
1373  #+end_src
1374  
1375  When emacs does not determine the right mode, I sometimes want a mode-string somewhere in the file. Typically that string gets inserted on a line which is commented out in the proper syntax for that mode.
1376  
1377  #+begin_src emacs-lisp
1378    (defun mrb/buffer-majormode (&optional buffer-or-name)
1379      "Returns the major-mode of the specified buffer, or
1380        the current buffer if no buffer is specified"
1381      (buffer-local-value 'major-mode
1382                          (if buffer-or-name
1383                              (get-buffer buffer-or-name)
1384                            (current-buffer))))
1385  
1386    (defun mrb/insert-mode-string()
1387      "Inserts a mode string for the current mode at beginning of the current buffer"
1388      (interactive)
1389      (let ((m (symbol-name (mrb/buffer-majormode))))
1390        (save-excursion
1391          (goto-char (point-min))
1392          (insert "-*- mode:"
1393                  (substring m 0 (string-match "-mode" m))
1394                  " -*-")
1395          (comment-region (point-min) (point)))))
1396  #+end_src
1397  
1398  First bring in a bunch of modes that are used regularly, but have no other config than a map to some extensions. =:requires= is set to nil for those packages which aret built in.
1399  
1400  #+begin_src emacs-lisp
1401    (use-package adoc-mode             :mode ("\\.asciidoc\\'" "\\.adoc\\'"))
1402    (use-package apache-mode           :mode "\\.htaccess\\'")
1403    (use-package conf-mode
1404      :straight (:type built-in))
1405    (use-package i3wm-config-mode)
1406    (use-package css-mode
1407      :straight (:type built-in)
1408      :mode ("\\.css\\'" "\\.mss\\'"))
1409    (use-package csv-mode              :mode "\\.csv\\'")
1410  
1411    (use-package gnuplot)
1412    (use-package gnuplot-mode          :mode "\\.gp\\'")
1413    (use-package js2-mode              :mode "\\.js\\'")
1414    (use-package lua-mode              :mode "\\.lua\\'")
1415    (use-package php-mode              :mode "\\.php\\'")
1416    (use-package sass-mode             :mode "\\.sass\\'")
1417    (use-package markdown-ts-mode      :mode "\\.md\\'")
1418    (use-package udev-mode             :mode "\\.rules\\'")
1419    (use-package dockerfile-mode       :mode "Dockerfile")
1420    (use-package docker)
1421  #+end_src
1422  
1423  Now, do the other mode related packages which require a bit of configuration.
1424  #+begin_src emacs-lisp
1425  
1426    (use-package eimp
1427      :hook (image-mode . eimp-mode))
1428  
1429    (use-package rainbow-mode
1430      :hook (conf-mode css-mode))
1431  
1432    ;; Open scratch buffer by default in the mode we are in at the moment
1433    ;; with C-u prefix a mode will be asked to use
1434    (use-package scratch
1435      :straight (:type built-in))
1436    ;; Don't throw away scratch buffer
1437    (use-package persistent-scratch
1438      :config
1439      (persistent-scratch-setup-default))
1440  
1441    (use-package nxml-mode
1442      :straight (:type built-in)
1443      :hook ((nxml-mode . (lambda () (set-input-method nil)))
1444             (nxml-mode . turn-off-auto-fill))
1445      :custom
1446      (nxml-heading-element-name-regexp "\\|.*")
1447      (nxml-section-element-name-regexp "\\|file\\|.+"))
1448  #+end_src
1449  * Org-mode
1450  Orgmode configuration is probably the largest part of my Emacs configuration, because most of the time I spent in Emacs, when not coding, is spent in org-mode.
1451  
1452  ** Initialization of Orgmode
1453  I loosely follow the GTD method for organizing things and I want a quick shortcut to start my main file.
1454  
1455  #+name: gtd-starter
1456  #+begin_src emacs-lisp :tangle no
1457    (defun mrb/gtd()
1458      "Start my GTD system"
1459      (interactive)
1460      (find-file org-default-notes-file))
1461  #+end_src
1462  
1463  We do not have to load the main orgmode location, because we already did that on the main initialization to get org-babel started.
1464  
1465  Also make sure we never load org from internal, this can happen when functions were defined in the included org version and not anymore in newer versions. We want an error, not a silent load of the older function.
1466  
1467  #+COMMENT: This duplicates the one in init.el, problems?
1468  #+begin_src emacs-lisp
1469    (require 'org-agenda)
1470  
1471    (use-package org
1472      :after outline
1473      :straight (org
1474                 :includes (org-agenda org-capture org-crypt org-datetree org-protocol))
1475      :hook ((org-mode . turn-on-visual-line-mode)
1476             (org-mode . turn-on-visual-fill-column-mode)
1477             (org-mode . (lambda () (setq-local tab-width 8)))
1478             (org-indent-mode . (lambda () (diminish 'org-indent-mode)))
1479             (org-agenda-mode . (lambda () (hl-line-mode 1))))
1480  
1481      :init
1482      <<orgmode-keywords>>
1483      <<gtd-starter>>
1484  
1485      :mode ("\\.txt\\'" "\\.org\'")
1486      :bind (("C-c g"   . 'mrb/gtd)
1487             ("C-c a"   . 'org-agenda)
1488             ("C-c b"   . 'org-switchb)
1489             ("C-s-s"   . 'org-save-all-org-buffers)
1490             ("C-c l"   . 'org-store-link)        ; Is this not bound to C-c l by default
1491             :map org-mode-map
1492             ("s-."     . 'org-todo)
1493             ("s->"     . 'mrb/org-cancel-todo)
1494             ("C-s-."   . 'mrb/force-org-todo)
1495             ("M-p"     . 'org-set-property)
1496             :map org-agenda-mode-map
1497             ("s-."   . 'org-agenda-todo)
1498             ("s->"   . 'mrb/org-agenda-cancel-todo)
1499             ("C-s-." . 'mrb/force-org-agenda-todo)
1500             ("C-."   . 'org-agenda-schedule)
1501             ("M-p"   . 'org-set-property)
1502             ("C-x m" . 'mrb/construct-mail))
1503  
1504      :custom-face
1505      (org-date                 ((t (:foreground "#8fbcbb" :underline nil))))
1506      (org-verbatim             ((t (:foreground "#8fbcbb"))))
1507      (org-list-dt              ((t (:foreground "#b48ead"))))
1508      (org-drawer               ((t (:height 0.8 :inherit font-lock-comment-face))))
1509      (org-code                 ((t (:foreground "dodger blue" :weight bold))))
1510      (org-block                ((t (:background "#313745"))))
1511      (org-block-begin-line     ((t (:foreground "dark gray"))))
1512      (org-headline-done        ((t (:inherit font-lock-comment-face))))
1513      (org-inlinetask           ((t (:inherit font-lock-comment-face))))
1514  
1515      ;; User defined faces for org-mode
1516      (mrb/org-todo-keyword-TODO     ((t ( :foreground "#88c0d0" :weight bold))))
1517      (mrb/org-todo-keyword-DONE     ((t ( :foreground "#a3be8c" :weight bold))))
1518      (mrb/org-todo-keyword-WAITING  ((t ( :foreground "#bf616a" :weight bold))))
1519      (mrb/org-todo-keyword-CANCELLED ((t ( :foreground "bisque4" :weight bold))))
1520      (mrb/org-todo-keyword-BUY      ((t ( :foreground "#d08770" :weight bold))))
1521      (mrb/org-todo-keyword-HOWTO    ((t ( :foreground "#5e81ac" :weight bold))))
1522      (mrb/org-todo-keyword-INFO     ((t ( :foreground "#ebcb8b" :weight bold))))
1523      (mrb/org-todo-keyword-COLLECT  ((t ( :foreground "#a3be8c" :weight bold))))
1524      (mrb/org-todo-keyword-SOLVE    ((t ( :foreground "#8fbcbb" :weight bold))))
1525      (mrb/org-todo-keyword-READ     ((t ( :foreground "#b48ead" :weight bold))))
1526      (mrb/org-todo-keyword-READING  ((t ( :foreground "#bf616a" :weight bold))))
1527      (mrb/org-todo-keyword-PLAN     ((t ( :foreground "#d8dee9" :weight bold))))
1528  
1529  
1530      :config
1531      <<orgmode-generalconfig>>
1532      ;; Category icons I have configured elsewhere.
1533      <<org-agenda-visuals>>
1534  
1535      <<orgmode-itemactions>>
1536  
1537      <<orgmode-scheduling>>
1538  
1539      ;; Allow for archiving and refiling in a date organized tree
1540      (use-package org-datetree )
1541      (use-package org-protocol
1542        :config
1543        ;; If nothing is specified, create a TODO item
1544        (setq org-protocol-default-template-key "t")))
1545  #+end_src
1546  
1547  Gather generic config variables for orgmode in one section
1548  
1549  #+name: orgmode-generalconfig
1550  #+begin_src emacs-lisp :tangle no
1551    ;; org-directory is set in personal.el
1552    (setq org-metadir (mrb/org-file "_orgmeta/")
1553  
1554          org-use-fast-todo-selection t
1555          org-use-fast-tag-selection t
1556          org-use-speed-commands t
1557          org-fast-tag-selection-single-key 'expert
1558          org-enforce-todo-dependencies t       ; we do want task dependencies
1559          org-enforce-todo-checkbox-dependencies nil ; but relax checkbox constraints for it
1560  
1561          ;; We do not do priorities
1562          org-enable-priority-commands nil
1563  
1564          ;; Agenda settings
1565          org-agenda-inhibit-startup t
1566          org-agenda-dim-blocked-tasks nil
1567          org-agenda-use-tag-inheritance nil
1568  
1569          ;; NOTE: this will get overridden later on by org-mem
1570          org-agenda-files (mrb/org-file ".agenda_files")
1571          org-agenda-include-diary t
1572          org-agenda-start-with-log-mode t
1573          org-agenda-todo-ignore-scheduled "future"
1574          org-agenda-ignore-properties '(effort stats appt category)
1575          org-agenda-todo-ignore-scheduled 'future
1576          org-agenda-log-mode-items '(closed clock state)
1577          org-agenda-skip-deadline-prewarning-if-scheduled t
1578          org-agenda-text-search-extra-files (cons 'agenda-archives (directory-files
1579                                                                     (expand-file-name (concat org-metadir)) t ".+\\.org"))
1580          ;; Habits
1581          org-habit-show-habits-only-for-today nil
1582  
1583          ;; Pressing enter on a link should activate it
1584          org-return-follows-link t
1585  
1586          ;; Shift+arrow key handling: org-mode commands vs. text selection
1587          ;; Setting: t = org commands take precedence in org buffers, shift-select works elsewhere
1588          ;; Trade-off: 'always would enable shift-select everywhere but would disable org-mode's
1589          ;; shift+arrow commands (S-left/S-right for promote/demote, S-up/S-down for move).
1590          ;; Current setting (t) is the best compromise: org functionality works fully.
1591          org-support-shift-select t
1592  
1593  
1594          ;; Auto detect blank line need, this is the default, but I explicitly set this
1595          ;; because it needs to be this or my capturing breaks due to org-capture popup
1596          org-blank-before-new-entry '((heading . auto) (plain-list-item . auto))
1597          org-export-htmlize-output-type 'css
1598  
1599          ;; Use auto-mode-alist for all file links/types
1600          org-file-apps '((auto-mode . emacs))
1601  
1602          org-fontify-done-headline t
1603          org-goto-interface 'outline-path-completion
1604          ;; non nil is just direct children, what an ODD name!!!!
1605          org-hierarchical-todo-statistics nil
1606          org-provide-todo-statistics t
1607  
1608          org-log-into-drawer t
1609          org-log-redeadline 'note
1610          org-log-reschedule 'time
1611          org-todo-repeat-to-state "TODO"
1612  
1613          org-modules '(org-habit
1614                                 org-inlinetask
1615                                 org-toc org-mac-iCal org-mouse
1616                                 org-tempo)
1617          org-remember-default-headline ""
1618          org-special-ctrl-a/e t
1619          org-stuck-projects '("-inactive/+TODO" ("TODO" "WAITING") nil "")
1620          org-track-ordered-property-with-tag nil
1621          org-startup-indented t
1622          org-startup-folded 'show2levels
1623  
1624          org-archive-location (concat org-metadir
1625                                        (format-time-string "archive-%Y.org")
1626                                        "::datetree/")
1627          org-default-notes-file (mrb/org-file "GTD.org")
1628          diary-file (concat org-metadir "DIARY")
1629  
1630          ;; Heading and list behavior
1631          org-M-RET-may-split-line '((default . t) (headline))
1632          org-insert-heading-respect-content nil
1633          org-list-allow-alphabetical t
1634  
1635          ;; Blocking and clocking
1636          org-blocker-ignore-ancestor-siblings t
1637          org-clock-x11idle-program-name "xprintidle"
1638          org-closed-keep-when-no-todo t
1639  
1640          ;; Export and entities
1641          org-entities-user '(("cmd" "\\cmd{}" nil "⌘" "⌘" "⌘" "⌘"))
1642          org-html-toplevel-hlevel 3
1643          org-timestamp-custom-formats '("<%m/%d/%y %a>" . "<%H:%M>")
1644  
1645          ;; Agenda custom commands
1646          org-agenda-custom-commands
1647          '(("w" "Waiting For list" tags-todo "-inactive/WAITING"
1648             ((org-agenda-overriding-header "WAITING FOR-list") (org-agenda-dim-blocked-tasks t)
1649              (org-agenda-group-by-property "Responsible")))
1650            ("b" "Buying list"
1651             ((tags-todo "-inactive+buy/-BUY" ((org-agenda-overriding-header "Buying list (tagged)")))
1652              (tags-todo "-inactive/BUY" ((org-agenda-overriding-header "Buying list (keyword)"))))
1653             nil)
1654            ("p" "Active project list" tags-todo
1655             "-ignore-inactive+LEVEL>1-TODO=\"DONE\"-TODO=\"CANCELLED\"-TODO=\"INFO\""
1656             ((org-agenda-overriding-header "Active project list")
1657              (org-agenda-skip-function 'mrb/skip-non-projects) (org-agenda-dim-blocked-tasks nil)
1658              (org-agenda-group-by-property "Group") (org-agenda-sorting-strategy '(alpha-up))))
1659            ("A" "Active task list" tags-todo "+SCHEDULED=\"\"-inactive/TODO"
1660             ((org-agenda-group-by-property "Group") (org-agenda-dim-blocked-tasks 'invisible)))
1661            ("r" "To Review"
1662             ((tags-todo "SCHEDULED=\"\"+DEADLINE=\"\"-{.}/TODO"
1663                         ((org-agenda-overriding-header "Untagged items")))
1664              (tags-todo "-inactive+SCHEDULED=\"\"+DEADLINE=\"\"+TODO=\"TODO\"+{.}"
1665                         ((org-agenda-overriding-header "Unscheduled active items"))))
1666             ((org-agenda-dim-blocked-tasks 'invisible)))
1667            ("c" "Scheduled overview" tags-todo "SCHEDULED<>\"\"|DEADLINE<>\"\"/TODO"
1668             ((org-agenda-overriding-header "SCHEDULED") (org-agenda-view-columns-initially t)
1669              (org-agenda-overriding-columns-format "%65ITEM %25Responsible %SCHEDULED %DEADLINE %TAGS")
1670              (org-agenda-dim-blocked-tasks t)))
1671            ("l" "Blocked projects and tasks"
1672             ((tags-todo "+BLOCKED=\"t\"/PROJ"
1673                         ((org-agenda-overriding-header "Blocked projects")
1674                          (org-agenda-dim-blocked-tasks t)))
1675              (tags-todo "+BLOCKED=\"t\"/TODO"
1676                         ((org-agenda-overriding-header "Blocked tasks") (org-agenda-dim-blocked-tasks t)
1677                          (org-agenda-group-by-property "Group"))))
1678             nil nil)
1679            ("n" "Next Action List [hides blocked/inactive/waiting/INBOX-ed]" tags-todo
1680             "+SCHEDULED=\"\"+DEADLINE=\"\"-BLOCKED=\"t\"-inactive-habit-ARCHIVE/-WAITING-INFO-HOWTO"
1681             ((org-agenda-overriding-header "Next Action List") (org-agenda-dim-blocked-tasks 'invisible)
1682              (org-agenda-group-by-property "CREATED")))
1683            ("D" "Items ready for archiving" todo "DONE"
1684             ((org-agenda-overriding-header "Items ready for archiving")
1685              (org-agenda-group-by-property "CREATED")))
1686            ("g" "AGENDA"
1687             ((agenda ""
1688                      ((org-agenda-filter-preset '("-inactive")) (org-agenda-span 'day)
1689                       (org-agenda-overriding-header "Day agenda")))
1690              (tags-todo "carryover/-INFO-WAITING"
1691                         ((org-agenda-overriding-header "Carry along list")
1692                          (org-agenda-files `(,(org-journal--get-entry-path))))))
1693             nil)))
1694  #+end_src
1695  
1696  ** Capturing information
1697  I guess 90% of the information I keep in the main orgmode files starts life originally as a captured item. I use it for:
1698  
1699  1. TODO items;
1700  2. BUY items;
1701  3. Journal entries;
1702  4. Logbook entries;
1703  5. Weblinks;
1704  6. Toots.
1705  
1706  #+begin_src emacs-lisp
1707    (use-package org-capture
1708      :diminish
1709  
1710      :custom
1711      ;; Do not do the automatic bookmarking when capturing
1712      (org-capture-bookmark nil)
1713      (bookmark-set-fringe-mark nil)
1714  
1715      ;; Special config for handling tags while capturing
1716      <<org-capture-taghandling>>
1717  
1718      :config
1719      ;; Configuration of all the capture templates
1720      <<org-capture-templates>>
1721      ;; and some shortcuts to run them
1722      <<org-capture-runners>>
1723  
1724      ;; Not a :custom, we're making this up as we go.
1725      (setq ocpf-frame-parameters
1726            '((name . "*capture*")
1727              (width . 115) (height . 15)
1728              (tool-bar-lines . 0) (menu-bar-lines . 0)
1729              (internal-border-width . 5)))
1730  
1731      (defun ocpf--capture-frame-p ()
1732        "Return t if current frame is the capture frame."
1733        (equal (cdr (assoc 'name ocpf-frame-parameters))
1734               (frame-parameter nil 'name)))
1735  
1736      (defun ocpf--funcall (func &rest args)
1737        "Call FUNC in our capture frame only."
1738        (when (ocpf--capture-frame-p)
1739          (apply func args)))
1740  
1741      (defun ocpf--delete-frame (&rest args)
1742        "Close capture frame."
1743        (ocpf--funcall 'delete-frame))
1744  
1745      (defun ocpf--delete-other-windows (&rest args)
1746        "Make sure we have the capture frame to ourselves."
1747        (ocpf--funcall 'delete-other-windows))
1748  
1749      (defun ocpf--window-system ()
1750        "Return appropriate window system symbol for current OS."
1751        (cond ((eq system-type 'darwin) 'ns)
1752              ((eq system-type 'gnu/linux) 'x)
1753              ((eq system-type 'windows-nt) 'w32)))
1754  
1755      (defun ocpf--org-capture (orig-fun &optional goto keys)
1756        "Create a new frame and run org-capture."
1757        (interactive)
1758        (let ((after-make-frame-functions
1759               (lambda (frame)
1760                 (select-frame frame)
1761                 (funcall orig-fun goto keys))))
1762          (make-frame
1763           `((window-system . ,(ocpf--window-system))
1764             ,@ocpf-frame-parameters))
1765          (setq header-line-format nil)))
1766  
1767      ;; All capture frames should have one window
1768      (add-hook 'org-capture-mode-hook 'delete-other-windows)
1769  
1770      ;; Wrap org-capture in our org-capture
1771      (advice-add 'org-capture :around #'ocpf--org-capture)
1772  
1773      ;; Proper clean up
1774      (advice-add 'org-capture-finalize :after #'ocpf--delete-frame))
1775  
1776  
1777  
1778  #+end_src
1779  
1780  Here are the templates used by org-capture. The /todo/ template is the most used, it is the same as the /link/ template, but does not include a reference to the current context, which is, in most cases, just annoying.
1781  
1782  #+name: org-capture-templates
1783  #+begin_src emacs-lisp :tangle no
1784    (setq
1785     org-capture-templates
1786     `(("b" "Buy"
1787        entry (function mrb/capture-location) "* BUY %?\n%(mrb/prop-or-nothing :annotation)\n" :prepend t :empty-lines 1)
1788       ("l" "Link"
1789        entry (function mrb/capture-location) "* TODO %?\n%(mrb/prop-or-nothing :annotation)\n" :prepend t :empty-lines 1)
1790       ("t" "Todo"
1791        entry (function mrb/capture-location) "* TODO %?\n" :prepend t :empty-lines 1)
1792       ("g" "Generic Log Entry"
1793        item  (here) "- %u %?")
1794       ("z" "Ziggo Log Entry"
1795        item  (file+olp
1796               (mrb/home-file "dat/org/GTD.org")
1797               "Partners" "Leveranciers" "Ziggo" "Logbook" ,(format-time-string "%Y"))
1798        "- %u %?")))
1799  
1800    (defun mrb/prop-or-nothing(prop)
1801      "Insert `[:type]: [:property]` when :property has a value, otherwise nothing.
1802    Meant to be used inside org-capture-templates
1803  
1804    Example: `%(mrb/prop-or-nothing :annotation)`"
1805  
1806      (let ((prop-value (plist-get org-store-link-plist prop))
1807            (type (plist-get org-store-link-plist :type)))
1808        (if (equal prop-value "")
1809            ""
1810          (concat (when type (concat type ": ")) prop-value))))
1811  
1812    (defun mrb/capture-location()
1813      (interactive)
1814      "This function is meant to be used inside org-capture-templates
1815    to find a file and location where a captured ITEM should
1816    be stored."
1817  
1818      ;; Open journal file without creating a journal entry This has the
1819      ;; side effect that it creates the file and moves TODO items over
1820      ;; on first call and leaves the cursor at the end of the file.
1821      (org-journal-new-entry 1)
1822  
1823      ;; Find the id in this file and go there for the capture
1824      (setq loc (org-find-entry-with-id "new-todo-receiver"))
1825      (when loc
1826        (goto-char loc)))
1827  #+end_src
1828  
1829  Define functions for each piece of information captured, so they can be easily bound to keys.
1830  
1831  #+name: org-capture-runners
1832  #+begin_src emacs-lisp :tangle no
1833    :init
1834    (defun mrb/capture-todo ()
1835      "Capture a TODO item"
1836      (interactive)
1837      (org-capture nil "t"))
1838  
1839    (defun mrb/capture-buy ()
1840      "Capture a BUY item"
1841      (interactive)
1842      (org-capture nil "b"))
1843  
1844    (defun mrb/capture-link ()
1845      "Capture a TODO item, but link to source when we can"
1846      (interactive)
1847      (org-capture nil "l"))
1848  #+end_src
1849  
1850  These capture functions are called from shell scripts in the operating system and have a shortcut key assigned to them. The scripts are produced directly from this document, in a similar way as the main edit script was produced in [[Preparing for lift-off]]
1851  
1852  #+begin_src sh :exports code :tangle ~/bin/capture-todo.sh :shebang #!/bin/sh
1853    edit-noframe --eval '(mrb/capture-todo)'
1854  #+end_src
1855  
1856  #+begin_src sh :exports code :tangle ~/bin/capture-buy.sh :shebang #!/bin/sh
1857    edit-noframe --eval '(mrb/capture-buy)'
1858  #+end_src
1859  
1860  By default =C-c C-c= ends the capture, but is normally the shortcut to enter tags, so I define a shortcut to define tags while capturing.
1861  
1862  #+name: org-capture-taghandling
1863  #+begin_src emacs-lisp :tangle no
1864    :bind ( :map org-capture-mode-map
1865            ("C-c C-t" . mrb/add-tags-in-capture))
1866  
1867    :init
1868    (defun mrb/add-tags-in-capture()
1869      (interactive)
1870      "Insert tags in a capture window without losing the point"
1871      (save-excursion
1872        (org-back-to-heading)
1873        (org-set-tags-command)))
1874  #+end_src
1875  
1876  ** Workflow
1877  Orgmode used a couple of thing which enable you to steer the workflow for items. Item states are the most prominent ones. Org-mode uses keyword definitions to denote states on items. I keep an [[file:org-config.org][Orgmode configuration file]] (=org-config.org)= file which contains the description of the workflow in a format suitable to include directly into orgmode files. The configuration of emacs itself is limited to dressing up this configuration with things less suitable to go into that config file. The configuration here and the org config file should be kept in sync.
1878  
1879  Adapt the colors of the states I use a bit:
1880  
1881  #+name: orgmode-keywords
1882  #+begin_src emacs-lisp :tangle no
1883    ;; Define face specs for our keywords, so they can be used in the
1884    ;; theme adjustment like standard faces
1885    (defface mrb/org-todo-keyword-TODO      nil "")
1886    (defface mrb/org-todo-keyword-DONE      nil "")
1887    (defface mrb/org-todo-keyword-WAITING   nil "")
1888    (defface mrb/org-todo-keyword-CANCELLED nil "")
1889    (defface mrb/org-todo-keyword-BUY       nil "")
1890    (defface mrb/org-todo-keyword-HOWTO     nil "")
1891    (defface mrb/org-todo-keyword-INFO      nil "")
1892    (defface mrb/org-todo-keyword-COLLECT   nil "")
1893    (defface mrb/org-todo-keyword-SOLVE     nil "")
1894    (defface mrb/org-todo-keyword-READ      nil "")
1895    (defface mrb/org-todo-keyword-READING   nil "")
1896    (defface mrb/org-todo-keyword-PLAN      nil "")
1897  
1898  
1899    (setq org-todo-keyword-faces `(
1900                                   ("TODO"      . mrb/org-todo-keyword-TODO)
1901                                   ("DONE"      . mrb/org-todo-keyword-DONE)
1902                                   ("WAITING"   . mrb/org-todo-keyword-WAITING)
1903                                   ("CANCELLED" . mrb/org-todo-keyword-CANCELLED)
1904                                   ("BUY"       . mrb/org-todo-keyword-BUY)
1905                                   ("HOWTO"     . mrb/org-todo-keyword-HOWTO)
1906                                   ("INFO"      . mrb/org-todo-keyword-INFO)
1907                                   ("COLLECT"   . mrb/org-todo-keyword-COLLECT)
1908                                   ("SOLVE"     . mrb/org-todo-keyword-SOLVE)
1909                                   ("READ"      . mrb/org-todo-keyword-READ)
1910                                   ("READING"   . mrb/org-todo-keyword-READING)
1911                                   ("PLAN"      . mrb/org-todo-keyword-PLAN)))
1912  #+end_src
1913  
1914  Make sure we keep a clean tag slate when changing tag state. This means that when I move to an active state, remove inactive tags; if something is DONE, remove tags from it and automatically adding a 'buy' tag when a BUY item is created.  Note: capturing does not honor this, i.e. when creating a new item.
1915  
1916  #+begin_src emacs-lisp
1917    (setq org-todo-state-tags-triggers
1918          '(("TODO" ("inactive"))          ; remove inactive tags if moved to any active state
1919            ("DONE" ("inactive") ("fork")) ; remove tags from any inactive state
1920            ("BUY"  ("buy" . t))))         ; add buy tag when this is a buying action
1921  #+end_src
1922  
1923  To keep the TODO list clean we archive completed entries. The archiving only occurs when an item is in the 'DONE' or 'CANCELLED' state and is not marked as a habit. A guard prevents archiving incomplete TODO items.
1924  
1925  #+begin_src emacs-lisp
1926    ;; I need a modified version of org-is-habit, which takes inheritance
1927    ;; in to account
1928    (defun mrb/org-is-habit-p (&optional pom)
1929      "org-is-habit-p taking property inheritance into account"
1930      (string-equal "habit" (org-with-point-at (or pom (point))
1931                              (org-entry-get-with-inheritance "STYLE"))))
1932  
1933    (setq mrb/archivable-todo-states '("DONE" "CANCELLED" "INFO"))
1934  
1935    (defun mrb/can-archive-p (archivable-states)
1936      "Check if current heading can be archived.
1937  ARCHIVABLE-STATES is a list of TODO states from which archiving is allowed."
1938      (let ((todo-state (org-get-todo-state)))
1939        (member todo-state archivable-states)))
1940  
1941    ;; Guard to org-archive-subtree to prevent archiving incomplete items
1942    (advice-add 'org-archive-subtree :before
1943                (lambda (&rest _args)
1944                  (unless (mrb/can-archive-p mrb/archivable-todo-states)
1945                    (let ((state (org-get-todo-state))
1946                          (allowed-states (string-join mrb/archivable-todo-states ", ")))
1947                      (user-error "Cannot archive: item is in %s state (must be %s)" state allowed-states)))))
1948  #+end_src
1949  
1950  In addition to the above I'm experimenting with =org-edna=  (Extensible Dependencies 'N' Actions) which has a bit more flexibility. Notably the blocking of tasks is what I want to peruse a bit, because the builtin system hasn't worked for me. I may still use the builtin way to block items, but perhaps under the dynamic control of org-edna instead of manually specifying =order= properties etc.
1951  
1952  For now, a basic config and enabling of =org-edna=
1953  
1954  #+begin_src emacs-lisp
1955    (use-package org-edna
1956      :diminish
1957      :after org
1958      :config
1959      (org-edna-mode))
1960  #+end_src
1961  
1962  ** Marking items as DONE
1963  Marking work as completed should be a smooth process to stop getting in the way of doing the actual work. A shortcut is defined to mark items done in the standard way and have an additional shortcut to mark it done should it be blocked.
1964  
1965  When an item changes to the DONE state, a question is asked if the item should be archived, to which the normal answer should be 'Yes' to keep the active file as clean as possible.
1966  
1967  #+name: orgmode-itemactions
1968  #+begin_src emacs-lisp :tangle no
1969    ;; Macro to consolidate duplicated org-todo command logic
1970    (defmacro mrb/make-org-todo-command (name base-fn &optional state)
1971      "Create an interactive TODO command that toggles state with org-inhibit-blocking.
1972  NAME is the function name to create, BASE-FN is org-todo or org-agenda-todo,
1973  STATE is an optional target state like \"CANCELLED\"."
1974      `(defun ,name ()
1975         ,(format "Toggle org TODO%s." (if state (format " to %s" state) ""))
1976         (interactive)
1977         (let ((org-inhibit-blocking t))
1978           (funcall ',base-fn ,@(if state `(,state) nil)))))
1979  
1980    ;; Create the four todo command variants
1981    (mrb/make-org-todo-command mrb/force-org-todo org-todo)
1982    (mrb/make-org-todo-command mrb/force-org-agenda-todo org-agenda-todo)
1983    (mrb/make-org-todo-command mrb/org-cancel-todo org-todo "CANCELLED")
1984    (mrb/make-org-todo-command mrb/org-agenda-cancel-todo org-agenda-todo "CANCELLED")
1985  #+end_src
1986  
1987  The above may be influenced, or even made redundant, by the =org-edna= package configuration.
1988  ** Registering creation time of todo items
1989  Over time it gets a bit messy in my orgmode files. I can not remember when something was created and thus, by judging the time I didn't do anything with the item, decide if it is still important or not.
1990  
1991  So, to help with that I created a little glue to make sure each actionable item gets a =CREATED= property with the date in it on which that item was created. I use the contributed =org-expiry= for that and adjust it a bit.
1992  
1993  I want the property to be name 'CREATED' (I don't remember what the org-expiry default name is, but it is different) and the timestamps inserted must not be active, otherwise they'll appear all over the place in the agenda.
1994  
1995  #+begin_src emacs-lisp
1996    (use-package org-contrib)
1997    (use-package org-expiry
1998      :custom
1999      (org-expiry-created-property-name "CREATED")
2000      (org-expiry-inactive-timestamps  t)
2001  
2002      :config
2003      <<org-expiry-createdtimestampfunction>>
2004  
2005      )
2006  #+end_src
2007  
2008  So, to create the timestamp I need a little helper function which actually inserts it, using org-expiry. There is some additional cursor munging to make sure it is used comfortably during editing.
2009  To actually make this active we advice the operations that insert headers: =org-insert-todo-heading= and =org-capture= when we are actually capturing a TODO item.
2010  
2011  #+name: org-expiry-createdtimestampfunction
2012  #+begin_src emacs-lisp :tangle no
2013    (defun mrb/insert-created-timestamp(&rest args)
2014      "Insert a CREATED property using org-expiry.el for TODO entries"
2015      (org-expiry-insert-created)
2016      (org-back-to-heading)
2017      (org-end-of-line))
2018  
2019    ;; for insert todo heading, always add the CREATED property
2020    (advice-add 'org-insert-todo-heading
2021                :after #'mrb/insert-created-timestamp)
2022  
2023    ;; for capturing, only when state is in the TODO keywords list active in buffer
2024    (advice-add 'org-capture
2025                :after (lambda (&rest r)
2026                         (when (member (org-get-todo-state) org-todo-keywords-1)
2027                           (mrb/insert-created-timestamp))))
2028  #+end_src
2029  
2030  Related to the above, with some regularity I want to record timestamps, for example for documenting longer during tasks and recording incremental progress or information on them. Orgmode provides the binding '=C-c !=' for this which inserts an inactive date-stamp, optionally including the time as well.
2031  
2032  Lastly, there's a mechanism in emacs which can automatically insert time-stamps, based on a placeholder in the file and a format variable. I sometimes use that, so I add it to my before save hook. Typically, the format string is file specific and will be held in a file local variable.
2033  
2034  #+begin_src emacs-lisp
2035    (add-hook 'before-save-hook 'time-stamp)
2036  #+end_src
2037  
2038  ** Scheduling items
2039  Orgmode has a number of provisions to schedule items, either explicitly by setting the SCHEDULE property, inferring a deadline by setting the DEADLINE property, thus scheduling the task in an interval before the deadline expires.
2040  
2041  The main key for scheduling will be =C-.= (Control key with dot). A todo is marked to the next state with =M-.= so this makes sense, at least to me. In plain org and in org-agenda mode this key is used most often, but I expect this to be useful in other modes as well. I will try to u use the same keybinding in those modes as well.
2042  
2043  I had /schedule-for-today/ functions earlier, but those really just save me from pressing return in a normal =org-schedule= function, so their added value was minimal and I have since deleted them.
2044  
2045  ** Visual settings
2046  Having an attractive screen to look at becomes more important if you use the system all day long. /Attractive/ is rather subjective here. For me it mainly consists of functional things. Anyways, this section groups settings for the visual characteristics of orgmode.
2047  
2048  I want to hide the leading stars in the out-liner, and do it *exactly* in the background color. This is redundant actually in my case, as it is also specified in the org config file that I include. Or rather, it is redundant there, because I want it always to be the case.
2049  
2050  #+begin_src emacs-lisp
2051    (setq org-hide-leading-stars t)
2052  #+end_src
2053  
2054  For the collapsed items in the outline orgmode uses the variable =org-ellipsis= to determine what character-sequence should be used to show that the item can be expanded. The variable can contain a string, which will then be used instead of the standard 3 dots, or a face which will then be used to render the standard 3 dots.
2055  
2056  #+begin_src emacs-lisp
2057    ;; Use larger ellipsis symbol
2058    (setq org-ellipsis "⋯")
2059  #+end_src
2060  
2061  There are a couple of ways within org to emphasize text inline for *bold*, /italics/, _underlined_ etc. These are set in the text by enclosing regions with delimiters. I do not want to see these delimiters, but rather render the text. The org-appear package makes this even fancier by showing the characters when inside the marked area, so editing is a lot easier.
2062  
2063  #+begin_src emacs-lisp
2064    (use-package org-appear
2065      :after org
2066      :hook (org-mode . org-appear-mode)
2067      :custom
2068      (org-hide-emphasis-markers t)    ; Is this used by org-appear mode?
2069      )
2070  #+end_src
2071  
2072  Similar to inline emphasis is the /rewriting/ with pretty entity characters (like '\delta' for example). These characters can be added to the text by adding a '\' before a symbol name ('delta' in the example).  I make an exception for the sub- and superscript characters. This happens a lot in variable names etc. and I a big annoyance if those get rendered to subscript all the time.
2073  
2074  #+begin_src emacs-lisp
2075    (setq org-pretty-entities 1)
2076    (setq org-pretty-entities-include-sub-superscripts nil)
2077  #+end_src
2078  
2079  Related to that is the display of links. I want them to be explicit most of the time to avoid confusion, but the 'fancy' display is easier at first:
2080  
2081  #+begin_src emacs-lisp
2082    (setq org-link-descriptive t)
2083  #+end_src
2084  
2085  For most of the source blocks I want Emacs to render those blocks in their native mode. This had a serious performance problem in the past, but I think it has been solved recently.
2086  
2087  #+begin_src emacs-lisp
2088    (setq org-src-fontify-natively t)
2089  #+end_src
2090  
2091  For the headings at each level a =*= is normally used. As we're in unicode world now, we can do a bit better.
2092  
2093  #+begin_src emacs-lisp
2094    (use-package org-bullets
2095      :hook (org-mode . (lambda () (org-bullets-mode 1))))
2096  #+end_src
2097  
2098  The item lists can be made a whole lot more attractive by attaching some icons based on the category an items belongs to. The category assignment itself is done by setting the =CATEGORY= property explicitly on the item or on the file.
2099  
2100  #+name: org-agenda-visuals
2101  #+begin_src emacs-lisp :tangle no
2102    (setq org-agenda-category-icon-alist
2103          `(
2104            ("Afspraak"      ,(mrb/org-file "images/stock_new-meeting.png") nil nil :ascent center)
2105            ("Blogging"      ,(mrb/org-file "images/edit.png") nil nil :ascent center)
2106            ("Car"           ,(mrb/org-file "images/car.png") nil nil :ascent center)
2107            ("Cobra"         ,(mrb/org-file "images/car.png") nil nil :ascent center)
2108            ("DVD"           ,(mrb/org-file "images/media-cdrom.png") nil nil :ascent center)
2109            ("Emacs"         ,(mrb/org-file "images/emacs.png") nil nil :ascent center)
2110            ("Finance"       ,(mrb/org-file "images/finance.png") nil nil :ascent center)
2111            ("Habitat"       ,(mrb/org-file "images/house.png") nil nil :ascent center)
2112            ("Habit"         ,(mrb/org-file "images/stock_task-recurring.png") nil nil :ascent center)
2113            ("Hobbies"       ,(mrb/org-file "images/hobbies.png") nil nil :ascent center)
2114            ("Partners"      ,(mrb/org-file "images/partners.png") nil nil :ascent center)
2115            ("Personal"      ,(mrb/org-file "images/personal.png") nil nil :ascent center)
2116            ("Task"          ,(mrb/org-file "images/stock_todo.png") nil nil :ascent center)
2117            ("Org"           ,(mrb/org-file "images/org-mode-unicorn.png") nil nil :ascent center)
2118            ("Systeem"       ,(mrb/org-file "images/systeembeheer.png") nil nil :ascent center)
2119            ("Wordpress"     ,(mrb/org-file "images/wordpress.png") nil nil :ascent center)
2120            ))
2121  #+end_src
2122  
2123  Showing items in the agenda views reacts to a number of settings. In my setup I want blocked tasks hidden, that is the reason for blocking. Hide tasks which are DONE already and a deadline is coming up, no use showing those; the same goes for tasks which are DONE and are scheduled. In short, anything that does not need my attention needs to be hidden.
2124  
2125  #+begin_src emacs-lisp
2126    (setq
2127     org-agenda-dim-blocked-tasks t
2128     org-agenda-skip-deadline-if-done t
2129     org-agenda-skip-scheduled-if-done t
2130     org-agenda-skip-archived-trees nil
2131     )
2132  #+end_src
2133  
2134  After experimenting with automatic aligning of tags I've come to the conclusion I want to have it manual. For now, I set the tags to align flush right with column 110 but ideally I'd want something more dynamic (in the sense that visual-line mode is also dynamic)
2135  
2136  #+begin_src emacs-lisp
2137    (setq org-tags-column -95)
2138    (defun mrb/org-align-all-tags ()
2139      "Wrap org-align-tags to be interactive and apply to all"
2140      (interactive)
2141      (org-align-tags t))
2142    (bind-key "M-\"" 'mrb/org-align-all-tags)
2143  #+end_src
2144  ** Agenda customization
2145  Settings which are just applicable for the org-mode agenda view.
2146  
2147  #+begin_src emacs-lisp
2148    ;; Show properties in agenda view
2149    (use-package org-agenda-property
2150      :custom
2151      (org-agenda-property-list '("LOCATION" "Responsible")))
2152  
2153    ;; When done rendering, go to the top
2154    (add-hook 'org-agenda-finalize-hook #'(lambda () (goto-char (point-min))) 100)
2155  #+end_src
2156  
2157  A snippet which resized the agenda to its window whenever its rebuilt by orgmode The 'auto value of the =org-agenda-tags-column= variable right aligns the tags in the agenda view
2158  
2159  #+begin_src emacs-lisp
2160    (setq org-agenda-tags-column 'auto)
2161    (defun mrb/fit-agenda-window (&rest _)
2162      "Fit the Org Agenda to its buffer."
2163      (fit-window-to-buffer)
2164      (goto-char (point-min)))
2165    (advice-add 'org-agenda-redo :after #'mrb/fit-agenda-window)
2166  #+end_src
2167  *** Calendaring
2168  Traditionally somewhat related to mail, although mostly through orgmode these days, I want a simple setup to pull in some calendaring information. I run a radicale calendar server somewhere, but having the same info in orgmode makes sense for me.
2169  
2170  I have used =org-caldav= but even in just a read only setup it does not work properly. Now rocking a setup which uses =vdirsyncer= to pull in calendaring information and =khal= and =khalel= to use that information from the command line and integrated with org-mode in Emacs.
2171  
2172  Set up vdirsyncer and khal first; the khalel config in Emacs is than rather straightforward
2173  
2174  #+begin_src elisp
2175    ;; `'khalel-import-events`' fills calendar.org, by default 1 Month in the past/future from today
2176    (use-package khalel
2177      :after org
2178      :custom
2179      (khalel-khal-command "khal")
2180      (khalel-vdirsyncer-command "vdirsyncer")
2181      ;; khalel-import-org-file is set in personal.el
2182      (khalel-import-org-file-confirm-overwrite nil)
2183      (khalel-import-start-date "-30d")
2184      (khalel-import-end-date "+30d")
2185      (khalel-import-format "* {title} {cancelled} \n:PROPERTIES:\n:CALENDAR: {calendar}\n:LOCATION: {location}\n:ID: {uid}\n:END:\n- When: <{start-date-long} {start-time}>--<{end-date-long} {end-time}>\n- Where: {location}\n- Description: {description}\n- URL: {url}\n- Organizer: {organizer}\n\n[[elisp:(khalel-edit-calendar-event)][Edit this event]]    [[elisp:(progn (khalel-run-vdirsyncer) (khalel-import-events))][Sync and update all]]\n")
2186      :config
2187      (khalel-add-capture-template))
2188    #+end_src
2189  
2190  This creates a =calendar.org= file with events from the last and next month. Adding that file to =org-agenda-files= makes sure the events show up in orgmode agenda related views.
2191  
2192  There exists a vdirsyncer timer which syncs the events with the CalDAV server to a folder, filling it with *.ics files. The command =khalel-import-events= filters those ics files into an orgmode file.
2193  
2194  All events from all calendars are put into one file for now. I plan to separate the events from different calendars into different files and have tasks sync as well.
2195  
2196  ** Babel / Literate programming
2197  Specific settings for babel and literate programming within org-mode
2198  
2199  #+begin_src emacs-lisp
2200    (setq org-babel-load-languages
2201          '((awk        . t)
2202            (calc       . t)
2203            (css        . t)
2204            (ditaa      . t)
2205            (emacs-lisp . t)
2206            (gnuplot    . t)
2207            (haskell    . t)
2208            (js         . t)
2209            (lisp       . t)
2210            (org        . t)
2211            (plantuml   . t)
2212            (python     . t)
2213            (scheme     . t)
2214            (shell      . t)
2215            (sql        . t)))
2216  
2217    ;; Activate Babel languages
2218    (org-babel-do-load-languages 'org-babel-load-languages org-babel-load-languages)
2219  #+end_src
2220  
2221  The elements of org babel are blocks of source code. Some of the modes for sources have some helper programs.
2222  
2223  Plantuml transforms diagram specifications with a /mini programming language/ into diagrams. It has support for many types of diagram. Ditaa transforms ascii diagrams into graphics.
2224  
2225  #+begin_src emacs-lisp
2226    (use-package plantuml-mode
2227      :after org    ; strictly not needed, but i use it mainly from org
2228      :init
2229      (setq plantuml-jar-path "/usr/share/java/plantuml/plantuml.jar")
2230      (setq org-plantuml-jar-path plantuml-jar-path)
2231      (setq plantuml-default-exec-mode 'jar))
2232  
2233    (setq org-ditaa-jar-path    "/usr/share/java/ditaa/ditaa-0.11.jar")
2234  #+end_src
2235  
2236  ** Refiling
2237  A big part of organizing information and task is shuffling things around. The 'thing' to throw around is a heading and 'refiling' is the term org-mode uses for throwing.
2238  
2239  When filing, or capturing we want the items at the bottom of what we are filing it into. The main reason for this is that a large part of the sections that contain items are ordered. Should we file the item at the top, in many cases that would mean it is the most imminent thing to do, which is not the case. For some files, we *do* want the items on top though
2240  
2241  #+begin_src emacs-lisp
2242    (setq org-reverse-note-order '(("workshop.org" . t)
2243                                   (".*" . nil))    ; File for others at the bottom of an entry
2244          org-refile-allow-creating-parent-nodes 'confirm
2245          org-refile-use-outline-path 'file
2246          org-outline-path-complete-in-steps nil
2247          org-refile-use-cache t)
2248  #+end_src
2249  
2250  The base list of files I want to be able to refile to are:
2251  1. The /active/ file list, i.e. everything in =org-agenda-files=, regardless of these files are currently opened or not;
2252  2. All open orgmode based files which, surprisingly, I need to generate myself?
2253  
2254  #+begin_src emacs-lisp
2255    (setq org-refile-targets '((org-agenda-files :maxlevel . 10)))
2256  
2257    ;; Borrows from https://yiming.dev/blog/2018/03/02/my-org-refile-workflow/
2258    (defun mrb/org-opened-buffers ()
2259      "Return the list of org files opened in emacs"
2260  
2261      (defun buffer-mode (buf)
2262        (buffer-local-value 'major-mode (get-buffer buf)))
2263  
2264      (delq nil
2265            (mapcar (lambda (x)
2266                      (if (and (buffer-file-name x) ;-buffer has a file?
2267                               (provided-mode-derived-p
2268                                (buffer-mode x) 'org-mode))
2269                          (buffer-file-name x)))
2270                    (buffer-list))))
2271  
2272    ;; Adjust refile target to include all opened org files
2273    ;; Is it bad that we have duplicates here?
2274    (add-to-list 'org-refile-targets
2275                 '(mrb/org-opened-buffers :maxlevel . 10))
2276  
2277  
2278  #+end_src
2279  
2280  The type of headers to refile to is, in the default orgmode config, basically everything. I limit it above to a maximum depth of 10, but then still. By using =org-refile-target-verify-function= we can fine-tune the decision whether to use a header as target or not.  The following conditions have been implemented:
2281  
2282  1. The header must /not/ be a DONE item;
2283  2. The header needs to have children.
2284  
2285  The latter is the most important one and will prevent creating 'gathering' items to tasks themselves.
2286  
2287  #+begin_src emacs-lisp
2288    (defun mrb/has-DONE-keyword()
2289      "Return t when the heading at point has a `DONE' like keyword"
2290      (member (nth 2 (org-heading-components)) org-done-keywords))
2291  
2292    (defun mrb/verify-refile-target()
2293      "Decide if a target header can be used as a refile target
2294    Conditions to return t:
2295    - header must not have one of the DONE keywords
2296    - it must be a parent of something already"
2297      ;; interactive during testing
2298      (interactive)
2299  
2300      (and
2301                                            ; exclude done keyword headers
2302       (not (mrb/has-DONE-keyword))
2303                                            ; must have a child
2304       (save-excursion (org-goto-first-child))))
2305  
2306  
2307    (setq org-refile-target-verify-function 'mrb/verify-refile-target)
2308  #+end_src
2309  ** Exporting to other formats
2310  Orgmode can export to a variety of formats, I mainly use LaTeX (PDF) and HTML as destination format.
2311  
2312  #+begin_src emacs-lisp
2313    (use-package htmlize)                   ; for html export source highlighting
2314    ;; {% raw %}
2315    (setq org-latex-title-command " "
2316  
2317          org-html-htmlize-output-type 'css
2318  
2319          org-latex-src-block-backend 'engraved
2320  
2321          org-export-copy-to-kill-ring 'if-interactive
2322  
2323          org-latex-classes
2324          '(
2325            ("article" "\\documentclass[11pt,a4paper,twoside]{article}"
2326             ("\\section{%s}" . "\\section*{%s}")
2327             ("\\subsection{%s}" . "\\subsection*{%s}")
2328             ("\\subsubsection{%s}" . "\\subsubsection*{%s}")
2329             ("\\paragraph{%s}" . "\\paragraph*{%s}")
2330             ("\\subparagraph{%s}" . "\\subparagraph*{%s}"))
2331            ("report" "\\documentclass[11pt]{report}"
2332             ("\\part{%s}" . "\\part*{%s}")
2333             ("\\chapter{%s}" . "\\chapter*{%s}")
2334             ("\\section{%s}" . "\\section*{%s}")
2335             ("\\subsection{%s}" . "\\subsection*{%s}")
2336             ("\\subsubsection{%s}" . "\\subsubsection*{%s}"))
2337            ("book" "\\documentclass[11pt]{book}"
2338             ("\\part{%s}" . "\\part*{%s}")
2339             ("\\chapter{%s}" . "\\chapter*{%s}")
2340             ("\\section{%s}" . "\\section*{%s}")
2341             ("\\subsection{%s}" . "\\subsection*{%s}")
2342             ("\\subsubsection{%s}" . "\\subsubsection*{%s}"))
2343            ("beamer" "\\documentclass{beamer}" org-beamer-sectioning))
2344  
2345          ;; No tags, todo keywords or time-stamp
2346          org-export-with-tags nil
2347          org-export-with-todo-keywords nil
2348          org-export-time-stamp-file nil
2349          org-export-backends '(ascii html icalendar latex md odt org texinfo)
2350          )
2351    ;; {% endraw %}
2352  #+end_src
2353  
2354  ** Journaling
2355  While the standard capture method is useful I found myself not using it very much. Not sure why. It turns out that org-journal is a much better fit for my workflow, especially using the carry-over functionality.
2356  
2357  #+begin_src emacs-lisp
2358    (use-package org-journal
2359      :after (org)
2360      :commands (org-journal-new-entry)
2361      :hook ((org-journal-mode . turn-on-visual-line-mode)
2362             (org-journal-mode . turn-on-visual-fill-column-mode))
2363      :bind (("C-c j"   . org-journal-new-entry)
2364             ("C-s-j"   . mrb/open-current-journal))
2365  
2366      :config
2367      (defun mrb/open-current-journal()
2368        "Open current journal file, applying startup folding on first visit only."
2369        (interactive)
2370        (let* ((org-journal-file (org-journal--get-entry-path))
2371               (buf-exists (find-buffer-visiting org-journal-file)))
2372          (if buf-exists
2373              (switch-to-buffer buf-exists)
2374            ;; New entry: create buffer and apply startup folding after creation
2375            (org-journal-new-entry 1)
2376            (org-cycle-set-startup-visibility))))
2377  
2378      :custom
2379      (org-journal-dir (mrb/org-file "journal/"))
2380  
2381      ;; Bring our config to every journal file
2382      (org-journal-file-header (concat "#+SETUPFILE: " user-emacs-directory "org-config.org"))
2383  
2384      ;; Match the journal files (TODO  make this independent of earlier assignments)
2385      (org-agenda-file-regexp "^[0-9]+\\.org")
2386      (org-journal-file-format  "%Y%m%d.org")
2387  
2388      ;; Put day on top of file, uses `format-time-string`
2389      (org-journal-date-format "%A, %-e-%m-%Y")
2390  
2391      ;; Put day on top of file, uses `org-journal-date-format`
2392      (org-journal-date-prefix "#+TITLE: ")
2393      ;; New entries go at the bottom, make sure we are at top level
2394      (org-journal-time-format "[%R] ")
2395      (org-journal-time-prefix "* ")
2396  
2397      ;; Carry over TODO items and items explicitly marked
2398      (org-journal-carryover-items "+carryover|+TODO=\"TODO\"")
2399      ;; and for this to work, we need agenda integration
2400      (org-journal-enable-agenda-integration t)
2401      ;; Remove empty journals after carryover
2402      (org-journal-carryover-delete-empty-journal 'always)
2403  
2404      ;; I want to have encryption, but how do TODO items bubble up then in the agenda
2405      (org-journal-enable-encryption nil)
2406      (org-crypt-disable-auto-save t)
2407      (org-journal-enable-cache t)
2408      (org-element-use-cache t))
2409  #+end_src
2410  
2411  Particularly in journaling, but also in org-mode in general, I want to be able to quickly insert screenshots. Rather, images in general but 90% of my use-case is really screenshots.
2412  
2413  There's a package =org-attach-screenshot= which matches my use-case 99% so let's use that and worry about extending it to images later on.
2414  
2415  #+begin_src emacs-lisp
2416    (use-package org-attach-screenshot
2417      :bind (("C-c i" . org-attach-screenshot)))
2418  #+end_src
2419  
2420  The 1% I was referring to above is that the original package exclusively supports org-mode. I patched it, which was trivial to support org-mode and all its derived modes. (org-journal in my case)
2421  
2422  ** Publishing
2423  There's plenty ways to publish orgmode content on the web. I use a couple of them sparingly. For most of them I don't need a permanent configuration. For the ones that I do need a config, there's this section.
2424  
2425  *** Writefreely
2426  Writefreely is a Write.as derivative and published as open source. This allows me to just kick out a quick orgmode file with a =#+TITLE:= and a =#+DATE:= header and throw it on a blog like site rather quickly.
2427  
2428  #+begin_src emacs-lisp
2429    ;; Still needed:
2430    ;; - post as draft by default, this prevents automatic posting by accident for federated collections
2431    ;; - save augment -> publish / update automatically, probably a custom hook on minor mode?
2432    ;; - wf: have autorefresh GET parameter which monitors updates and
2433    ;;       refreshes automatically
2434    ;; - set local save dir for the files
2435    ;; Make sure the variables of THIS file are loaded before THIS file is loaded
2436    (use-package writefreely
2437      :after org
2438      :custom
2439      ;; writefreely-instance-url and writefreely-instance-api-endpoint are set in personal.el
2440      (writefreely-maybe-publish-created-date t)
2441      ;; Auth token requires yubikey via password-store; fetch on publish, not at startup
2442      (writefreely-auth-token nil))
2443  #+end_src
2444  
2445  *** Hugo
2446  I use hugo to sporadically publish some content to my blog. Not sure if this is what I want, I post too little to spend much time on it. But here is a small config for the main posts:
2447  
2448  #+begin_src emacs-lisp
2449    (use-package easy-hugo
2450      :init
2451      (setq ;; easy-hugo-basedir and easy-hugo-url are set in personal.el
2452            easy-hugo-postdir "content/post/main"
2453            easy-hugo-default-ext ".org"
2454            easy-hugo-org-header t))
2455  #+end_src
2456  
2457  ** Encrypting information in org-mode
2458  I use the /encrypt/ tag for encrypting sections in org-mode (and sometimes my journal). The sections get encrypted and decrypted automatically on saving and opening. This uses the EasyPG library to get to my GPG key.
2459  
2460  #+begin_src emacs-lisp
2461    (use-package org-crypt
2462      :custom
2463      (org-crypt-tag-matcher "encrypt")
2464      (org-crypt-key user-gpg-key)
2465  
2466      :config
2467      (org-crypt-use-before-save-magic))
2468  #+end_src
2469  
2470  We do not want to inherit this tag automatically, as its behavior is already subsection inclusive. When you encrypt a section, everything below it is considered content of that section and gets encrypted. I also add the value "crypt" as that is the org default, so it won't be inherited by mistake.
2471  
2472  #+begin_src emacs-lisp
2473    (add-to-list 'org-tags-exclude-from-inheritance '"encrypt")
2474    (add-to-list 'org-tags-exclude-from-inheritance '"crypt")
2475    (add-to-list 'org-tags-exclude-from-inheritance '"area")
2476  #+end_src
2477  
2478  ** Dynamic behaviour
2479  Not sure where to put this configuration in general. The problem I'm trying to solve is to keep only relevant files in org-agenda. Too much and the performance collapses, too little and nothing can be found. I'm trying =org-mem= for this, and perhaps add =org-node= later on.
2480  
2481  =org-mem= creates a cache of metadata about org-mode files. =org-node= uses this as a library and is similar to =org-roam=, which many people tout as a game changer in todo management, so I think it is worth to dedicate some time to it.
2482  
2483  First step is to have org-agenda-files managed automatically based on the cache, filtered for relevance. It does not add much to my current setup, but has the advantage to have this cache available for other purposes (finding things much/much faster is the main thing).
2484  
2485  #+begin_src emacs-lisp
2486    (use-package org-mem
2487      :after org
2488      :custom
2489      ;; Basically list all folders which have org files in them.
2490      (org-mem-watch-dirs `(,org-directory "~/dat/blogs" "~/dat/caldata"))
2491      (org-mem-do-sync-with-org-id t) ;; not sure what this does exactly
2492      :config
2493      ;; Include in agenda-files:
2494      ;; - non archive named files
2495      ;; - not my org config file
2496      ;; - files with active timestamps
2497      ;; - having entries in /todo/ states
2498      ;; - having entries scheduled/deadlined
2499      (defun mrb/agenda-exclude-file-p (file)
2500        "Return t if FILE should be excluded from agenda."
2501        (or (string-search "archive" file)
2502            (string-search "org-config.org" file)))
2503  
2504      (defun mrb/agenda-relevant-entry-p (entry)
2505        "Return t if ENTRY has properties that make it relevant for the agenda."
2506        (or (org-mem-entry-active-timestamps entry)
2507            (org-mem-entry-todo-state entry)
2508            (org-mem-entry-closed entry)
2509            (org-mem-entry-scheduled entry)
2510            (org-mem-entry-deadline entry)))
2511  
2512      (defun mrb/set-agenda-files (&rest _)
2513        "Update `org-agenda-files' to include only files with relevant entries."
2514        (setq org-agenda-files
2515              (cl-loop for file in (org-mem-all-files)
2516                       unless (mrb/agenda-exclude-file-p file)
2517                       when (seq-find #'mrb/agenda-relevant-entry-p
2518                                      (org-mem-entries-in file))
2519                       collect file)))
2520      (add-hook 'org-mem-post-full-scan-functions #'mrb/set-agenda-files)
2521      (org-mem-updater-mode))
2522  
2523    (use-package vulpea-journal
2524      :custom
2525      (vulpea-journal-default-template
2526       '(:file-name "vulpea-journal/%Y-%m-%d.org"
2527         :title "%Y-%m-%d %A"
2528         :tags ("journal")
2529         :head "#+created: %<[%Y-%m-%d]>")))
2530  #+end_src
2531  
2532  ** Old configuration
2533  Below is what was contained in the old configuration. I will slowly migrate this into more literal sections
2534  
2535  #+begin_src emacs-lisp
2536    ;; Bit of a leftover from reorganizing bits, do this later
2537    (add-to-list 'org-tags-exclude-from-inheritance '"sell")
2538  
2539    (defun mrb/has-todo-subtask-p ()
2540      "Return t if current heading has at least one TODO subtask."
2541      (let ((subtree-end (save-excursion (org-end-of-subtree t))))
2542        (save-excursion
2543          (forward-line 1)
2544          (catch 'found
2545            (while (and (< (point) subtree-end)
2546                        (re-search-forward "^\*+ " subtree-end t))
2547              (when (member (org-get-todo-state) org-todo-keywords-1)
2548                (throw 'found t)))))))
2549  
2550    (defun mrb/is-project-p ()
2551      "Return t if the current entry is a project.
2552  A project is a heading with a TODO keyword and at least one TODO subtask."
2553      (and (member (nth 2 (org-heading-components)) org-todo-keywords-1)
2554           (mrb/has-todo-subtask-p)))
2555  
2556    ;; Generic skip helpers to reduce duplication
2557    (defun mrb/skip-unless (predicate)
2558      "Skip subtree unless PREDICATE returns true.
2559  PREDICATE should be a function with no arguments that returns t or nil."
2560      (let ((subtree-end (save-excursion (org-end-of-subtree t))))
2561        (unless (funcall predicate) subtree-end)))
2562  
2563    (defun mrb/skip-when (predicate)
2564      "Skip subtree when PREDICATE returns true.
2565  PREDICATE should be a function with no arguments that returns t or nil."
2566      (let ((subtree-end (save-excursion (org-end-of-subtree t))))
2567        (when (funcall predicate) subtree-end)))
2568  
2569    ;; Helper to check for next task in subtree
2570    (defun mrb/has-next-task-p (subtree-end)
2571      "Return t if there's a TODO/BUY/WAITING task after current position within SUBTREE-END."
2572      (save-excursion
2573        (forward-line 1)
2574        (and (< (point) subtree-end)
2575             (re-search-forward "^*+ \\(TODO\\|BUY\\|WAITING\\)" subtree-end t))))
2576  
2577    ;; TODO  testing for tag presence should be easier than a re-search forward
2578    ;; TODO  are we not searching for all 'incomplete' type keywords here?,
2579    ;;        there must be an org function for that
2580    (defun mrb/skip-non-stuck-projects ()
2581      "Skip trees that are not stuck projects."
2582      (let ((subtree-end (save-excursion (org-end-of-subtree t))))
2583        (unless (and (mrb/is-project-p)
2584                     (not (mrb/has-next-task-p subtree-end)))
2585          subtree-end)))
2586  
2587    (defun mrb/skip-non-projects ()
2588      "Skip trees that are not projects."
2589      (mrb/skip-unless #'mrb/is-project-p))
2590  
2591    (defun mrb/skip-projects ()
2592      "Skip trees that are projects."
2593      (mrb/skip-when #'mrb/is-project-p))
2594  
2595    ;; Remove empty property drawers
2596    (defun mrb/org-remove-empty-property-drawers ()
2597      "*Remove all empty property drawers in current file."
2598      (interactive)
2599      (unless (eq major-mode 'org-mode)
2600        (error "You need to turn on Org mode for this function."))
2601      (save-excursion
2602        (goto-char (point-min))
2603        (while (re-search-forward ":PROPERTIES:" nil t)
2604          (save-excursion
2605            (org-remove-empty-drawer-at "PROPERTIES" (match-beginning 0))))))
2606  
2607    (defun mrb/org-remove-redundant-tags ()
2608      "Remove redundant tags of headlines in current buffer.
2609  
2610     A tag is considered redundant if it is local to a headline and
2611     inherited by a parent headline."
2612      (interactive)
2613      (when (eq major-mode 'org-mode)
2614        (save-excursion
2615          (org-map-entries
2616           '(lambda ()
2617              (let ((alltags (split-string (or (org-entry-get (point) "ALLTAGS") "") ":"))
2618                    local inherited tag)
2619                (dolist (tag alltags)
2620                  (if (get-text-property 0 'inherited tag)
2621                      (push tag inherited) (push tag local)))
2622                (dolist (tag local)
2623                  (if (member tag inherited) (org-toggle-tag tag 'off)))))
2624           t nil))))
2625  
2626  
2627    (defvar org-agenda-group-by-property nil
2628      "Set this in org-mode agenda views to group tasks by property")
2629  
2630    (defun mrb/org-group-items-by-property (prop items)
2631      "Group ITEMS by the value of property PROP.
2632  Returns a sorted alist of (PROP-VALUE . ITEMS) pairs."
2633      (let ((buckets (make-hash-table :test 'equal)))
2634        ;; Accumulate items into hash table buckets (O(n) with O(1) lookups)
2635        (dolist (item items)
2636          (let* ((marker (get-text-property 0 'org-marker item))
2637                 (pvalue (org-entry-get marker prop t)))
2638            (puthash pvalue
2639                     (cons item (gethash pvalue buckets nil))
2640                     buckets)))
2641        ;; Convert hash to alist, reverse each bucket, and sort by property value
2642        (let ((result nil))
2643          (maphash (lambda (key items)
2644                     (push (cons key (nreverse items)) result))
2645                   buckets)
2646          (sort result (lambda (i1 i2)
2647                         (string< (first i1) (first i2)))))))
2648  
2649    (defun mrb/org-group-agenda-finalize (orig-fun list &optional nosort)
2650      "Prepare bucketed agenda entry lists.
2651  Wraps `org-agenda-finalize-entries' to group entries by property."
2652      (if org-agenda-group-by-property
2653          ;; Collect parts in list, then join efficiently (O(n) instead of O(n²))
2654          (let ((parts nil))
2655            (dolist (bucket (mrb/org-group-items-by-property
2656                             org-agenda-group-by-property
2657                             list))
2658              (let ((header (concat "Property "
2659                                    org-agenda-group-by-property
2660                                    " is "
2661                                    (or (first bucket) "<nil>") ":\n")))
2662                (add-text-properties 0 (1- (length header))
2663                                     (list 'face 'org-agenda-structure)
2664                                     header)
2665                (push header parts)
2666                (push (let ((org-agenda-group-by-property nil))
2667                        (org-agenda-finalize-entries
2668                         (rest bucket) nosort))
2669                      parts)
2670                (push "\n\n" parts)))
2671            (mapconcat #'identity (nreverse parts) ""))
2672        (funcall orig-fun list nosort)))
2673    (advice-add 'org-agenda-finalize-entries :around #'mrb/org-group-agenda-finalize)
2674  
2675  
2676    (defvar mrb/org-my-archive-expiry-days 365
2677      "The number of days after which a completed task should be auto-archived.
2678     This can be 0 for immediate, or a floating point value.")
2679  
2680    (defun mrb/days-since (time-string)
2681      "Return number of days elapsed since TIME-STRING."
2682      (let ((parsed (org-parse-time-string time-string)))
2683        (time-to-number-of-days
2684         (time-subtract (current-time)
2685                        (apply #'encode-time parsed)))))
2686  
2687    (defun mrb/heading-expired-p (state-regexp end)
2688      "Return t if heading's closed time exceeds archive expiry threshold.
2689  Search for STATE-REGEXP before END position."
2690      (when (re-search-forward state-regexp end t)
2691        (>= (mrb/days-since (match-string 2))
2692            mrb/org-my-archive-expiry-days)))
2693  
2694    (defun mrb/org-my-archive-done-tasks ()
2695      "Archive DONE tasks older than `mrb/org-my-archive-expiry-days'."
2696      (interactive)
2697      (save-excursion
2698        (goto-char (point-min))
2699        (let ((done-regexp
2700               (concat "\\* \\(" (regexp-opt org-done-keywords) "\\) "))
2701              (state-regexp
2702               (concat "- State \"\\(" (regexp-opt org-done-keywords)
2703                       "\\)\"\\s-*\\[\\([^]\n]+\\)\\]")))
2704          (while (re-search-forward done-regexp nil t)
2705            (let ((end (save-excursion (outline-next-heading) (point))))
2706              (goto-char (line-beginning-position))
2707              (when (mrb/heading-expired-p state-regexp end)
2708                (org-archive-subtree)))))))
2709  
2710    (defalias 'archive-done-tasks 'mrb/org-my-archive-done-tasks)
2711  #+end_src
2712  
2713  * Key and mouse bindings
2714  Keyboard bindings are the primary way to interact for me. I have been struggling with consistent keyboard shortcuts and how to integrate them with the other systems on my machine which capture shortcut keys. At this time the following applications capture shortcut keys:
2715  
2716  1. the awesome window manager captures keys;
2717  2. xbindkeys provides a number of key bindings for application
2718     dependent operations;
2719  3. emacs (and obviously all other applications, but those are largely
2720     irrelevant).
2721  4. the X-windows server has the kbd extension which has some keyboard
2722     related things to configure.
2723  5. The linux kernel provides key mapping, so I have to look at that
2724     place too (xmodmap)
2725  
2726  Because I am daft, here is the notation for the modifiers:
2727  - C - :: control
2728  - s - :: super, meaning the (left) windows key in my configuration
2729  - M - :: meta, meaning the (left) alt key in my configuration
2730  - S - :: shift
2731  
2732  To help me out with this when writing about key bindings, the lisp function =key-description= can help out, with a little bit of glue around it:
2733  
2734  #+begin_src emacs-lisp
2735    (defun mrb/insert-key-description ()
2736      "Insert a pretty printed representation of a key sequence"
2737      (interactive)
2738      (insert (key-description (read-key-sequence "Type a key sequence:"))))
2739  #+end_src
2740  
2741  I like the explicit notation where the name of the key is spelled out better, and I'll move all configured keybindings to that eventually.
2742  
2743  The right alt and the right <menu> key should be the same as the left alt and the super key, but I haven't gotten around to configuring that yet.
2744  
2745  Furthermore, because I still can't remember keys, after pressing a prefix key like =C-c= the package =which-keys= can show me a formatted menu with the combinations of keys that can follow it.
2746  
2747  #+begin_src emacs-lisp
2748                                            ; which keys shows menu with completions of prefix-key
2749    (use-package which-key
2750      :diminish
2751      :custom
2752      (which-key-idle-delay 1.8)
2753      (which-key-show-operator-state-maps t)
2754  
2755      :config
2756      (which-key-mode))
2757  #+end_src
2758  
2759  Similarly, the =casual= package shows menu completions, when summoned, for several modes
2760  
2761  #+begin_src elisp
2762    (use-package casual)
2763  #+end_src
2764  
2765  For binding keys there are many ways it seems, with all different syntaxes and uses. I've tried to do everything with the =bind-key= package, because that is part of =use-package= so we get that for free.
2766  
2767  Bind-key can define both global keys as map-based key settings and accepts all kinds of key specifications, including strings.
2768  
2769  ** First, unsetting the keys I don't want.
2770  Let's begin with killing some bindings which are in my way, notably the standard right mouse click behavior. This is because I want it to behave in org-mode, which apparently sets this. I should probably find out a better way for this.
2771  
2772  #+begin_src emacs-lisp
2773    (unbind-key "<mouse-3>")
2774  
2775    ;; Make `C-x C-m' and `C-x RET' be different (since I tend
2776    ;; to type the latter by accident sometimes.)
2777    (unbind-key "C-x RET")
2778  #+end_src
2779  
2780  ** Setting keys
2781  The standard open-line-splits the line, which is useful, but not what I want, so define a version which can open a line above or below the current line without changing the cursor position.
2782  
2783  #+begin_src emacs-lisp
2784    (defun mrb/openline (arg)
2785      (interactive "P")
2786      (save-excursion
2787        (end-of-line (if arg nil 0))        ; with prefix, open line below, else above
2788        (open-line 1)))
2789  
2790    ;; should prefix be for above or below open line?
2791    (bind-key "C-o" 'mrb/openline)
2792  
2793  #+end_src
2794  
2795  Not sure how to bind it, C-o seems kinda busy.
2796  ** Key bindings
2797  *** Global
2798  I am running the emacs daemon and sometime when I quit emacs, I want it to quit too. This sounds a bit counter-intuitive, but as long as my emacs config is moving and I am not proficient enough in making sure I can apply the changed settings reliably from within emacs, restarting emacs is just easier. This saves me from having to kill the emacs daemon from the terminal.
2799  
2800  #+begin_src emacs-lisp
2801    (bind-key "C-x C-q" 'save-buffers-kill-emacs)
2802  #+end_src
2803  
2804  Probably the most important key is =M-x= (as set by default). That key gives access to other commands within emacs, so it better be effective. If I wasn't already used to it, I'd certainly not consider =M-x= as a first candidate. The main objection I have is that the two keys are close to each-other, making it hard to press in a typing flow.
2805  
2806  Set of bindings on which I would like to have cut, copy and paste and friends:
2807  
2808  #+begin_src emacs-lisp
2809    ;; this kinda sucks now, because the rest of the OS does not do this
2810    ;; SOLUTION: learn to work with standard emacs keybinding and adjust the OS  ?
2811    (bind-keys ("s-z" . undo)
2812               ("s-x" . clipboard-kill-region)
2813               ("s-c" . clipboard-kill-ring-save)
2814               ("s-v" . yank)
2815               ("s-a" . mark-whole-buffer))
2816  #+end_src
2817  
2818  Some operations on buffers:
2819  
2820  #+begin_src emacs-lisp
2821    ;; Buffer handling shortcuts
2822    (bind-keys ("s-n" . (lambda () (interactive) (switch-to-buffer (generate-new-buffer "Untitled"))))
2823               ("s-s" . save-buffer)
2824               ("s-k" . kill-buffer))
2825  #+end_src
2826  
2827  And the rest, for now uncategorized:
2828  
2829  #+begin_src emacs-lisp
2830    ;; Open my emacs config; I wanted something with '~'
2831    (bind-key "C-~" 'mrb/open-config)
2832  
2833    ;; Font scaling, like in firefox
2834    (bind-key "C-+" 'text-scale-increase)
2835    (bind-key "C--" 'text-scale-decrease)
2836  
2837    ;; Line handling functions
2838    (bind-key "s-`" 'toggle-truncate-lines)
2839    ;; Most of the time I want return to be newline and indent
2840    ;; Every mode can augment this at will obviously (org-mode does, for example)
2841    (bind-key "RET" 'newline-and-indent)
2842  
2843    ;; Comment code lines, command reacts based on the major mode.
2844    (bind-key "s-/" 'comment-dwim)
2845  
2846    ;; Keypad delete
2847    (bind-key [(kp-delete)] 'delete-char)
2848  
2849  #+end_src
2850  **** What should have been in emacs
2851  Sometimes there are /logical gaps/ in emacs' keybinding. I put them here
2852  
2853  #+begin_src emacs-lisp
2854    (bind-keys :map help-map
2855               ("A" . describe-face))
2856  #+end_src
2857  
2858  **** Special keys
2859  For some special keys I have defined some commands. Special keys are those keys that may not be on every keyboard, within reason. I consider the function keys also as special, although they do not fit the previous definition.
2860  
2861  #+begin_src emacs-lisp
2862    ;; Menu key does M-x, if we have it.
2863    ;;(bind-key (kbd "<apps>") 'execute-extended-command)
2864    (bind-key "<f1>" 'help-command)
2865    (bind-key "<f2>" 'save-buffer)
2866    (bind-key "<f4>" 'find-file)
2867  #+end_src
2868  
2869  **** Resizing and switching windows and frames
2870  
2871  Treading cautiously here as ideally frames sizing is the responsibility of the window manager.
2872  
2873  #+begin_src emacs-lisp
2874    ;; Moving back and forth in windows For now, I'm using the Fn Key +
2875    ;; Arrows, seems consistent with the other window movements
2876    (bind-key [XF86AudioNext] 'next-multiframe-window)
2877    (bind-key [XF86AudioPrev] 'previous-multiframe-window)
2878  
2879    ;; Alt-Cmd left-right arrows browse through buffers within the same frame
2880    (bind-key "<M-s-left>"  'previous-buffer)
2881    (bind-key "<M-s-right>" 'next-buffer)
2882  
2883    ;; These would conflict with awesome bindings, perhaps we should change those bindings
2884    ;; (bind-key "<C-S-left>"  'buf-move-left)
2885    ;; (bind-key "<C-S-right>" 'buf-move-right)
2886  
2887    ;; Awesome uses Super+Arrows to move between its 'frames'
2888    ;; Emacs   uses Shift-Super+Arrows to move between its windows
2889    (bind-key "<S-s-right>" 'windmove-right)
2890    (bind-key "<S-s-left>"  'windmove-left)
2891    (bind-key "<S-s-up>"    'windmove-up)
2892    (bind-key "<S-s-down>"  'windmove-down)
2893  
2894  #+end_src
2895  *** For active regions
2896  In quite a few situations I would like to use keybindings which are only valid when a selection/region is active.
2897  
2898  We can bind keys into the selected-keymap if we want keys to be active on every region. When only needed for a certain major mode, define a keymap named =selected-<major-mode>-map= and bind keys into that.
2899  
2900  Within orgmode I'd like to have the emphasis markers react when a region is active, so let's create the necessary fragment for those bindings. These should in theory also work in org-journal mode, but they don't.
2901  
2902  During mail composition, when deleting text, this should be replace by '[...]' to signal that we removed text from the original mail.
2903  
2904  #+begin_src emacs-lisp
2905    (use-package selected
2906      :diminish selected-minor-mode
2907      :after (org org-journal mu4e)                        ; all modes we supply binding for
2908      :demand t                             ; So we can use global mode enable in :config
2909      :commands (selected-global-mode selected-minor-mode) ; redundant?
2910      :init
2911      (setq selected-org-mode-map         (make-sparse-keymap)
2912            selected-mu4e-compose-mode-map (make-sparse-keymap))
2913  
2914      ;; Signal a deletion in compose
2915      ;; TODO  do something clever with the quote level?
2916      (defun mrb/compose-mark-deletion ()
2917        "Delete selected text and insert '[...]' to indicate elided content."
2918        (interactive)
2919        (delete-active-region)
2920        (insert "[...]"))
2921  
2922      :bind ( :map selected-keymap           ; bindings for all active regions
2923              ("C-q" . selected-off)
2924              ("C-u" . upcase-region)
2925              ("C-d" . downcase-region)
2926              :map selected-org-mode-map
2927              ("~" . (lambda () (interactive) (org-emphasize ?~)))
2928              ("_" . (lambda () (interactive) (org-emphasize ?_)))
2929              ("*" . (lambda () (interactive) (org-emphasize ?*)))
2930              ("C-b" . (lambda () (interactive) (org-emphasize ?*)))
2931              ("+" . (lambda () (interactive) (org-emphasize ?+)))
2932              ("=" . (lambda () (interactive) (org-emphasize ?=)))
2933              ("/" . (lambda () (interactive) (org-emphasize ?/)))
2934              :map selected-mu4e-compose-mode-map
2935              ("." . mrb/compose-mark-deletion))
2936  
2937      :config
2938      (selected-global-mode))
2939  #+end_src
2940  
2941  
2942  ** Other key and mouse related settings
2943  Emacs has, like for everything else, a peculiar idea on scrolling and moving from screen to screen.
2944  
2945  These settings work better for me.
2946  
2947  I have my keyboard repeat rate rather quick; this helps by moving the cursor fast. It also means that if I press a key like backspace things disappear quite quickly, so it's important that what happens on the screen is 'real-time'. The effect I want to prevent is that when releasing the backspace key, the cursor keeps on going and deletes way more than needed. I think, by default, this is properly configured, and there used to be a variable for it, which has now been obsoleted.
2948  
2949  
2950  When scrolling, I don't tend to think in /half-screens/ like emacs does, I just want the text in the window to move up or down without having to guess where it's going to be. Make sure we scroll 1 line and have a small, or none at all, scroll-margin. Having both at a value of 1 is intuitive.
2951  
2952  #+begin_src emacs-lisp
2953    (setq scroll-margin 1  scroll-step 1)
2954  #+end_src
2955  
2956  Make sure that the scroll wheel scrolls the window that the mouse pointer is over and that we are scrolling 1 line at a time. I don't use any modifiers with the scroll wheel.
2957  
2958  #+begin_src emacs-lisp
2959    (xterm-mouse-mode)
2960    (setq mouse-wheel-follow-mouse 't)
2961    (setq mouse-wheel-scroll-amount '(1 ((shift) . 1)))
2962    (setq focus-follows-mouse t) ;; i3wm changes focus automatically
2963  #+end_src
2964  
2965  * Terminals, Character encodings and emulation
2966  My policy is to use Emacs for as many things and use as little other programs as necessary. All this within reason obviously.
2967  
2968  This sections describes how terminals and shells are used from within Emacs. In an ideal situation I won't be needing any other terminal emulator program, other than the ones used directly from Emacs.
2969  
2970  ** Terminal
2971  While eshell is the more emacsy solution, it has some limitations which makes me reach for xterm for daily work. However, there's still a good option to benefit from all the emacs configuration in that case and that is the =vterm= package. This is an interface to the =libvterm= library creating a real terminal emulation using emacs for display.
2972  
2973  The basic install is easy, but does need an emacs compiled with module support.
2974  
2975  #+begin_src emacs-lisp
2976    (use-package vterm
2977      ;;:straight (:type built-in)
2978      :if (fboundp'module-load)
2979      :custom-face
2980      (vterm-color-black   ((t (:foreground "#2e3440"))))
2981      (vterm-color-red     ((t (:foreground "#bf616a"))))
2982      (vterm-color-green   ((t (:foreground "#a3be8c"))))
2983      (vterm-color-yellow  ((t (:foreground "#ebcb8b"))))
2984      (vterm-color-blue    ((t (:foreground "#81a1c1"))))
2985      (vterm-color-cyan    ((t (:foreground "#88c0c0"))))
2986      (vterm-color-magenta ((t (:foreground "#b48ead"))))
2987      :custom
2988      (vterm-timer-delay 0)
2989      (vterm-min-window-width 0)  ;; Allow vterm to adapt to any window width
2990      :config
2991      ;; Note: DFT_BACKGROUND=light produces correct difftastic colors despite dark terminal background
2992      (setenv "DFT_BACKGROUND" "light"))
2993  #+end_src
2994  
2995  To benefit from some of the most useful features, there is a little bit of shell configuration involved. The package includes a shell script for that which I am sourcing in my =.zshrc= configuration file.
2996  
2997  The =vterm= brings me some advantages, even close to being able to replace xterm for daily use, for example:
2998  - magit is now effectively a terminal package everywhere available
2999  - a terminal is an emacs buffer, so it's easy to have it available anywhere in emacs
3000  - most emacs commands will just work in a predictable way.
3001  
3002  The most important downside is that if emacs does weird things, like hang or crash, this will affect all my terminals too, which is unworkable in some situations, so xterm is not going anywhere soon (but eshell is out I think)
3003  
3004  ** Process handling
3005  Sometimes processes get stuck and i want a way to delete those processes easily.
3006  
3007  #+begin_src emacs-lisp
3008    (defun mrb/delete-process-interactive ()
3009      "Based on an auto-completed list of process, choose one process to kill"
3010      (interactive)
3011      (let ((pname (completing-read "Process Name: "
3012                                    (mapcar 'process-name (process-list)))))
3013        (delete-process (get-process pname))))
3014  #+end_src
3015  ** Log files
3016  Although handling log files is still best done in an actual terminal, not emacs, sometimes there is a need for it. The =journalctl= package is configured for that.
3017  
3018  #+begin_src elisp
3019    (use-package journalctl-mode
3020      :bind (("C-x j" . journalctl)))
3021  #+end_src
3022  
3023  * Completion
3024  
3025  There are 2 types of completion:
3026  1. Input completion in the minibuffer
3027  2. Inline completion in another buffer
3028  
3029  I want completion to work as follows:
3030  
3031  1. completion functions are always bound to a keybinding involving the TAB-key, with as little modifiers as possible;
3032  2. completion should *always* produce *something*, even if emacs has no special semantic knowledge of the current mode, it should produce /something/ which makes sense;
3033  3. completion should be inline whenever possible.
3034  4. for each mode, a specialization is ok, if that improves the situation; I expect to have many specializations to improve the auto-complete quality;
3035  5. if a completion window *must* be opened, do this at the same place always and do not mess up other windows.
3036  6. Completion should behave somewhat like real-time systems. An answer *must* be produced within a certain amount of time. If a completion answer takes longer than the amount of type to type it in full, the system has collapsed, so the time needs to be in the order of one third of the typing time.
3037  
3038  The next sections deal with the above requirements
3039  ** Ad 1. Bind completion always involves TAB-key
3040  The package =smart-tab= seems to fit this bill, but the thing that I care about can be achieved fine without it (I only found this out after considerable time using smart-tab).
3041  
3042  So, tab tries to indent, which is the main expectation, and if it can't it tries to complete stuff.
3043  
3044  #+begin_src emacs-lisp
3045    (setq tab-always-indent 'complete)
3046  #+end_src
3047  
3048  There are some situations where tab does completion (which is good), but the type of completion is not what I want (which is bad). Currently this includes only =quail-completion= of which I don't even know what it is for. This code prevents having a quail completion window when pressing tab inline.
3049  
3050  #+begin_src elisp
3051                                            ; Bit of a kludge, but when quail loads, get rid of the completion
3052    (with-eval-after-load 'quail (defun quail-completion ()))
3053  #+end_src
3054  
3055  In a standard emacs installation, TAB indents, depending on mode obviously. If indenting would not make sense, a TAB can be inserted or completion could start. The function =completion-at-point= is used in some situations. Ideally the =corfu-complete= function could take over in many cases. Here's a simplistic approach to get me started:
3056  1. if in minibuffer, do completion there like we are used to;
3057  2. if cursor is at the end of a symbol, try to complete it with corfu;
3058  3. else, indent according to mode.
3059  
3060  This is probably incomplete or wrong even in some cases, but it's a start.
3061  
3062  This way, TAB always does completion or indent, unless corfu-mode is not active.
3063  
3064  ** Ad 2. Completion should always produce something
3065  Not sure if there is anything to do here.
3066  ** Ad 3. Inline completion when possible
3067  With inline completion I mean without opening a whole new **Completions** window if not needed.
3068  
3069  Content that I want:
3070  - languages: lisp, python, ruby, bash, C/C++ roughly in that order (function and argument completion)
3071  - for all languages, function/method signature shorthands
3072  - speed, slowness is killing here
3073  - prevent minibuffer distractions, put info where my eyes are and that is the cursor in most cases.
3074  - maybe: spelling suggestions
3075  - nick completion in irc channels
3076  
3077  Candidates:
3078  - auto-complete :: http://cx4a.org/software/auto-complete/
3079  - company-mode :: http://company-mode.github.io
3080  - corfu-mode :: https://github.com/minad/corfu
3081  
3082  All of these work fine, but corfu gets installed because it fits right in with the rest of the completion packages and does more than enough for what I need. Given the quick popup is nice to complete inline, but when still confused, press =M-m= to move the completion list to the minibuffer (or whatever vertico has been configured to) to show the list and have more info on the items in the list, while competion still continues.
3083  
3084  #+begin_src emacs-lisp
3085    (use-package corfu
3086      :custom
3087      (corfu-auto t)
3088      (corfu-auto-delay 0.5)  ; increased from default 0.2s to reduce typing lag
3089      (corfu-quit-at-boundary 'separator)
3090      (corfu-cycle t)
3091  
3092  
3093      :bind ( :map corfu-map
3094              ("M-m" . mrb/corfu-move-to-minibuffer))
3095      :init
3096      ;; Enable it globally
3097      (global-corfu-mode 1)
3098  
3099      ;; But not for some
3100      (setq global-corfu-modes
3101            '(message-mode
3102              (not text-mode)
3103              t)))
3104  #+end_src
3105  
3106  ** Ad 4. Mode specialization
3107  There's always exceptions to the rule; with Emacs doubly so. Here's the relevant exceptions for my configuration.
3108  
3109  ** Ad 5. Open completion window predictably
3110  The configuration of the completion is handled by vertico now, so nothing needs to be done here.
3111  
3112  For most of the completion that is needed which is not inline (for which I am using the =corfu= package above, helm seems to be the most powerful solution, but feels isolated. For every integration something extra seems to be needed. =Vertico= seems the completing framework that fits the integration requirement best; it clearly states so in its goal to only depending on the Emacs API and not create a new one. Integrating it with others looks a lot more orthogonal than Helm.
3113  
3114  #+begin_src emacs-lisp
3115    (use-package vertico
3116      :demand t
3117      :straight (vertico :files (:defaults "extensions/*")
3118                         :includes (vertico-reverse vertico-multiform))
3119  
3120      :custom-face
3121      (vertico-current ((t (:background "#5e81ac"))))
3122      :config
3123      (vertico-mode))
3124  #+end_src
3125  
3126  As =M-x= or =execute-extended-command= is probably the most used command in Emacs, I want to give it a bit more prominent place on the scree than just the tiney minibuffer at the bottom. The =vertico-posframe= package turns the minibuffer into a floating popup sort of screen by using posframe. This puts the minibuffer more in my face and gives some better options to present the command information.
3127  
3128  #+begin_src emacs-lisp
3129    (use-package vertico-posframe
3130      ;; Customize border face to same color as cursor
3131      :custom-face
3132      (vertico-posframe-border ((t (:background "DarkOrange" :inherit default))))
3133      :config
3134      (setq vertico-posframe-parameters
3135            `((left-fringe . 18)
3136              (right-fringe . 18)
3137              (border-width . 4)
3138              (border-color . ,mrb/cursor-color)
3139              (child-frame-border-width . 4)))
3140  
3141      (setq vertico-multiform-commands
3142            '((consult-line posframe (vertico-posframe-fallback-mode . vertico-buffer-mode))
3143              (t posframe)))
3144  
3145      (vertico-multiform-mode 1))
3146  #+end_src
3147  
3148  The first thing to complement =vertico= is the =orderless= package. My main use is to have the matching for completing items match 'first second' as well as 'second first' regardless of their order. Note that this works for every completion, also within =corfu= which is especially useful.
3149  
3150  #+begin_src emacs-lisp
3151    (use-package orderless
3152      :custom-face
3153      (orderless-match-face-0 ((t (:foreground "dark orange"))))
3154      :custom
3155      (completion-styles '(orderless basic)))
3156  #+end_src
3157  
3158  The default list of candidates can be nicely augmented with =marginalia= which shows extra info about the matching items in the right aligned margin.
3159  
3160  #+begin_src emacs-lisp
3161    (use-package marginalia
3162      :custom
3163      (marginalia-align 'right)
3164  
3165      :init
3166      (marginalia-mode))
3167  #+end_src
3168  
3169  The =consult= package exposes a number of =consult-= commands which work with the completing framework and typically put an original command on steroids. Some of the commands support live previewing while browsing the selections.
3170  
3171  #+begin_src emacs-lisp
3172    (use-package consult
3173      ;; Replace some bindings with a consult command, so we get things on steroids
3174      :bind (("C-x b" . consult-buffer)))
3175  #+end_src
3176  
3177  Embark is the last of the set, and adds a new level of interaction. The basic principle normally in Emacs is that you specify a command and then tell what to act on. Like opening a file is the `find-file` command and then specifying what file to find.
3178  
3179  Embark is the other way around. Something is active, depending on context and `embark-act` give a list of things you can do with it.
3180  
3181  #+begin_src emacs-lisp
3182  
3183    (use-package embark
3184      :straight (embark :files (:defaults "embark-org.el"))
3185  
3186      :bind
3187      (("C-." . embark-act)         ;; pick some comfortable binding
3188       ("C-;" . embark-dwim)        ;; good alternative: M-.
3189       ("C-h B" . embark-bindings)) ;; alternative for `describe-bindings'
3190  
3191      :init
3192      ;; Optionally replace the key help with a completing-read interface
3193      (setq prefix-help-command #'embark-prefix-help-command)
3194  
3195      :config
3196      ;; Hide the mode line of the Embark live/completions buffers
3197      (add-to-list 'display-buffer-alist
3198                   '("\\`\\*Embark Collect \\(Live\\|Completions\\)\\*"
3199                     nil
3200                     (window-parameters (mode-line-format . none)))))
3201  
3202    ;; Consult users will also want the embark-consult package.
3203    (use-package embark-consult
3204      :ensure t ; only need to install it, embark loads it after consult if found
3205      :hook
3206      (embark-collect-mode . consult-preview-at-point-mode))
3207  #+end_src
3208  ** Ad 6. Guaranteed response-time
3209  I haven't found a solution for this yet, but also have not found it to be a problem that needs solving in practice so far.
3210  
3211  * Editing control
3212  Editing control are generic features which make editting easier, faster, more productive etcetera
3213  
3214  Techniques in use by me:
3215  - multiple cursors :: doing the same edit actions over multiple locations at once
3216  - templates :: using a /shortcut/ or /abbreviation/ to insert templates/forms to fill in common constructs
3217  
3218  Multiple cursors is a package which turns 1 cursor into many, depending on the active region, and enters insertions at all cursor locations.
3219  
3220  #+begin_src emacs-lisp
3221    (use-package multiple-cursors
3222      :bind (("C-c e" . 'mc/edit-lines)))
3223  #+end_src
3224  
3225  Tempel is a templating system, with templates being lisp data, which hooks into modes and autocompletion system. The folder =~`emacs-user-dir`/templates= holds =*.eld= files which specify templates which get autoloaded based on the major mode of the current buffer.
3226  
3227  I set it up for relevant modes as a CAPF handler (for programming and orgmode for now)
3228  #+begin_src emacs-lisp
3229    (use-package tempel
3230      :custom
3231      ;; This would start completion directly, which I do NOT want
3232      (tempel-trigger-prefix nil)
3233      (tempel-path `(,(mrb/emacs-file "templates/*.eld")))
3234  
3235      :bind (("M-<insert>" . tempel-complete))
3236  
3237      :after (org)
3238  
3239      :init
3240  
3241      (setq org-tab-before-tab-emulation-hook nil)
3242      (defun mrb/tempel-setup-capf()
3243        ;; Remove org structured templates, which are just tempo templates as well
3244        ;; TODO make this smarter? (there might be other things in there which I *do* need)
3245        (setq org-tab-before-tab-emulation-hook nil)
3246        (setq-local completion-at-point-functions
3247                    (cons #'tempel-complete ; use tempel-expand if only exact matches wanted
3248                          completion-at-point-functions)))
3249  
3250      ;; Do template name completion in programming, textmodes and orgmode
3251      ;; TODO why not global?
3252      :hook ((prog-mode text-mode org-mode) . mrb/tempel-setup-capf))
3253  #+end_src
3254  
3255  Formatting a file properly is trusted by indent rules, this one indents the whole buffer at once, making formatting completely proper  🫠
3256  
3257  #+begin_src elisp
3258    (defun mrb/indent-whole-buffer ()
3259      "Indent the entire buffer without affecting point or mark."
3260      (interactive)
3261      (save-excursion
3262        (save-restriction
3263          (indent-region (point-min) (point-max)))))
3264  
3265    (global-set-key (kbd "C-c TAB") 'mrb/indent-whole-buffer)
3266  #+end_src
3267  ** Navigation
3268  Navigating pieces of text effectively is probably the best optimization to make in the emacs configuration.
3269  
3270  In my browser I use vimium to jump to links with the keyboard. In emacs =avy= does something similar. On pressing the hotkey, all matches of the first char typed are highlighted (frame wide) by a single character. By pressing that character the cursor is place there directly. This makes =within-frame= navigation a 3-key operation, which is considerably faster than anything else.
3271  
3272  #+begin_src emacs-lisp
3273    ;; Bind super-j globally to jump quickly to anything in view
3274    (use-package avy
3275      :bind (("s-j" . 'avy-goto-char)))
3276  #+end_src
3277  In a more general sense, =evil-mode=, the VI emulation layer on top of emacs, is the ultimate navigational package. I have tried this for about 6 months, but it's not for me (yet).
3278  
3279  ** Searching
3280  Incremental search (isearch) is a core navigation tool for finding text in buffers. The default faces don't provide enough visual distinction between the current match and other matches. With better color contrast, it's immediately clear which match is selected and where the other matches are located.
3281  
3282  #+begin_src emacs-lisp
3283    ;; Improve search highlighting to clearly distinguish current match from others
3284    (use-package isearch
3285      :straight (:type built-in)
3286      :custom-face
3287      ;; Current match: bright yellow text on dark background for maximum visibility
3288      ;; Other matches: bright purple text on dark background for clear distinction
3289      (isearch                        ((t (:foreground "#f0d77d" :background "#2e3440"))))
3290      (lazy-highlight                 ((t (:foreground "#b988dd" :background "#2e3440")))))
3291  #+end_src
3292  
3293  ** Outlining
3294  Outline mode provides a way to structure text hierarchically. The outline heading levels use distinct colors to visually separate different depth levels, making it easy to navigate document structure at a glance.
3295  
3296  Provide base faces for the outline levels, packages supporting some form of outlining should inherit from these.
3297  
3298  #+begin_src emacs-lisp
3299    (use-package outline
3300      :straight (:type built-in)
3301      :custom-face
3302      (outline-1 ((t (:foreground "#5e81ac"))))
3303      (outline-2 ((t (:foreground "#a3be8c"))))
3304      (outline-3 ((t (:foreground "#b48ead"))))
3305      (outline-4 ((t (:foreground "#81a1c1"))))
3306      (outline-5 ((t (:foreground "#ebcb8b"))))
3307      (outline-6 ((t (:foreground "#8fbcbb"))))
3308      (outline-7 ((t (:foreground "#d08770"))))
3309      (outline-8 ((t (:foreground "#bf616a")))))
3310  #+end_src
3311  
3312  * Remote editing
3313  As my default shell is =zsh= tramp does not work out of the box for me. It gets confused by my prompt, so on top of my =.zshrc= file I have the following line:
3314  
3315  #+begin_src sh
3316    # Immediately bail out if we are coming in with a dumb terminal
3317    # which, in my case, mostly means TRAMP is asking for something.
3318    [[ $TERM == "dumb" ]] && unsetopt zle && PS1='$ ' && return
3319  #+end_src
3320  
3321  Once that is in place files can be opened with =/ssh:/host:/path/to/file= syntax. Surprisingly, the syntax I got used to in the past (i.e. =/host:/path/to/file=) does not work for me anymore.
3322  
3323  #+begin_src emacs-lisp
3324    (use-package tramp
3325      :straight (:type built-in)            ; tramp is closely tied to emacs, use builtin
3326      :custom
3327      (tramp-default-method "sshx")
3328      (tramp-syntax 'default)
3329      (tramp-terminal-type "dumb")
3330      (tramp-default-proxies-alist '(("^ha$" "^root$" "/sshx:ha:")))
3331  
3332      :config
3333      (setenv "SHELL" "/bin/sh")
3334      (add-to-list 'tramp-connection-properties
3335                   (list ".*" "locale" "LC_ALL=C")))
3336  
3337  #+end_src
3338  * Browser integration
3339  My default browser, as set in the OS, should be started automatically on opening =http:= like stuff and =html= files. The only link to the /outside world/ is specifying that =xdg-open= should figure out what program to use.
3340  
3341  #+begin_src emacs-lisp
3342    (setq browse-url-browser-function 'browse-url-generic)
3343    (setq browse-url-generic-program "xdg-open")
3344  #+end_src
3345  
3346  Not exactly browser integration, but 'openstreetmap browsing' is close enough. I would like to use this for planning trips, so something like a 'set of view settings' for this package would be nice. This would allow me to gather maps views, augment them with a gpx route and bookmarks. Not sure if gpx waypoints/POI sets would work.
3347  
3348  The package as is, is already useful for searching locations and storing links to locations.
3349  
3350  #+begin_src emacs-lisp
3351    (use-package osm
3352      :straight (:host github :repo "minad/osm")
3353      :commands (osm osm-mode)
3354      :bind ( :map osm-mode-map
3355              ("q" . (lambda() (interactive) (quit-window t))))
3356      :init
3357      ;; Load Org link support
3358      (with-eval-after-load 'org
3359        (require 'osm-ol))
3360  
3361      :custom
3362      (osm-outdoor-url
3363       (concat "https://tile.thunderforest.com/outdoors/{%z}/{%x}/{%y}.png?apikey="
3364               (password-store-get "API/tile.thunderforest.com")))
3365      (map5-base (concat "https://s.map5.nl/map/"
3366                         (password-store-get "API/map5.nl")
3367                         "/tiles/"))
3368      (osm-map5-opentopo-url
3369       (concat map5-base "opentopo/EPSG900913/{%z}/{%x}/{%y}.jpeg"))
3370  
3371      (osm-map5-opensimpletopo-url
3372       (concat map5-base "opensimpletopo/EPSG900913/{%z}/{%x}/{%y}.jpeg"))
3373  
3374      (osm-map5-openlufo-url
3375       (concat map5-base "openlufo/EPSG900913/{%z}/{%x}/{%y}.jpeg"))
3376  
3377      (osm-server-list
3378       `((default
3379          :name "Mapnik"
3380          :description "Standard Mapnik map provided by OpenStreetMap"
3381          :url "https://%s.tile.openstreetmap.org/%z/%x/%y.png"
3382          :group "Standard")
3383         (outdoor :name "Outdoor"
3384                  :description "Outdoor focussed maps"
3385                  :url ,osm-outdoor-url
3386                  :group "Personal")
3387         (opentopo :name "OpenTopo NL"
3388                   :description "Map5.nl OpenTopo"
3389                   :url ,osm-map5-opentopo-url
3390                   :group "Personal")
3391         (opensimpletopo  :name "OpenSimpleTopo NL"
3392                          :description "Map5.nl OpenSimpleTopo"
3393                          :url ,osm-map5-opensimpletopo-url
3394                          :group "Personal")
3395         (openlufo  :name "OpenLufo NL"
3396                    :description "Map5.nl OpenLufo"
3397                    :url ,osm-map5-openlufo-url
3398                    :group "Personal")))
3399  
3400      ;; set proper home location, TODO  read this from private store
3401      (osm-home '(51.6441759 4.4377029 17))
3402      (osm-copyright nil))
3403  #+end_src
3404  * Messaging and chatting
3405  ** Mail
3406  This section describes a search for the mail setup I want in Emacs. There are a few mail related packages and I'm unsure at what I want to use; reading the feature list doesn't cut it.  So, I'm having a setup where multiple mail handling packages may exist and hopefully one will float to the top as being *the one*
3407  
3408  *** Composing mail
3409  Composing mail is often an /out of band/ activity, like creating a tweet or a capture, so I would like to have roughly the same behavior. This is by default provided by compose-mail-other-frame, which in turn calls the right mua to do the job. Oddly enough, it is still required to have =mu4e-compose-in-new-frame= set to actually have another frame, so I'm writing it with mu4e-compose directly.
3410  
3411  #+name: mu4e-composeconfig
3412  #+begin_src emacs-lisp :tangle no
3413    :bind (("C-x m" . 'mrb/compose-mail)
3414           ( :map mu4e-compose-mode-map
3415             ("C-;" . mu4e-compose-context-switch)))
3416  
3417    :custom
3418    (mu4e-compose-format-flowed t)
3419    (mu4e-compose-switch nil)
3420    (mu4e-compose-dont-reply-to-self t)
3421    (mu4e-compose-complete-only-after nil) ; no limit on contact completion for now
3422  
3423    :config
3424    (defun mrb/compose-mail (&optional mailto-url)
3425      "Run mail-compose, use mailto URI if it is given."
3426      (interactive)
3427  
3428      ;; If we have a mailto argument, parse and use it
3429      (if (and (stringp mailto-url)
3430               (string-match "\\`mailto:" mailto-url))
3431          (browse-url-mail mailto-url)
3432        ;; No mailto, argument, just run the mua
3433        (mu4e-compose-new)))
3434  
3435    (defun mrb/mailfile()
3436      "Compose mail message from current buffer file, typically a pdf is viewed for my use-case"
3437      (interactive)
3438      (if-let ((file buffer-file-name))
3439          (let ((mimetype (mm-default-file-type file)))
3440            (mu4e-compose-new)
3441            (mml-attach-file file mimetype (concat mimetype " attachment") "attachment"))
3442        (user-error "Current buffer is not visiting a file")))
3443  
3444    ;; Context switching in compose
3445    <<mu4e-context-switch-in compose>>
3446  
3447    ;; Extend attaching files with extra info insertion
3448    <<extend-attach>>
3449  
3450  
3451  #+end_src
3452  
3453  To be able to use the =mrb/compose-mail= function as a mailto handler we need to be able to call it from outside of emacs. Let's define a small shell script that does exactly this. The SRC attributes tangle it into my =bin= directory where is will be in the path. In the OS, this script will need to be set as the default mail handler.
3454  
3455  #+begin_src sh :exports code :tangle ~/bin/mailto-handler :shebang #!/bin/bash
3456    # Emacs mailto URI handler
3457  
3458    # Make sure mailto: is always prepended to $1
3459    mailto="mailto:${1#mailto:}"
3460    mailto=$(printf '%s\n' "$mailto" | sed -e 's/[\"]/\\&/g')
3461  
3462    # Call the elisp function handling our mailto URI
3463    elisp_expr="(mrb/compose-mail \"$mailto\")"
3464  
3465    # Do not create new frame, because mu4e does that already
3466    # which is a simpler solution in this case
3467    edit-noframe --eval "$elisp_expr"
3468  #+end_src
3469  
3470  On top of that, I want a manual capture script which is basically the same as the mailto handler.
3471  #+begin_src sh :exports code :tangle ~/bin/capture-mail.sh :shebang #!/bin/sh
3472    edit -e '(mrb/compose-mail)'
3473  #+end_src
3474  
3475  **** Attachments
3476  When attaching files, it is sometimes useful to extract some info from the attached file and include it in the message. My main usecase is extracting PDF annotations (mostly for people who cannot process the annotations in their PDF viewer directly.
3477  
3478  The way I have implemented this is by advicing the function =mml-insert-empty-tag= which is used by the =mml-attach-file= function. At that point in the code the file and point in the active buffer are known, and we just have to process the extra actions.
3479  The advice function extracts the annotations from the pdf file (unconditionally at the moment) and inserts a rendering of them just before inserting the attachment itself.
3480  
3481  #+name: extend-attach
3482  #+begin_src emacs-lisp :tangle no
3483    :config
3484    ;; TODO  return info, do not use insert directly, so we have more control
3485    (defun mrb/pdf-extract-info (file)
3486      "Extract and render the pdf annotations in FILE"
3487      (mapc
3488       (lambda (annot) ;; traverse all annotations
3489         (progn
3490           (let ((page (cdr (assoc 'page annot)))
3491                 (highlighted-text
3492                  (if (pdf-annot-get annot 'markup-edges)
3493                      (let ((highlighted-text
3494                             (pdf-info-gettext (pdf-annot-get annot 'page)
3495                                               (pdf-tools-org-edges-to-region
3496                                                (pdf-annot-get annot 'markup-edges))
3497                                               t
3498                                               file)))
3499                        (replace-regexp-in-string "\n" " " highlighted-text))
3500                    nil))
3501                 (note (pdf-annot-get annot 'contents))
3502                 (type (pdf-annot-get annot 'type))) ; underline, strike-through etc.
3503             ;; Stuff gathered, start rendering
3504             (when (or highlighted-text (> (length note) 0))
3505               (insert (format "\n- page %s" page))
3506  
3507               (when highlighted-text
3508                 (insert (format ": “%s”\n" highlighted-text)))
3509  
3510               (if (> (length note) 0)
3511                   (insert (format "\n  %s\n" note))
3512                 (insert "\n" ))))))
3513       ;; list to traverse, second arg of mapc
3514       (cl-remove-if                          ; don't process links?
3515        (lambda (annot) (member (pdf-annot-get-type annot) (list 'link)))
3516        ;; file is typically already visited
3517        (pdf-annot-getannots nil '() (find-file-noselect file)))
3518       ))
3519  
3520  
3521    (defun mrb/extended-attach-file (name &rest plist)
3522      "Advice `:before `mml-insert-empty-tag to grab additional info from the attachment."
3523      (let* ((file (plist-get plist 'filename))
3524             (mimetype (if file (mm-default-file-type file) nil)))
3525        (when (string= mimetype "application/pdf")
3526          ;; Extract and render annotations
3527          (mrb/pdf-extract-info file))))
3528  
3529    (advice-add
3530     'mml-insert-empty-tag                  ; a lot easier because we already know the file
3531     :before
3532     'mrb/extended-attach-file
3533     '((name . "extended-attach")))
3534  #+end_src
3535  
3536  **** Footnotes
3537  While composing mail referring to external resources through links is done very often. While creating links in HTML mail, which I do not use, gives the option to name a link, there's no such thing in plain text. This usually works out fine, but when there are many links I fall back to creating 'footnotes' which actuall hold the links while in the text there is just an identifier to point the reader to the proper footnote.
3538  
3539  Of course these footnotes can be used for other things as well.
3540  
3541  Here's what I would like:
3542  
3543  - easy shortcut to start adding a footnote
3544  - easy shortcut to return to the main text
3545  - insert the proper numbers automatically, optionally renumber when I put an extra in later on.
3546  - insert superscript like \sup1 or \sup2 into the text
3547  - insert captured content at bottom of current text (but not below quotes)
3548  
3549  Footnote mode seems to do exactly that, configured properly. Its prefix key is a bit awkward, so I have changed it and having the unicode parentheses for start and end tag is not ideal, but the package requires there to be /something/; I'd prefer there to be /nothing/ but that messes up renumbering and other operations.
3550  
3551  - Key shortcuts are a bit awkward, as ! is awkward
3552    A: changed the prefix to ="C-c n"= which makes more sense
3553  
3554  - auto enable in message mode, mu4e compose
3555  
3556    #+begin_src emacs-lisp
3557      (use-package footnote
3558        :straight (:type built-in)
3559  
3560        ;; Use a better prefix than the default 'C-c !'
3561        :bind-keymap ("C-c n" . footnote-mode-map)
3562  
3563        :hook (message-mode . footnote-mode)  ; I use it in mu4e
3564        :config
3565        (setq footnote-section-tag "" ; No introduction needed?
3566              footnote-start-tag "⁽"  ; doc says not to use empty string
3567              footnote-end-tag "⁾"
3568              footnote-style 'unicode         ; this makes footnotes use superscript numbering
3569              footnote-body-tag-spacing 1     ; between the \sup1 and the footnote text
3570              ))
3571    #+end_src
3572  *** Sending mail
3573  Sending mail through smtp, using the smtpmail package
3574  
3575  #+begin_src emacs-lisp
3576    (use-package smtpmail
3577      :straight (:type built-in)
3578      :custom
3579      (smtpmail-default-smtp-server "localhost")
3580      (smtpmail-service 25)
3581      (smtpmail-local-domain user-domain)
3582      (smtpmail-sendto-domain user-domain)
3583      ;; User agent style is message mode by default, specific mail packages may override this
3584      (mail-user-agent 'message-user-agent)
3585      (send-mail-function 'smtpmail-send-it) ;This is for mail-mode
3586      (message-send-mail-function 'message-smtpmail-send-it) ; This is for message-mode
3587  
3588      (password-cache t)            ; default is true, so no need to set this actually
3589      (password-cache-expiry 28800)) ; default is 16 seconds, which is ridiculously low
3590  #+end_src
3591  
3592  At times we want to send signed and/or encrypted mail
3593  
3594  #+begin_src emacs-lisp
3595    (use-package mml-sec
3596      :straight (:type built-in)
3597      :config
3598      ;; Use my key to sign messages and make it safe if other keys exist
3599      (add-to-list 'mml-secure-openpgp-signers user-gpg-key)
3600      (setq mml-secure-key-preferences
3601            '((OpenPGP
3602               (sign)
3603               (encrypt
3604                (user-mail-address user-gpg-key)))
3605              (CMS
3606               (sign)
3607               (encrypt))))
3608  
3609      ;; Add my bcc address to list of safe addresses in bcc for secure message
3610      (add-to-list 'mml-secure-safe-bcc-list mrb/bcc-address)
3611  
3612      ;; Always encrypt to self, so I can read my own messages
3613      (setq mml-secure-openpgp-encrypt-to-self `(,user-gpg-key)))
3614  #+end_src
3615  
3616  *** Generic mail message settings
3617  It's not entirely clear which package is exactly responsible for what, but there are a few settings which are related to the =message= package
3618  
3619  #+begin_src emacs-lisp
3620    (use-package message
3621      :demand t ; mu4e won't load otherwise
3622      :straight (:type built-in)
3623  
3624      :init
3625      (defun mrb/message-mode-hook ()
3626        "This configures message mode and thus other modes which inherit message mode."
3627        ;; We have to make sure that the messages we produce are
3628        ;; format=flowed compatible. This happens on encoding when sending
3629        ;; it. However, during compose we need to make sure that what we
3630        ;; offer to encode is suitable for it.
3631  
3632        ;; Let's do the compose part first No reflowing is possible without
3633        ;; having hard newlines, so lets always enable those
3634        (use-hard-newlines t 'always)
3635  
3636        ;; Use visual line mode and visually break at word boundaries which
3637        ;; is the same as the email encoding on send (see below)
3638        (visual-line-mode 1)
3639        (auto-fill-mode 0)                    ; Makes no sense in visual mode
3640        (setq-local adaptive-fill-mode nil)   ; This messes up fill-flowed-encode
3641  
3642        (setq visual-fill-column-width nil)   ; Make sure `fill-column gets used
3643        (setq fill-column 80)                 ; Not the same as encode column below!
3644        (visual-fill-column-mode 1)           ; Show it
3645  
3646        ;; Next, do the encoding and sending part
3647        ;; Set the fill column on encoding (send)
3648        ;; 66 would be the proper value, but to help rendering
3649        ;; on the other end 998 is the max which helps a bit
3650        (setq fill-flowed-encode-column 66)
3651  
3652        ;; This enables the f=f encoding for sending
3653        (setq mml-enable-flowed t))
3654  
3655      :hook (message-mode . mrb/message-mode-hook)
3656  
3657      :config
3658      (setq
3659       message-signature-directory mrb/maildir
3660       message-signature-file mrb/default-signature-file
3661  
3662       ;; Register my alternative email-adresses, other than the default
3663       ;; This steers From header when replying and as such has some overlap with mu4e contexts
3664       message-alternative-emails (regexp-opt mrb/private-addresses)
3665  
3666       ;; When citing, remove the senders signature.
3667       ;; TODO  often I want to remove a lot more, like the disclaimer
3668       message-cite-function 'message-cite-original-without-signature
3669  
3670       ;; Citing/Quoting when replying
3671       message-yank-prefix "> "             ; Add '> ' when quoting lines
3672       message-yank-cited-prefix ">"        ; Add '>' for cited lines
3673       message-yank-empty-prefix ""         ; Add nothing for empty lines
3674       )
3675  
3676      ;; Define the headers that will be sent
3677  
3678      ;; Add openpg preferences
3679      ;; message-openpgp-header is set in personal.el
3680      (add-hook 'message-header-setup-hook 'message-add-openpgp-header)
3681  
3682      ;; This will have an effect in mu4e starting from 1.11.23
3683      (setq
3684       ;; NOTE: mu4e sets message-hidden-headers to a local value based on by mu4e-compose-hidden-headers
3685       message-hidden-headers '(not "To:" "From:" "Cc:" "Subject:")
3686  
3687       ;; How to attribute
3688       message-citation-line-format "[%N]:"
3689       message-citation-line-function 'message-insert-formatted-citation-line
3690  
3691       message-kill-buffer-on-exit t))
3692  #+end_src
3693  
3694  *** Mu4e specific settings
3695  After a journey with many different MUAs I seem to be settling on =mu4e=.
3696  
3697  #+begin_src emacs-lisp
3698    ;; TODO  check for system packages meson, g++, pkg-config,
3699    ;;        cmake, libglib2.0-dev, libgmime3.0-dev, libxapian-dev, texinfo
3700    ;; (names are debian package names)
3701    (use-package mu4e
3702      ;; Straight needs some tweaking
3703      :straight ( :type git :host github :repo "djcb/mu"
3704                  :pre-build (
3705                              ("./autogen.sh")
3706                              ("meson" "configure" "-D" "use-embedded-fmt=true" "build")
3707                              ("ninja" "-C" "build"))
3708                  :files (:defaults "build/mu4e/*.el")
3709                  :includes (mu4e-icalendar))
3710  
3711    :custom-face
3712    (mu4e-header-highlight-face ((t (:inherit hl-line :underline nil
3713                                              :weight normal :foreground unspecified))))
3714    (mu4e-unread-face           ((t (:inherit nil :underline nil :weight bold))))
3715    (mu4e-context-face          ((t (:inherit mu4e-link-face))))
3716    (mu4e-flagged-face          ((t (:foreground "#ebcb8b" ))))
3717    (mu4e-title-face            ((t (:foreground "#5e81ac"))))
3718  
3719    :custom
3720    (mu4e-mu-binary (mrb/emacs-file "straight/repos/mu/build/mu/mu"))
3721  
3722    :after (message)
3723    :commands (mu4e mrb/compose-mail mrb/mailfile)
3724    :hook ((mu4e-view-mode . mrb/mu4e-view-mode-hook)
3725           (mu4e-headers-mode . (lambda () (eldoc-mode -1))))
3726  
3727    :config
3728    (defun mrb/mu4e-view-mode-hook ()
3729      (interactive)
3730      (turn-on-visual-line-mode)
3731      ;; Reflow messages
3732      (setq mm-fill-flowed t))
3733  
3734    :bind ( :map mu4e-search-minor-mode-map
3735            ("/" . mu4e-search)
3736            ("s" . nil)
3737            ("e" . mu4e-search-edit))
3738  
3739    ;; Configuration sections
3740    <<mu4e-composeconfig>>
3741    <<mu4e-main-config>>
3742    <<mu4e-headers-config>>
3743    <<mu4e-view-config>>
3744    <<mu4e-packages>>
3745  
3746    ;; TODO  move these up
3747    (defun mrb/setsigfile (ctxname)
3748      (expand-file-name  (concat ".signature-" (downcase ctxname)) mrb/maildir))
3749  
3750    (setq
3751     ;; Override the default mail user agent with the mu4e specific one
3752     mail-user-agent 'mu4e-user-agent
3753     mu4e-sent-folder        "/Sent"
3754     mu4e-drafts-folder      "/Drafts"
3755     mu4e-trash-folder       "/Trash"
3756     mrb/mu4e-junk-folder    "/Junk"         ; Added for my specific config
3757     mrb/mu4e-archive-folder "/Archives"
3758     ;; mu4e-attachment-dir is set in personal.el
3759  
3760     ;; Refiling to /Archives/YYYY
3761     mu4e-refile-folder (lambda (msg)
3762                          "Refile message to archive organized by year."
3763                          (let* ((archive-base (concat mrb/mu4e-archive-folder "/"))
3764                                 (msg-date (mu4e-message-field msg :date))
3765                                 (year (when msg-date (format-time-string "%Y" msg-date))))
3766                            (if year
3767                                (concat archive-base year)
3768                              archive-base)))
3769  
3770  
3771     mu4e-use-fancy-chars t
3772     mu4e-get-mail-command "mbsync quick" ; Just get inbox and important stuff, not everything
3773     mu4e-decryption-policy 'ask
3774     mu4e-update-interval 120             ; we just index, no retrieve, so 2mins is fine
3775  
3776     ;; Set completing function to standard, so completing frameworks can use it
3777     mu4e-read-option-use-builtin nil
3778     mu4e-completing-read-function 'completing-read
3779     mu4e-save-multiple-attachments-without-asking t
3780  
3781     ;; Use same hidden setting as message mode
3782     mu4e-compose-hidden-headers message-hidden-headers
3783  
3784     ;; Context definitions
3785     ;; Goal 1. use global config, unless private address is used
3786     ;; TODO  Can we use the one that matched?
3787     mu4e-context-policy 'pick-first      ; When nothing matches only!
3788     mu4e-compose-context-policy 'nil     ; for compose, use current if nothing matches
3789  
3790     mu4e-search-results-limit -1         ; until we run into performance problems
3791     mu4e-search-include-related nil      ; do not included related by default
3792  
3793  
3794     mu4e-contexts
3795     `(,(let* ((ctxname "Default")
3796               (sigfile (mrb/setsigfile ctxname)))
3797          (make-mu4e-context
3798           :name ctxname
3799           :match-func (lambda (msg)
3800                         (when msg
3801                           (mu4e-message-contact-field-matches
3802                            msg `(:to :cc :from) mrb/default-email-address)))
3803           ;; Use global config, but restore what was changed in other contexts
3804           :vars `((user-mail-address      . ,mrb/default-email-address)
3805                   (message-signature-file    . ,sigfile)
3806                   (mu4e-compose-signature . (with-current-buffer
3807                                                 (find-file-noselect message-signature-file)
3808                                               (buffer-string))))))
3809       ,(let* ((ctxname "Private")
3810               (sigfile (mrb/setsigfile ctxname)))
3811          (make-mu4e-context
3812           :name "Private"
3813           :match-func (lambda (msg)
3814                         (when msg
3815                           (mu4e-message-contact-field-matches
3816                            msg `(:to :cc :from) mrb/private-addresses)))
3817           :vars `((user-mail-address     . ,mrb/default-private-address)
3818                   (message-signature-file   . ,sigfile)
3819                   (mu4e-compose-signature . (with-current-buffer
3820                                                 (find-file-noselect message-signature-file)
3821                                               (buffer-string))))))
3822       )
3823     )
3824  
3825    ;; Rendering of html mail is done with shr, but I prefer not to show it at all
3826    (with-eval-after-load "mm-decode"
3827      (add-to-list 'mm-discouraged-alternatives "text/html")
3828      (add-to-list 'mm-discouraged-alternatives "text/richtext"))
3829  
3830    (defun mrb/mu4e-shr2text (msg)
3831      "Overridden mu4e-shr2text"
3832      (mu4e~html2text-wrapper
3833       (lambda ()
3834         (let ((shr-inhibit-images nil)   ;
3835               (shr-width nil)            ; Use whole window width for rendering
3836               (shr-use-colors nil)       ; Don't use colors from html, they ugly
3837               (shr-bullet "• "))         ; Original was "* "
3838           (shr-render-region (point-min) (point-max)))) msg))
3839  
3840    (setq mu4e-html2text-command 'mrb/mu4e-shr2text))
3841  #+end_src
3842  **** Main screen
3843  When mu4e start, it always opens in its main screen, which contains the main operations, visible bookmarks.
3844  
3845  #+name: mu4e-main-config
3846  #+begin_src emacs-lisp :tangle no
3847    :bind ( :map mu4e-main-mode-map
3848            ("q" . mrb/mu4e-quit) ; just close the menu
3849            ("Q" . mu4e-quit)     ; really Quit
3850            (";" . nil)           ; this was too easy to press by mistake
3851            ("G" . (lambda () (interactive) (mu4e-update-mail-and-index 1)))
3852            ("C-;" . mu4e-context-switch))
3853    :init
3854    ;; Claim the whole screen in mu4e main window
3855    (defun mrb/mu4e-quit ()
3856      (interactive)
3857      (bury-buffer))
3858  
3859    :config
3860    (setq mu4e-main-buffer-hide-personal-addresses t ; no need to see my own addresses
3861          mu4e-main-hide-fully-read nil
3862          mu4e-confirm-quit nil
3863  
3864          mu4e-bookmarks (list
3865                          '( :name "Unread messages"
3866                             :query "flag:unread and not flag:trashed"
3867                             :key ?u)
3868                          '( :name "INBOX"
3869                             :query "(flag:unread or flag:new or maildir:/INBOX) and not (flag:draft or flag:flagged or maildir:/Trash or flag:trashed)"
3870                             :favorite t
3871                             :key ?i)
3872                          '( :name "TODO list"
3873                             :query "flag:flagged and not flag:trashed"
3874                             :key ?+)
3875                          '( :name "Today's messages"
3876                             :query "date:today..now"
3877                             :key ?d)
3878                          '( :name "Trash"
3879                             :query "maildir:/Trash or flag:trashed"
3880                             :key ?t)
3881                          '( :name "Junk"
3882                             :query "maildir:/Junk"
3883                             :key ?j)))
3884  #+end_src
3885  **** Header view
3886  The first view after searching is a list of mail headers. Set config vars for this view and define the visualisation of the marks. I need an extra action to mark mail as junk which, on processing, moves the marked messages to the junk folder
3887  
3888  #+name: mu4e-headers-config
3889  #+begin_src emacs-lisp :tangle no
3890    :bind ( :map mu4e-headers-mode-map
3891            ("s" . mu4e-headers-mark-for-junk)
3892            ("r" . mu4e-compose-reply)
3893            ("R" . mu4e-compose-wide-reply)
3894            ("A" . mu4e-headers-mark-for-refile)
3895            ("SPC" . mu4e-headers-mark-for-something)
3896            ("X" . (lambda () (interactive) (mu4e-mark-execute-all t))))
3897  
3898    :custom
3899    (mu4e-headers-include-related nil)
3900  
3901    (mu4e-headers-date-format "%F" "ISO format yyyy-mm-dd")
3902    (mu4e-headers-results-limit -1)  ; For now, needed to have related search results
3903    (mu4e-headers-visible-lines 20)  ; Default of 10 is a bit small
3904    (mu4e-headers-auto-update nil)   ; Prevent confusion
3905  
3906    ;; Make header configuration explicit
3907    (mu4e-headers-fields '((:flags . 6)
3908                           (:human-date . 12)
3909                           (:mailing-list . 10)
3910                           (:from . 22)
3911                           (:subject . nil)))
3912    (mu4e-use-fancy-chars t)
3913  
3914  
3915    ;; Define marks, possibly redundant, but I'd like to see them here explicitly
3916    ;; Note that these are not custom vars, why not?? (they wont work as :custom entries)
3917    :config
3918    (setq mu4e-headers-attach-mark    '("a" . "📎 "))
3919    (setq mu4e-headers-calendar-mark  '("c" . "📅"))
3920    (setq mu4e-headers-draft-mark     '("D" . "🚧 "))
3921    (setq mu4e-headers-encrypted-mark '("x" . "🔑 "))
3922    (setq mu4e-headers-flagged-mark   '("F" . "🚩 "))
3923    (setq mu4e-headers-list-mark      '("s" . "🔈"))
3924    (setq mu4e-headers-new-mark       '("N" . "✨ "))
3925    (setq mu4e-headers-passed-mark    '("P" . "↪ "))
3926    (setq mu4e-headers-personal-mark  '("p" . "👨"))
3927    (setq mu4e-headers-replied-mark   '("R" . "↩ "))
3928    (setq mu4e-headers-seen-mark      '("S" . " "))
3929    (setq mu4e-headers-signed-mark    '("s" . "🖊 "))
3930    (setq mu4e-headers-trashed-mark   '("T" . "🗑️"))
3931    (setq mu4e-headers-unread-mark    '("u" . "📩 "))
3932  
3933    ;; Add one specifically for marking as junk
3934    (add-to-list 'mu4e-marks
3935                 '(junk
3936                   :char ("J" . "💀") :prompt "Mark as junk"
3937                   :show-target (lambda (dyn-target) "Junk")
3938                   :ask-target (lambda () mrb/mu4e-junk-folder)
3939                   :action (lambda (docid msg target)
3940                             (mu4e--server-move
3941                              docid
3942                              (mu4e--mark-check-target target) "+S-N-u"))))
3943  
3944    ;; Let mu4e define functions for it
3945    (mu4e~headers-defun-mark-for junk)
3946    (mu4e--view-defun-mark-for junk)
3947  
3948  #+end_src
3949  
3950  **** Message view
3951  After selecting a message in the header view, a message view will open.
3952  
3953  #+name: mu4e-view-config
3954  #+begin_src emacs-lisp :tangle no
3955    :bind ( :map mu4e-view-mode-map
3956            ("s" . mu4e-view-mark-for-junk)
3957            ("r" . mu4e-compose-reply)
3958            ("A" . mu4e-view-mark-for-refile)
3959            ("X" . (lambda () (interactive) (mu4e~view-in-headers-context (mu4e-mark-execute-all t)))))
3960  
3961    :custom
3962    (mu4e-view-use-old nil)
3963    (mu4e-view-show-images t)           ; This creates connections, possible privacy issue?
3964    (mu4e-view-image-max-width 800)
3965    (mu4e-view-image-max-height 600)
3966    (mu4e-view-show-addresses t)        ; Show actual addresses instead of names
3967  
3968    (mu4e-view-actions '(("c - capture message" . mu4e-action-capture-message)
3969                         ("v - view in browser" . mu4e-action-view-in-browser)
3970                         ("s - save attachments" . mu4e-view-save-attachments)
3971                         ("t - show this thread" . mu4e-action-show-thread)))
3972  
3973  #+end_src
3974  **** Addon packages
3975  There are quite a few mu4e packages out there. Let's load them in one group for now, so it will be easy to /disable plugins/ if we need to.
3976  #+name: mu4e-packages
3977  #+begin_src emacs-lisp :tangle no
3978    :config
3979    ;; Show an alert when new mails come in, this relies on mu4e being active though
3980    (use-package mu4e-alert
3981      :hook (after-init . mu4e-alert-enable-notifications)
3982      :config
3983      ;; Just show subjects, not counts
3984      (setq mu4e-alert-email-notification-types '(subjects))
3985      (mu4e-alert-set-default-style 'libnotify))
3986  
3987    ;; Support icalendar Yes/No/Maybe reactions to calendar invites
3988    ;; TODO not needed anymore now?
3989    (use-package mu4e-icalendar
3990      :config
3991      (mu4e-icalendar-setup))
3992  
3993    (use-package mu4e-query-fragments)  ; Support reusable query pieces '%junk' etc.
3994    (use-package mu4e-jump-to-list)     ; Shortcut key 'l' to directly focus on list threads
3995  
3996    ;; Patch highlighting inside messages
3997    (use-package message-view-patch
3998      :after (magit)
3999      :hook (gnus-part-display . message-view-patch-highlight))
4000  
4001    (use-package org-mime
4002      :custom
4003      (org-mime-use-property-inheritance t))
4004  
4005    (use-package autocrypt
4006      ;; autocrypt-accounts is set in personal.el
4007      )
4008  
4009                                            ;(use-package org-mu4e :after org)
4010  #+end_src
4011  
4012  *** Sieve
4013  Sieve is used on the server to filter our mail and this is accessed by the sieve-manage package. For some reason I can't get the '^M' characters out of the process, so here's a hack for that.
4014  
4015  #+begin_src emacs-lisp
4016    (use-package sieve
4017      :straight (:type built-in)
4018      :init
4019      (defun dos2unix ()
4020        "Replace DOS eolns CR LF with Unix eolns CR"
4021        (interactive)
4022        (goto-char (point-min))
4023        (while (search-forward "\r" nil t) (replace-match "")))
4024      :commands sieve-manage
4025      :hook (sieve-mode . dos2unix)
4026      :custom
4027      (sieve-manage-authenticators '(plain digest-md5 cram-md5 scram-md5 ntlm login)))
4028  #+end_src
4029  
4030  which basically goes over the whole sieve script and removes the '^M' characters from the buffer
4031  ** Elfeed
4032  Elfeed handles my RSS feeds, which I specify using an orgmode file =feeds.org=
4033  
4034  #+begin_src emacs-lisp
4035    (use-package notifications
4036      :straight (:type built-in))
4037  
4038    (use-package elfeed
4039      :after notifications
4040      :commands elfeed
4041      :bind (("C-c f" . 'elfeed)
4042             :map elfeed-show-mode-map
4043             ("w" . 'mrb/elfeed-show-toggle-watchlater)
4044             ("v" . 'mrb/elfeed-play-with-mpv)
4045             :map elfeed-search-mode-map
4046             ("v" . 'mrb/elfeed-play-with-mpv)
4047             ("w" . 'mrb/elfeed-search-toggle-watchlater))
4048      :init
4049      (setf url-queue-timeout 30)
4050      ;; elfeed-db-directory is set in personal.el
4051  
4052      :custom-face
4053      (elfeed-search-tag-face ((t (:foreground "#a3be8c"))))
4054      (elfeed-search-feed-face ((t (:foreground "#ebcb8b"))))
4055      (elfeed-search-date-face ((t (:foreground "#88c0d0"))))
4056      :config
4057      (defun mrb/elfeed-search-toggle-tag(tag)
4058        (let ((entries (elfeed-search-selected)))
4059          (cl-loop for entry in entries do
4060                   (if (elfeed-tagged-p tag entry)
4061                       (elfeed-untag entry tag)
4062                     (elfeed-tag entry tag)))
4063          (mapc #'elfeed-search-update-entry entries)
4064          (unless (use-region-p) (forward-line))))
4065  
4066      (defun mrb/elfeed-search-toggle-watchlater()
4067        (interactive)
4068        (mrb/elfeed-search-toggle-tag 'watchlater))
4069  
4070      (defun mrb/elfeed-show-toggle-tag(tag)
4071        (interactive)
4072        (if (elfeed-tagged-p tag elfeed-show-entry)
4073            (elfeed-show-untag tag)
4074          (elfeed-show-tag tag)))
4075  
4076      (defun mrb/elfeed-show-toggle-watchlater()
4077        (interactive)
4078        (mrb/elfeed-show-toggle-tag 'watchlater))
4079  
4080      ;; umpv maintains a playlist, so adding more videos wil automatically queue
4081      (defun mrb/elfeed-play-with-mpv ()
4082        "Play elfeed link in mpv"
4083        (interactive)
4084        (notifications-notify
4085         :title "Elfeed action"
4086         :body "Playing video with MPV"
4087         :app-name "Elfeed")
4088  
4089        (start-process "elfeed-mpv" nil
4090                       "~/bin/umpv"
4091                       (elfeed-entry-link (elfeed-search-selected t))))
4092  
4093  
4094      ;; New entry hook allows meta information manipulation
4095      ;; without directly having to change elfeed-feeds
4096      (add-hook 'elfeed-new-entry-hook
4097                (elfeed-make-tagger :feed-url "youtube\\.com"
4098                                    :add '(video youtube)))
4099      (add-hook 'elfeed-new-entry-hook
4100                (elfeed-make-tagger :feed-url "vimeo\\.com"
4101                                    :add '(video vimeo))))
4102  
4103  #+end_src
4104  
4105  Elfeed allows to interactively subscribe to a feed (defaulting to what is in the clipboard. Managing elfeed-feeds as a variable is kinda clumsy, although very flexible. There is a middle ground which fits me even better. At the cost of the bare-bone flexibility of having the feeds directly in lisp, I'm using an orgmode file to record the feeds i want.
4106  
4107  #+begin_src emacs-lisp
4108    (use-package elfeed-org
4109      :after elfeed
4110      :config
4111      (setq rmh-elfeed-org-files (list (concat elfeed-db-directory "feeds.org")))
4112      (elfeed-org))
4113  #+end_src
4114  
4115  Quite a few of my feeds are youtube channels, the =elfeed-tube= package has some extra features for reading those feeds
4116  
4117  #+begin_src elisp
4118    (use-package elfeed-tube
4119      :after elfeed
4120      :config
4121      (elfeed-tube-setup)
4122  
4123      :bind (:map elfeed-show-mode-map
4124                  ("F" . elfeed-tube-fetch)
4125                  ([remap save-buffer] . elfeed-tube-save)
4126                  :map elfeed-search-mode-map
4127                  ("F" . elfeed-tube-fetch)
4128                  ([remap save-buffer] . elfeed-tube-save)))
4129  #+end_src
4130  
4131  ** IRC
4132  
4133  I used to run a weechat relay on a vps for IRC. The main reason for this was that the relay allowed multiple clients to be active at the same time an have at least an attempt at saving the state betwee multiple devices.
4134  
4135  The major downside is that a special weechat client is needed to make this work, which is not the same as a standard IRC client. This made the choice for clients very limited, but luckily there was an emacs client.
4136  
4137  After using it some time and maintaining some local patches; the main program is largely unmaintained, it was time to look for an alternative.
4138  
4139  While having the weechat relay is very nice, I also want to have a /"standard"/ IRC configuration where everything is happening in the client, possibly augmented by having an IRC bouncer to maintain persistent connections. [[https://wiki.znc.in/ZNC][ZNC]] might almost have the featureset to do what weechat does, or very closely to it.
4140  
4141  ERC seems to be the most used IRC client for emacs and included with it, so it would make sense to use it. I like the simplicity of [[https://github.com/emacs-circe/circe/wiki][circe]] though, so I opted to create a config for that.
4142  
4143  #+begin_src emacs-lisp
4144    (use-package circe
4145      :commands (circe mrb/chat)
4146      :custom
4147      ;; circe-default-nick and circe-default-user are set in personal.el
4148      (circe-network-options
4149       `(("libera"
4150          :host "chat.hsdev.com" :port 1066
4151          :pass ,(concat "mrb@emacs/libera:"
4152                         (password-store-get "chat.hsdev.com"))
4153          :channels ("#emacs"
4154                     "#talos-workstation"
4155                     "#vikings"))
4156         ("oftc"
4157          :host "chat.hsdev.com" :port 1066
4158          :pass ,(concat "mrb@emacs/oftc:"
4159                         (password-store-get "chat.hsdev.com"))
4160          :channels ("#osm"))))
4161  
4162      ;; Simplify and improve formatting, remove some details
4163      (circe-format-say "{nick:10s}: {body}" "right align nicks at 10th pos")
4164      (circe-format-self-say "{nick:10s}: {body}")
4165      (circe-format-server-part "*** Part: {nick} left {channel}")
4166      (circe-format-server-quit-channel "*** Quit: {nick} left {channel}")
4167      (circe-format-server-quit "*** Quit: {nick}")
4168      (circe-format-server-join "*** Join: {nick}")
4169      (circe-format-server-rejoin "*** Re-joined: {nick}")
4170      (lui-time-stamp-position 'right-margin)
4171      (lui-fill-type nil)
4172      (circe-reduce-lurker-spam t "dont show notices for non talkers")
4173      (circe-default-part-message "" "no parting reason")
4174      (circe-default-quit-message "" "no quitting reason")
4175  
4176      :custom-face
4177      (circe-prompt-face ((t (:foreground ,mrb/cursor-color :height 1.3))))
4178      (circe-originator-face ((t (:height 0.8))))
4179      (lui-time-stamp-face ((t (:height 0.8))))
4180      (lui-track-bar ((t (:background "LawnGreen"))))
4181  
4182      :config
4183      (enable-lui-track)                   ; show indicator to where has been read
4184      (enable-circe-display-images)        ; turn links to images into images
4185      (enable-circe-color-nicks)           ; color nicks
4186  
4187      ;; Gather buffer local and line UI settings
4188      (defconst mrb/lui-nick-column-width 12
4189        "Width allocated for IRC nicknames, matching `circe-format-say'.")
4190  
4191      (defconst mrb/lui-timestamp-width 7
4192        "Width of timestamp margin, sized for [hh:mm].")
4193  
4194      (defun mrb/lui-setup ()
4195        "Configure buffer-local display settings for lui IRC buffers."
4196        (setq fringes-outside-margins t
4197              right-margin-width mrb/lui-timestamp-width
4198              word-wrap t
4199              wrap-prefix (make-string mrb/lui-nick-column-width ?\s))
4200        (setf (cdr (assoc 'continuation fringe-indicator-alist)) nil))
4201      (add-hook 'lui-mode-hook 'mrb/lui-setup)
4202  
4203      ;; Set the prompt properly on entering the chat mode
4204      (defun mrb/set-circe-prompt ()
4205        (lui-set-prompt (propertize (concat "[" circe-chat-target "]➜ ")
4206                                    'face 'circe-prompt-face)))
4207      (add-hook 'circe-chat-mode-hook 'mrb/set-circe-prompt)
4208  
4209      ;; Make a convenient connect function
4210      ;; TODO make this re-entrant
4211      (defun mrb/chat()
4212        (interactive)
4213        (circe "libera")
4214        (circe "oftc")))
4215  #+end_src
4216  
4217  ** Mastodon
4218  Not messaging or chatting perse, but a micro blog social network. The 'capture a toot' is hot-keyed through =xbindkeys= like other capture commands.
4219  
4220  #+begin_src emacs-lisp
4221    ;; Mastodon in emacs
4222    (use-package mastodon
4223      :straight (mastodon :host codeberg :repo "martianh/mastodon.el"
4224                          :branch "develop")
4225      ;; mastodon-instance-url and mastodon-active-user are set in personal.el
4226      :config
4227      ;; Convenience function
4228      (defun mrb/capture-toot()
4229        (interactive)
4230        (mastodon-toot)))
4231  #+end_src
4232  
4233  #+begin_src sh :exports code :tangle ~/bin/capture-toot.sh :shebang #!/bin/sh
4234    edit-noframe --eval '(mrb/capture-toot)'
4235  #+end_src
4236  
4237  * Development settings
4238  Some settings which aid in development tasks.
4239  
4240  ** Generic
4241  Let's start with some generic development related settings and packages
4242  #+begin_src emacs-lisp
4243    (use-package request
4244      :custom
4245      (request-log-level 'verbose))
4246  
4247    (use-package cmake-mode)                ; adjusts auto-mode-alist on load
4248  
4249    (use-package yaml-mode)                 ; used by ansible, magit(?)
4250  #+end_src
4251  
4252  For most of the languages I use (Bash, python, C, Go, HTML Haskell, Lua), the Language Server Protocol seems to be supported by =eglot=, which is now a built-in package, so I'm setting this up generally here and enable it for each language I'm using if it is supported.
4253  
4254  #+begin_src emacs-lisp
4255    (use-package eglot
4256      :straight (:type built-in))
4257  #+end_src
4258  
4259  Diff and merge tools for version control:
4260  
4261  #+begin_src emacs-lisp
4262    (use-package diff-mode
4263      :straight (:type built-in)
4264      :mode "\\.patch\\'"
4265      :custom-face
4266      ;; Unified diff faces — diff-mode is the single source of truth,
4267      ;; magit, ediff and smerge inherit from it.
4268      (diff-added                     ((t (:foreground "#a3be8c" :background "#2e4a2e"))))
4269      (diff-removed                   ((t (:foreground "#bf616a" :background "#4a2e2e"))))
4270      (diff-refine-added              ((t (:foreground "#eceff4" :background "#5a7a52"))))
4271      (diff-refine-removed            ((t (:foreground "#eceff4" :background "#943b44"))))
4272      (diff-header                    ((t (:foreground "#e5e9f0" :background "#3b4252"))))
4273      (diff-hunk-header               ((t (:foreground "#e5e9f0" :background "#434c5e"))))
4274      (diff-context                   ((t (:foreground "#d8dee9"))))
4275      )
4276  
4277    (use-package ediff
4278      :straight (:type built-in)
4279      :custom-face
4280      ;; ediff — A = removed/old, B = added/new
4281      (ediff-current-diff-A           ((t (:inherit diff-removed))))
4282      (ediff-current-diff-B           ((t (:inherit diff-added))))
4283      (ediff-fine-diff-A              ((t (:inherit diff-refine-removed))))
4284      (ediff-fine-diff-B              ((t (:inherit diff-refine-added))))
4285      (ediff-even-diff-A              ((t (:background "#3b4252"))))
4286      (ediff-even-diff-B              ((t (:background "#3b4252"))))
4287      (ediff-odd-diff-A               ((t (:background "#434c5e"))))
4288      (ediff-odd-diff-B               ((t (:background "#434c5e")))))
4289  
4290    (use-package smerge-mode
4291      :straight (:type built-in)
4292      :custom-face
4293      (smerge-upper                   ((t (:inherit diff-removed))))
4294      (smerge-lower                   ((t (:inherit diff-added))))
4295      (smerge-refined-added           ((t (:inherit diff-refine-added))))
4296      (smerge-refined-removed         ((t (:inherit diff-refine-removed)))))
4297  #+end_src
4298  
4299  Make our source look a bit more attractive by enabling =prettify-symbol-mode= in all programming modes
4300  
4301  #+begin_src emacs-lisp
4302    (use-package prog-mode
4303      :straight (:type built-in)
4304      ;; TODO move hooks to their defining packages?
4305      :hook ((prog-mode                     ; all modes that derive from prog-mode
4306              lisp-interaction-mode) . mrb/prettify-symbols)
4307      :init
4308  
4309      ;; TODO Move this to visual?
4310      ;; TODO this is a bit messy, do this in mode packages and per mode
4311      (defun mrb/prettify-symbols ()
4312        (interactive)
4313        ;; set buffer local variable to map the symbols
4314        (setq prettify-symbols-alist
4315              '(("lambda" . ?λ)
4316                                            ;("lambda*" . (?λ (Br . Bl) ?*)) ; just in scheme really
4317                                            ;("-lambda" . (?- (Br . Bl) ?λ))
4318                                            ;("map"    . ?↦)     ; this one is a bit of a pain actually
4319                ("->"     . ?⟶)
4320                ("<-"     . ?⟵)
4321                ("=>"     . ?⟹)
4322                                            ;("#t"     . ?⟙)     ; bad idea
4323                                            ;("#f"     . ?⟘)     ; bad idea
4324                ("|>"     . ?▷)
4325                ("<|"     . ?◁)
4326                ("->>"    . ?↠)
4327                ("<="     . ?≤)
4328                (">="     . ?≥)))
4329        (prettify-symbols-mode)))
4330  #+end_src
4331  
4332  In general, the syntax highlighting of emacs is sufficient, but is based on defining regular expression to match language contents to color them. There is a better way to do this; based on generating a semantic tree representation of the language in question. There is a tool called [[https://tree-sitter.github.io/][tree-sitter]] which does this.
4333  
4334  The main advantages of the method that tree-sitter uses are:
4335  - the same tree representation is used for all languages, so adding new languages is relatively easy
4336  - using the representation for syntax highlighting is just one application: code folding or semantic extending a region, all of which are available in emacs in other ways, is a lot better using the tree-sitter method.
4337  - it's super fast. fast enough to keep up with typing.
4338  
4339  As of emacs 29 the treesit package is built-in, so let's use that in any case for highlighting where it is possible. I think over time the mapping of the modes below will just disappear and everything will be treesitter based.
4340  
4341  #+begin_src emacs-lisp
4342    ;; Use the built-in treesit and load all language grammars
4343    (use-package treesit
4344      :straight (:type built-in)
4345      :config
4346      (setq treesit-extra-load-path
4347            `(,(mrb/emacs-file "var/tree-sitter/langs")))
4348      ;; Replace relevant modes with the treesitter variant
4349      (dolist (mode
4350               '((bash-mode       . bash-ts-mode)
4351                 (c-mode          . c-ts-mode)
4352                 (c++-mode        . c++-ts-mode)
4353                 (css-mode        . css-ts-mode)
4354                 (dockerfile-mode . dockerfile-ts-mode)
4355                 (go-mode         . go-ts-mode)
4356                 (javascript-mode . js-ts-mode)
4357                 (js-json-mode    . json-ts-mode)
4358                 (markdown-mode   . markdown-ts-mode)
4359                 (python-mode     . python-ts-mode)
4360                 (typescript-mode . typescript-ts-mode)
4361                 (yaml-mode       . yaml-ts-mode)))
4362        (add-to-list 'major-mode-remap-alist mode)))
4363  #+end_src
4364  
4365  Code folding via tree-sitter. Provides semantic-aware folding (on functions, classes, blocks) across all tree-sitter modes.
4366  
4367  #+begin_src emacs-lisp
4368      (use-package treesit-fold
4369        :ensure t
4370        :demand t
4371        :custom-face
4372        ;; Treesit-fold replacement text (ellipsis when code is folded)
4373        (treesit-fold-replacement-face  ((t (:foreground "white" :weight bold))))
4374  
4375        ;; Treesit-fold fringe indicators (the triangles in the fringe)
4376        ;; Use subtle grey to reduce visual noise from range indicators
4377        (treesit-fold-fringe-face ((t (:foreground "#81a1c1"))))
4378  
4379        :config
4380        ;; Define C-c f as a prefix for folding commands
4381        (define-prefix-command 'treesit-fold-prefix-command)
4382        (global-set-key (kbd "C-c t") 'treesit-fold-prefix-command)
4383        (define-key treesit-fold-prefix-command (kbd "t") 'treesit-fold-toggle)
4384        (define-key treesit-fold-prefix-command (kbd "c") 'treesit-fold-close-all)
4385        (define-key treesit-fold-prefix-command (kbd "o") 'treesit-fold-open-all)
4386        (define-key treesit-fold-prefix-command (kbd "r") 'treesit-fold-open-recursively)
4387  
4388        (global-treesit-fold-mode 1)
4389        (global-treesit-fold-indicators-mode 1)
4390  
4391        ;; Use full rendering with filtering to remove range indicators
4392        (setq treesit-fold-indicators-render-method 'full)
4393  
4394        ;; Fix YAML folding issue where block_sequence_item can return ranges with nil values
4395        ;; Override the YAML parser to handle missing key/value nodes gracefully
4396        ;; See: https://github.com/emacs-tree-sitter/treesit-fold/issues/48
4397  
4398        (require 'treesit-fold-parsers)
4399        (let ((yaml-parsers (treesit-fold-parsers-yaml)))
4400          ;; Wrap the block_sequence_item handler to validate beg/end values
4401          (setf (alist-get 'block_sequence_item yaml-parsers)
4402                (lambda (node offset)
4403                  (let* ((key (treesit-search-subtree
4404                               node
4405                               (lambda (n)
4406                                 (string= "key" (treesit-node-field-name n)))))
4407                         (value (treesit-search-subtree
4408                                 node
4409                                 (lambda (n)
4410                                   (string= "value" (treesit-node-field-name n)))))
4411                         ;; Only compute beg if we have a valid key or value
4412                         (beg (when (or key value)
4413                                (treesit-node-end
4414                                 (if (and value (string= "block_node" (treesit-node-type value)))
4415                                     key
4416                                   value))))
4417                         (end (treesit-node-end node)))
4418                    ;; Only return a range if both beg and end are valid numbers
4419                    (when (and (numberp beg) (numberp end) beg end)
4420                      (treesit-fold--cons-add (cons beg end) offset)))))
4421          ;; Update the alist for YAML
4422          (setf (alist-get 'yaml-ts-mode treesit-fold-range-alist) yaml-parsers))
4423  
4424        ;; Define custom fringe bitmaps for fold indicators
4425        ;; Right-pointing triangle (▶) for folded code, down-pointing (▼) for open code
4426        ;; Right-pointing triangle (▶) - 16x16 fully filled
4427        (define-fringe-bitmap 'treesit-fold-indicators-fr-plus
4428          (vector #b1000000000000000
4429                  #b1100000000000000
4430                  #b1111000000000000
4431                  #b1111110000000000
4432                  #b1111111100000000
4433                  #b1111111111000000
4434                  #b1111111111110000
4435                  #b1111111111111100
4436                  #b1111111111111100
4437                  #b1111111111110000
4438                  #b1111111111000000
4439                  #b1111111100000000
4440                  #b1111110000000000
4441                  #b1111000000000000
4442                  #b1100000000000000
4443                  #b1000000000000000)
4444          16 16)
4445        ;; Down-pointing triangle (▼) - 16x16
4446        (define-fringe-bitmap 'treesit-fold-indicators-fr-minus-tail
4447          (vector #b1111111111111111
4448                  #b1111111111111111
4449                  #b0111111111111110
4450                  #b0011111111111100
4451                  #b0001111111111000
4452                  #b0000111111110000
4453                  #b0000011111100000
4454                  #b0000001111000000
4455                  #b0000000110000000
4456                  #b0000000100000000
4457                  #b0000000000000000
4458                  #b0000000000000000
4459                  #b0000000000000000
4460                  #b0000000000000000
4461                  #b0000000000000000
4462                  #b0000000000000000)
4463          16 16)
4464  
4465        ;; Define empty bitmaps for range indicators to hide vertical lines
4466        (define-fringe-bitmap 'treesit-fold-indicators-fr-center
4467          (vector #b0000000000000000
4468                  #b0000000000000000
4469                  #b0000000000000000
4470                  #b0000000000000000
4471                  #b0000000000000000
4472                  #b0000000000000000
4473                  #b0000000000000000
4474                  #b0000000000000000
4475                  #b0000000000000000
4476                  #b0000000000000000
4477                  #b0000000000000000
4478                  #b0000000000000000
4479                  #b0000000000000000
4480                  #b0000000000000000
4481                  #b0000000000000000
4482                  #b0000000000000000)
4483          16 16)
4484  
4485        (define-fringe-bitmap 'treesit-fold-indicators-fr-end-left
4486          (vector #b0000000000000000
4487                  #b0000000000000000
4488                  #b0000000000000000
4489                  #b0000000000000000
4490                  #b0000000000000000
4491                  #b0000000000000000
4492                  #b0000000000000000
4493                  #b0000000000000000
4494                  #b0000000000000000
4495                  #b0000000000000000
4496                  #b0000000000000000
4497                  #b0000000000000000
4498                  #b0000000000000000
4499                  #b0000000000000000
4500                  #b0000000000000000
4501                  #b0000000000000000)
4502          16 16)
4503  
4504        (define-fringe-bitmap 'treesit-fold-indicators-fr-end-right
4505          (vector #b0000000000000000
4506                  #b0000000000000000
4507                  #b0000000000000000
4508                  #b0000000000000000
4509                  #b0000000000000000
4510                  #b0000000000000000
4511                  #b0000000000000000
4512                  #b0000000000000000
4513                  #b0000000000000000
4514                  #b0000000000000000
4515                  #b0000000000000000
4516                  #b0000000000000000
4517                  #b0000000000000000
4518                  #b0000000000000000
4519                  #b0000000000000000
4520                  #b0000000000000000)
4521          16 16)
4522  
4523    )
4524  #+end_src
4525  
4526  To have some control over the environment for specific projects I use [[http://direnv.net][direnv]], so let's install emacs support for it as well.
4527  
4528  #+begin_src emacs-lisp
4529    (use-package envrc
4530      :config
4531      (envrc-global-mode))
4532  
4533  #+end_src
4534  ** Reference & documentation
4535  While emacs itself has a very nicely documentation system, I still don't understand why this is not interpolated into every other mode and or language.
4536  
4537  Example: =C-h-f= in lisp-mode describes the function to me. If I am in a python file, I want it to do exactly the same, but for the python function.  There's a page on emacs wiki dealing with this a bit: https://www.emacswiki.org/emacs/Context_sensitive_help
4538  
4539  There's a package =ghelp= (/generic help/ I guess) which promises to do what I want, but I can't get it installed properly with =use-package=. That package can use the =helpful= package as back-end which is also helpful on it own, so I'm installing that anyways and replace the internal help commands from emacs with them.
4540  
4541  #+begin_src emacs-lisp
4542    (use-package helpful
4543      :bind (("C-h f" . #'helpful-callable)
4544             ("C-h h" . #'helpful-at-point) ; I don't care about the emacs 'hello' file
4545             ("C-h v" . #'helpful-variable)
4546             ("C-h k" . #'helpful-key)))
4547  
4548  #+end_src
4549  
4550  Anyways, here's what I have configured related to reference information and documentation
4551  
4552  *** Context sensitive
4553  Regardless of (programming) language there's certain information that's needed on the spot. These are the function signatures, the specific syntax when typing Math symbols, orgmode oddities etc.
4554  
4555  I want that type of information directly at hand. One part of the solution is [[https://www.emacswiki.org/emacs/ElDoc][ElDoc]]. This shows documentation in the echo area for a language. ElDoc is built into Emacs so that's a good start. Let's enable it for the modes that have support for it.
4556  
4557  #+begin_src emacs-lisp
4558    (dolist (mode-hook '(emacs-lisp-mode-hook
4559                        lisp-interaction-mode-hook
4560                        ielm-mode-hook
4561                        python-mode))
4562      (add-hook mode-hook 'eldoc-mode))
4563    (diminish 'eldoc-mode)
4564  #+end_src
4565  
4566  The location of the eldoc information in the message area is a bit far away from the editing point. I have looked at eldoc-overlay and others to resolve that, but haven't found a satisfying solution yet.
4567  
4568  *** Reference information
4569  If the inline documentation presented by ElDoc is not sufficient, I want a way to spawn reference documentation based on the context.
4570  
4571  The closest thing I found is =zeal= or =dash= which allow docsets to be searched. There is a GUI and helm-dash is an interface to the docsets for emacs. Typically I install docsets through the GUI or manually (the folder names are sometimes not the same when I use helm-dash to install docsets). This solution gives me at least /access/ to reference information for most of the programming languages I use. The direct access to the sources is missing, or I don't know how to do this yet.
4572  
4573  #+begin_src emacs-lisp
4574    ;; Unify reference documentation with dash
4575    (use-package helm-dash
4576      :after helm
4577      :commands (helm-dash helm-dash-at-point)
4578      :config
4579      ;; Make sure docset directory exists
4580      (make-directory helm-dash-docsets-path t)
4581      :custom
4582      (dash-docs-enable-debugging nil)
4583      (helm-dash-browser-func 'eww "Within dash, keep browser links within emacs")
4584      (helm-dash-common-docsets (helm-dash-installed-docsets)))
4585  #+end_src
4586  
4587  Dash show documentation inside emacs, which is my preferred method. There are times however I need the desktop GUI Zeal. For one, it's a lot easier to see which docsets are installed and manage them.
4588  
4589  #+begin_src emacs-lisp
4590    ;; Global keybinding ot open documentation
4591    ;; This should probably be somewhere else
4592    (use-package zeal-at-point
4593      :bind
4594      ("s-d" . zeal-at-point))
4595  #+end_src
4596  
4597  *** RFC document reader
4598  I often need to consult RFC documents, so not having to leave emacs for that and be able to use all tools on RFC documents is very helpful.
4599  
4600  #+begin_src emacs-lisp
4601    ;; Special mode to read rfc documents locally
4602    (use-package rfc-mode
4603      :custom
4604      ;; rfc-mode-directory is set in personal.el
4605      (rfc-mode-index-path (concat rfc-mode-directory"rfc-index.txt")))
4606  #+end_src
4607  
4608  *** Man pages
4609  There's a builtin package to read man pages, let's make it explicit.
4610  
4611  #+begin_src emacs-lisp
4612    (use-package man
4613      :straight (:type built-in)
4614      :custom-face
4615      ;; Man pages inside emacs
4616      (Man-overstrike ((t (:foreground "#bf616a" :weight bold))))
4617      (Man-underline ((t (:foreground "#a3be8c" :weight bold)))))
4618  #+end_src
4619  
4620  Not really man pages as such, but meant as their /advanced/ cousin for GNU programs is the info format.
4621  
4622  #+begin_src emacs-lisp
4623    (use-package info
4624      :straight (:type built-in)
4625      :bind (:map Info-mode-map ("C-o" . 'casual-info-tmenu)))
4626  #+end_src
4627  
4628  *** Epub documents
4629  Not often, but increasingly so, reference books are in epub format. So, for that we need a reader package. =nov= seems to be the best option
4630  
4631  #+begin_src emacs-lisp
4632    (use-package nov
4633      :mode ("\\.epub" . nov-mode))
4634  #+end_src
4635  ** Coding styles
4636  Different projects use different coding styles. The ones I need I'll gather here for now.
4637  
4638  My personal style will be called =mrb= and basing it on the =linux= style.
4639  #+begin_src emacs-lisp
4640  
4641    ;; Basing it on k&r, no real reason, does it really matter?
4642    (c-add-style "mrb"
4643                 '("k&r"
4644                   (indent-tabs-mode . nil)
4645                   (c-basic-offset . 2)
4646                   (c-cleanup-list . (scope-operator
4647                                      space-before-funcall))))
4648  
4649    ;; Make my style the default
4650    (setq c-default-style
4651          '((java-mode . "java")
4652            (awk-mode  . "awk")
4653            (other     . "mrb")))
4654  
4655    ;; EditorConfig support, not used much by the looks of it
4656    (use-package editorconfig
4657      :disabled t
4658      :diminish
4659      :config
4660      (editorconfig-mode 1))
4661  #+end_src
4662  ** Language support
4663  *** Python
4664  Just enabling eglot for now.
4665  
4666  #+begin_src emacs-lisp
4667    (use-package python
4668      :straight (:type built-in)
4669      :hook (python-mode . eglot-ensure))
4670  #+end_src
4671  *** Haskell
4672  I am just starting out with haskell, but the two things that are probably needed in any case are a mode to use when editing Haskell source (*.hs files) and the ghc-mod package to help with completion and showing syntax errors.
4673  
4674  #+begin_src emacs-lisp
4675    (use-package haskell-mode
4676      :hook (;(haskell-mode . interactive-haskell) ;; Produces elc load error
4677             (haskell-mode . turn-on-haskell-doc)
4678             (haskell-mode . haskell-indentation-mode))
4679  
4680      :mode "\‌\.hs\\'"
4681      :custom
4682      (haskell-font-lock-symbols t)
4683      (haskell-interactive-popup-errors nil)
4684      (haskell-process-type 'stack-ghci)
4685      :config
4686      ;; Replace ⇒ with ⇉
4687      (delete '("=>" . "⇒") haskell-font-lock-symbols-alist)
4688      (add-to-list 'haskell-font-lock-symbols-alist '("=>" . "⇉")))
4689  #+end_src
4690  
4691  Enable the language server for haskell
4692  
4693  #+begin_src emacs-lisp
4694    (use-package lsp-haskell
4695      :after (haskell-mode lsp-mode)
4696      :hook ((haskell-mode  haskell-literate-mode) . lsp))
4697  #+end_src
4698  
4699  Because the haskell environment can be different for each project, we need a way to adjust to that for certain things. One such thing is flycheck which will give false-negatives when checking haskell files if the specific build environment is not taken into account. The package flycheck-haskell automatically adjusts flycheck.
4700  
4701  #+begin_src emacs-lisp
4702    (use-package flycheck-haskell
4703      :after flycheck
4704      :hook (flycheck-mode . flycheck-haskell-setup))
4705  #+end_src
4706  
4707  For creating web apps I use yesod, which is supported by a number of packages
4708  #+begin_src emacs-lisp
4709    ;; Yesod's html like files
4710    (use-package hamlet-mode)
4711  #+end_src
4712  *** Go
4713  #+begin_src emacs-lisp
4714    (use-package go-mode
4715      ;; TODO  godef is fairly heavy on my little machines, make optional?
4716      :ensure-system-package godef
4717  
4718      :init
4719      ;; Set up before-save hooks to format buffer and add/delete imports.
4720      ;; Make sure you don't have other gofmt/goimports hooks enabled.
4721      ;; TODO  would this not add hooks for all files that are saved??
4722      (defun lsp-go-install-save-hooks ()
4723        (add-hook 'before-save-hook #'lsp-format-buffer t t)
4724        (add-hook 'before-save-hook #'lsp-organize-imports t t))
4725      (add-hook 'go-mode-hook #'lsp-go-install-save-hooks))
4726  #+end_src
4727  *** Rust
4728  #+begin_src emacs-lisp
4729    (use-package rust-mode)
4730  #+end_src
4731  *** Lisp-like
4732  There are a number of languages which are lisp-like, not in the least Emacs lisp itself. It makes sense to group the configuration for these languages together. In the first part there will be configuration for all lisp-like languages (like the =puni= configuration for example). For each specific implementation there may be augmentations to the config, or even a completely separate.
4733  
4734  For all of those languages, I want to use =puni= which makes editing, longer term, a lot more productive. For autopairing, which =puni= does not do itself unlike =paredit=, I use the built-in =electric-pair-mode= Both of these support multiple modes, so are really generic packages, but lisp-like modes are the modes typically benefitting the most from them.
4735  
4736  #+begin_src emacs-lisp
4737    (use-package puni
4738      :init
4739      (puni-global-mode)                    ; perhaps too much, we'll see
4740  
4741      :bind ( :map puni-mode-map
4742              ("C-<right>" . puni-slurp-forward) ; foo (bar|) baz  ->  foo (bar| baz)
4743              ("C-<left>"  . puni-barf-forward)  ; foo (bar| baz)  ->  foo (bar|) baz
4744              )
4745  
4746      :hook ((org-mode . puni-disable-puni-mode)
4747             (prog-mode . electric-pair-local-mode)))
4748  #+end_src
4749  
4750  Evaluating an expression with the cursor after it is often used, I'd like being able to do the same when the cursor is in front of such an expression
4751  
4752  #+begin_src emacs-lisp
4753    ;; The reciprocate of C-x C-e
4754    ;; As in:  _cursor_(...eval what is in here...)
4755    (defun mrb/eval-next-sexp ()
4756      (interactive)
4757      (save-excursion
4758        (forward-sexp)
4759        (eval-last-sexp nil)))
4760  #+end_src
4761  
4762  Not sure how to bind this though. I'd like it close to C-x C-e obviously
4763  
4764  When evaluating expressions, the result is way down in the status area. I would like to have it inline in the buffer in an overlay. The package =cider= does this, but I have no need for the complete clojure environment. There is a specific package for emacs-lisp though, called =eros=
4765  
4766  #+begin_src emacs-lisp
4767    (use-package eros
4768      :config
4769      (eros-mode 1))
4770  #+end_src
4771  
4772  **** Scheme
4773  The generic scheme support is the builtin scheme package, let's tell it that I use guile as my default scheme program.
4774  
4775  #+begin_src emacs-lisp
4776    (use-package scheme
4777      :straight (:type built-in)
4778      :custom
4779      (scheme-program-name "/usr/bin/guile"))
4780  #+end_src
4781  
4782  [[https://www.nongnu.org/geiser][Geiser]] is, among other things, a repl for schemes. I try to use one, mostly for educational purposes. I chose guile because it also claims to support =emacs-lisp= and might be the future vm on which emacs-lisp will run.
4783  
4784  #+begin_src emacs-lisp
4785    ;; Specific implementations depend on geiser
4786    (use-package geiser-guile
4787      :straight (:host gitlab :repo "emacs-geiser/guile")
4788      :defer t
4789      :commands (run-guile)
4790      :hook ((geiser-repl-mode . mrb/prettify-symbols))
4791  
4792      :custom
4793      (geiser-guile-binary "/usr/bin/guile")
4794      (geiser-active-implementations '(guile))
4795      (geiser-default-implementation 'guile)
4796  
4797      ;; Do NOT evaluate on return when inside an expression
4798      (geiser-repl-send-on-return-p nil)
4799  
4800      ;; start repl and eval results inline in buffer
4801      ;; TODO I do want this eval result to be transient, not inserted!
4802      (geiser-mode-start-repl-p t)
4803      (geiser-mode-eval-last-sexp-to-buffer t)
4804      (geiser-mode-eval-to-buffer-prefix " ;;=> ")
4805  
4806      )
4807  #+end_src
4808  
4809  Similar to using eros for emacs-lisp, I use geiser-eros for languages that geiser supports.
4810  
4811  #+begin_src emacs-lisp
4812    (use-package geiser-eros
4813      :after (eros geiser)
4814      :straight '(:type git :host sourcehut :repo "sokolov/geiser-eros")
4815      :config
4816      ;; Make sure geiser does not insert eval into buffer
4817      (setq geiser-mode-eval-last-sexp-to-buffer nil)
4818      (geiser-eros-mode 1))
4819  #+end_src
4820  While we're in the scheme section, let's configure =guix= and friends, which use scheme heavily
4821  
4822  #+begin_src emacs-lisp
4823    (use-package guix
4824      :config
4825      ;; geiser-guile-load-path is set in personal.el
4826  
4827      ;; Use tempel templates
4828      (with-eval-after-load 'tempel
4829        ;; Ensure tempel-path is a list -- it may also be a string.
4830        (add-to-list 'tempel-path "~/dat/src/guix/etc/snippets/tempel/*"))
4831  
4832      ;; guix uses debbugs, but this should probably be closer to mu4e and gnus config
4833      (use-package debbugs))
4834  #+end_src
4835  **** Common lisp
4836  The defacto standard for a development environment in Emacs for common-lisp is either the =slime= or =sly= package. The latter seems a bit more modern and easier to configure, so trying that one
4837  
4838  #+begin_src emacs-lisp
4839    (use-package sly
4840      :hook ((sly-mrepl-mode . sly-mrepl-font-lock-setup))
4841  
4842      :init
4843  
4844      ;; Copy the keywords from lisp mode
4845      (defvar sly-mrepl-font-lock-keywords lisp-el-font-lock-keywords-2)
4846  
4847      ;; Set them up
4848      (setq sly-mrepl-font-lock-keywords
4849            (cons '(sly-mrepl-font-lock-find-prompt
4850                    . 'sly-mrepl-prompt-face)
4851                  sly-mrepl-font-lock-keywords))
4852      (defun sly-mrepl-font-lock-setup ()
4853        (setq font-lock-defaults
4854              '(sly-mrepl-font-lock-keywords
4855                ;; From lisp-mode.el
4856                nil nil (("+-*/.<>=!?$%_&~^:@" . "w")) nil
4857                (font-lock-syntactic-face-function
4858                 . lisp-font-lock-syntactic-face-function))))
4859  
4860      :custom
4861      ;; Prevent usage of linedit when we are inside emacs, it's just for the terminal
4862      ;; Q: How about vterm?
4863      (inferior-lisp-program "sbcl –-noinform –-no-linedit")
4864  
4865      (sly-lisp-implementations
4866       '((sbcl ("sbcl" "--core" (mrb/home-file ".sbcl/sbcl.core-for-sly")))
4867         (qlot ("qlot" "exec" "sbcl"))))
4868      (sly-net-coding-system 'utf-8-unix)
4869  
4870      :config
4871      ;; Nyxt exposes a repl we can use
4872      <<nyxt-sly-config>>
4873  
4874      ;; Correct the REPL prompt
4875      (defun sly-mrepl-font-lock-find-prompt (limit)
4876        ;; Rough: (re-search-forward "^\\w*>" limit t)
4877        (let (beg end)
4878          (when (setq beg (text-property-any
4879                           (point) limit 'sly-mrepl-prompt-face t))
4880            (setq end (or (text-property-any
4881                           beg limit 'sly-mrepl-prompt-face nil)
4882                          limit))
4883            (goto-char beg)
4884            (set-match-data (list beg end))
4885            t))))
4886  #+end_src
4887  *** Gcode
4888  I'm not in the habit of editing gcode, but every now and then I need to look at it and I want it reasonably decent. My use-case is just sending gcode to my 3D-printer which runs Marlin firmware.
4889  
4890  Using =define-generic-mode= is more than enough to define something useful for me.
4891  
4892  #+begin_src emacs-lisp
4893    (use-package generic-x
4894      :straight (:type built-in))
4895  
4896    (define-generic-mode marlin-gcode-mode
4897      '((";"))   ;; ; starts a comment
4898      (apply 'append
4899             (mapcar #'(lambda (s) (list (upcase s) (downcase s) (capitalize s)))
4900                     '("keywords?" "exist?")))
4901      '(("\\([GM]\\)[0-9]+" (1 font-lock-function-name-face))    ; code letters
4902        ("\\([ABCDEFHIJKLNOPQRSTUVWXYZ]\\)[-]?[0-9]+" (1 font-lock-string-face)) ; parameter letters
4903        ("\\([\-+]?[0-9]*\\.[0-9]+\\)" (1 font-lock-constant-face)) ; 9.9 type numbers
4904        ("\\([\-+]?[0-9]+\\)" (1 font-lock-constant-face)))         ; 999 type numbers
4905      '("\\.gcode\\'")
4906      nil
4907      "Mode for marlin g-code files.")
4908  #+end_src
4909  *** Openscad
4910  Openscad is a script based 3d parametric drawing program. Obviously I'm editing those scripts in emacs.
4911  
4912  #+begin_src emacs-lisp
4913    (use-package scad-mode
4914      :custom
4915      (scad-keywords '("return" "true" "false" "include")))
4916  #+end_src
4917  *** SQL
4918  SQL has a number of different dialects I use, depending on the database software in use.
4919  
4920  #+begin_src emacs-lisp
4921    (use-package sql
4922      :straight (:type built-in)
4923  
4924      :custom
4925      ;; sql-server is set in personal.el
4926      (sql-postgres-options '("-P" "pager=off" "-p 5434")))
4927  #+end_src
4928  ** GIT integration
4929  A common factor in all my development is the use of git. For emacs this automatically means Magit. I've used eshell in the past but that config didn't work out.
4930  
4931  *** Magit
4932  For most, if not all development work (and some other work too) I use git as the revision control system. In emacs that translates to using magit, so let's begin with bringing that in.
4933  
4934  Magit forge is a sub-module for magit which helps in managing repositories in forges (notably github and gitlab). This looks very promising, albeit slow.
4935  
4936  Features I'm interested in: issue commenting, pull requests
4937  
4938  #+begin_src emacs-lisp
4939    (use-package magit
4940      :straight (:host github :repo "magit/magit")
4941  
4942      :demand t
4943      :after (org fullframe)
4944      :commands magit-status
4945      :bind
4946      ("C-c m" . magit-status)
4947  
4948      :init
4949      (fullframe magit-status magit-mode-quit-window)
4950  
4951      :custom
4952      (magit-last-seen-setup-instructions "1.4.0")
4953      (magit-diff-refine-hunk 'all)
4954  
4955      :custom-face
4956      ;; magit — inherit base, highlight variants get brighter bg
4957      (magit-diff-added               ((t (:inherit diff-added))))
4958      (magit-diff-removed             ((t (:inherit diff-removed))))
4959      (magit-diff-added-highlight     ((t (:inherit diff-added   :background "#3b5c3b"))))
4960      (magit-diff-removed-highlight   ((t (:inherit diff-removed :background "#5c3b3b"))))
4961      (magit-diff-context             ((t (:inherit diff-context))))
4962      (magit-diff-context-highlight   ((t (:inherit diff-context :background "#3b4252"))))
4963      (magit-diff-hunk-heading        ((t (:inherit diff-hunk-header))))
4964      (magit-diff-hunk-heading-highlight ((t (:inherit diff-hunk-header :background "#5e81ac"))))
4965  
4966  
4967      :config
4968      ;; Setting this to nil switches with-editor from using emacsclient to its
4969      ;; "sleeping editor" mode (signal-based). Counter-intuitive, but necessary:
4970      ;; emacsclient with $DISPLAY set sends -window-system to the Emacs server,
4971      ;; which creates a new X frame for COMMIT_EDITMSG. The sleeping editor
4972      ;; instead opens the buffer in the current frame via switch-to-buffer.
4973      ;; FIXME Not happy with this, it's counterintuitive, the manual says it should
4974      ;;       not be used and if you do, only temporarily. So, needs attention.
4975      (setq with-editor-emacsclient-executable nil)
4976  
4977      ;; Manage pre-commit from inside magit
4978      (use-package magit-pre-commit
4979        :straight (:host github :repo "DamianB-BitFlipper/magit-pre-commit.el"))
4980  
4981      ;; Enable links to magit fromm org
4982      (use-package orgit)
4983  
4984      (use-package forge
4985        :disabled
4986        :custom
4987        (forge-add-pullreq-refspec 'ask)
4988        (forge-pull-notifications t)
4989        (forge-topic-list-limit '(60 . 0))
4990  
4991        :config
4992        ;; Only show assigned stuff
4993        (magit-add-section-hook
4994         'magit-status-sections-hook
4995         'forge-insert-assigned-issues   nil t)
4996        (magit-add-section-hook
4997         'magit-status-sections-hook
4998         'forge-insert-assigned-pullreqs   nil t)))
4999  #+end_src
5000  
5001  *** Claude Code IDE Integration - Commit Helper
5002  Helper function for Claude Code IDE to start a magit commit with pre-filled message.
5003  
5004  #+begin_src emacs-lisp
5005    (defun mrb/claude-magit-commit (message &optional directory)
5006      "Prepare and start a magit commit with pre-filled message.
5007  This is called by Claude Code IDE when committing changes.
5008  MESSAGE is the commit message text.
5009  DIRECTORY is the git repository path (optional; uses current directory if nil)."
5010      (let* ((repo-dir (file-name-as-directory
5011                        (expand-file-name (or directory default-directory))))
5012             (tmp-file (make-temp-file "claude-commit-" nil ".txt")))
5013        (write-region message nil tmp-file)
5014        (with-temp-buffer
5015          (setq default-directory repo-dir)
5016          (magit-commit-create (list "-F" tmp-file "-e")))))
5017  #+end_src
5018  
5019  Most of the magit work is done through its status screen and I would like to see issues and todo's and fixme's and other work that needs to be done in that screen. The forge package does that from a remote forge, like github, but many tasks are hiddenn in the sources already by myself. The magit-todos package scans for those things and show them in the magit screen
5020  
5021  #+begin_src emacs-lisp
5022    (use-package magit-todos
5023      :disabled nil
5024      :after (magit)
5025      :custom
5026      (magit-todos-insert-after '(bottom))
5027      (magit-todos-exclude-globs '("org-config.org"))
5028      :config
5029      (magit-todos-mode 1))
5030  #+end_src
5031  
5032  *** Committing automatically
5033  I have lost a number of changes in the past because I reverted a file, made a mistake or whatever. Some of these mistakes can be reverted easily if saves are automatically committed
5034  
5035  Rather than using an after save hook, there is a minor git-auto-commit mode package which does just what I need.
5036  
5037  There is not much to configure for this minor mode. There are a couple of ways to enable it:
5038  1. file-local variable (put it in the file to be autocommitted)
5039  2. directory-local variable (make a =.dir-locals.el= file); this enables it for all files in the directory
5040  3. as a hook
5041  
5042  I'm using the first method on relevant files. The disadvantage of this method is that you have to think about it for each file, so perhaps a =.dir-locals.el= is a better solution.
5043  
5044  I am considering using a generic hook again to enable the method and either using =git commit --amend= and commit squashing if working on more structured commits. For files that I really do not want autocommit to run I can use a file local variable to disable the hook (or the minor mode)
5045  
5046  #+begin_src emacs-lisp
5047    (use-package git-auto-commit-mode
5048      :defer t
5049      :hook ((org-journal-mode . git-auto-commit-mode)) ; journals are always autocommitted
5050  
5051      :init
5052      (defun mrb/git-auto-commit-mode ()
5053        "Enable git-auto-commit-mode unless editing the config file."
5054        (interactive)
5055        (unless (and (buffer-file-name)
5056                     (string= (buffer-file-name) config-file))
5057          (git-auto-commit-mode))))
5058  #+end_src
5059  *** Miscellaneous
5060  A collection of loosely development related things.
5061  
5062  **** Online pastebin services
5063  I use github gists sometimes, but rather have more open options as well. These are mostly use to communicate longer pieces of text in chat programs, notably IRC
5064  
5065  #+begin_src emacs-lisp
5066    (use-package webpaste
5067      :custom
5068      (webpaste-provider-priority '("paste.rs"
5069                                     "dpaste.com"
5070                                     "paste.ubuntu.com"
5071                                     "bpa.st"
5072                                     "gist.github.com"))
5073      (webpaste-paste-confirmation t)
5074      (webpaste-open-in-browser t))
5075  #+end_src
5076  
5077  **** Grepping files
5078  Searching for terms in files is a hard problem, but the vast majority of my use-cases can just be solved by searching for terms in the current project root, almost always defined by having a .git directory in a parent directory. So, let's start there and see what else I'll need.
5079  
5080  I've chosen =ag= as the engine for /grepping/ the files because it's very fast. I would like to have a better UI within Emacs though (consult maybe?)
5081  
5082  #+begin_src emacs-lisp
5083    (use-package ag)
5084  #+end_src
5085  ** AI tools
5086  Tools to use "AI" in Emacs.
5087  
5088  *** Claude code
5089  
5090  Use of /claude code/ inside emacs is fairly trivial. Once the cli tool is installed Emacs integrates with it through =vterm=.
5091  
5092  #+begin_src emacs-lisp
5093      (use-package claude-code-ide
5094        :after (vterm autorevert)
5095        :straight (:type git :host github :repo "manzaltu/claude-code-ide.el")
5096        :bind ("C-c C-'" . claude-code-ide-menu) ; menu keybinding
5097        :custom
5098        (claude-code-ide-use-ide-diff t)               ; use ediff, sometimes, see below
5099        (claude-code-ide-focus-claude-after-ediff nil) ; keep focus in control ediff
5100        (claude-code-ide-use-side-window nil)          ; disable side windows to allow C-x 1
5101        :config
5102        (claude-code-ide-emacs-tools-setup)
5103  
5104        ;; Configuration variables and constants
5105        (defcustom mrb/auto-revert-during-claude-files
5106          (list (mrb/emacs-file "mrb.org")
5107                (mrb/emacs-file "custom.el"))
5108          "Files to auto-revert without prompting when Claude Code IDE session is active."
5109          :type '(repeat string)
5110          :group 'mrb)
5111  
5112        (defcustom mrb/claude-code-ide-diff-threshold 5
5113          "Number of line changes at which to switch from auto-accept to ediff review.
5114  Changes with fewer lines than this threshold are auto-accepted without prompting.
5115  Changes with this many lines or more open ediff for interactive review.
5116  Note: New files (no old version) will have all lines counted as changes."
5117          :type 'integer
5118          :group 'claude-code-ide)
5119  
5120        (defconst mrb/claude-code-ide-diff-accepted
5121          '((type . "text") (text . "FILE_SAVED"))
5122          "MCP response indicating diff was accepted and file should be saved.")
5123  
5124        (defun mrb/org-reveal-context-on-open (result)
5125          "Show heading context around point when opening an org file via Claude IDE."
5126          (when (derived-mode-p 'org-mode)
5127            (org-show-context 'default))
5128          result)
5129  
5130        (defun mrb/org-fold-show-all-on-ediff-prepare ()
5131          "Show all org-mode content when preparing buffer for ediff."
5132          (when (and (derived-mode-p 'org-mode)
5133                     (fboundp 'org-fold-show-all))
5134            (org-fold-show-all)))
5135  
5136        (defun mrb/claude-session-active-p (&optional file-path)
5137          "Check if Claude Code IDE has an active session.
5138  If FILE-PATH is provided, check for session in that file's project.
5139  If FILE-PATH is nil, check if ANY session is active."
5140          (when (featurep 'claude-code-ide-mcp)
5141            (condition-case nil
5142                (if file-path
5143                    (when-let* ((project-dir (expand-file-name (file-name-directory file-path)))
5144                                (session (claude-code-ide-mcp--get-session-for-project project-dir)))
5145                      t)
5146                  (and (fboundp 'claude-code-ide-mcp--get-current-session)
5147                       (claude-code-ide-mcp--get-current-session)))
5148              (error nil))))
5149  
5150        (defun mrb/silent-revert-p (&optional file-path)
5151          "Return t if FILE-PATH should auto-revert silently (whitelist + active session).
5152    If FILE-PATH is nil, use the current buffer's file."
5153          (let ((file-path (or file-path (buffer-file-name))))
5154            (when file-path
5155              (let* ((file-canonical (file-truename file-path))
5156                     (whitelist-canonical (mapcar #'file-truename mrb/auto-revert-during-claude-files)))
5157                (and (member file-canonical whitelist-canonical)
5158                     (mrb/claude-session-active-p file-path))))))
5159  
5160        (defun mrb/revert-with-claude-awareness (orig-func arg1 arg2)
5161          "Revert buffer silently if whitelisted AND Claude session active."
5162          (if (mrb/silent-revert-p)
5163              ;; arg2 is t => silent revert
5164              (funcall orig-func arg1 t)
5165            ;; normal revert behaviour (may prompt)
5166            (funcall orig-func arg1 arg2)))
5167  
5168        ;; Wrap auto-revert behavior with our Claude-aware decision logic
5169        ;; Remove old advice if it exists, then add the corrected version
5170        (advice-remove 'revert-buffer--default #'mrb/revert-with-claude-awareness)
5171        (advice-add 'revert-buffer--default :around #'mrb/revert-with-claude-awareness)
5172  
5173        (advice-add 'claude-code-ide-mcp-handle-open-file
5174                    :filter-return #'mrb/org-reveal-context-on-open)
5175  
5176        ;; Unfold org-mode buffers when preparing for ediff
5177        (with-eval-after-load 'ediff
5178          (add-hook 'ediff-prepare-buffer-hook #'mrb/org-fold-show-all-on-ediff-prepare))
5179  
5180        ;; Smart diff routing: y/n prompt for small changes, ediff for large changes
5181        (defun mrb/count-diff-lines (old-content new-content)
5182          "Count lines that differ between old and new content."
5183          (let ((old-lines (split-string old-content "\n"))
5184                (new-lines (split-string new-content "\n")))
5185            (+ (length (seq-difference new-lines old-lines #'equal))
5186               (length (seq-difference old-lines new-lines #'equal)))))
5187  
5188        (defun mrb/claude-code-ide-diff-smart-router (orig-fn arguments)
5189          "Route diff review: auto-accept small changes, ediff for large changes.
5190    For small diffs (<threshold lines), auto-accepts and saves the file.
5191    For large diffs (>=threshold lines), opens ediff for interactive review.
5192    Falls back to ediff if session lookup fails."
5193          (condition-case err
5194              (let* ((old-file-path (alist-get 'old_file_path arguments))
5195                     (new-content (alist-get 'new_file_contents arguments))
5196                     (tab-name (alist-get 'tab_name arguments))
5197                     ;; Read old file if it exists (new files have no old content)
5198                     (old-content (and old-file-path (file-exists-p old-file-path)
5199                                      (with-temp-buffer
5200                                        (insert-file-contents old-file-path)
5201                                        (buffer-string))))
5202                     ;; Count actual lines changed
5203                     (num-changes (if old-content
5204                                    (mrb/count-diff-lines old-content new-content)
5205                                    (length (split-string new-content "\n"))))
5206                     ;; Get session for sending MCP response (try both methods, prefer find-session-for-file)
5207                     (session (or (and (fboundp 'claude-code-ide-mcp--find-session-for-file)
5208                                       (claude-code-ide-mcp--find-session-for-file old-file-path))
5209                                  (and (fboundp 'claude-code-ide-mcp--get-current-session)
5210                                       (claude-code-ide-mcp--get-current-session)))))
5211                (message "claude-code-ide diff: %d line(s) changed %s" num-changes
5212                         (if (>= num-changes mrb/claude-code-ide-diff-threshold) "(ediff)" "(auto-accept)"))
5213                (if (>= num-changes mrb/claude-code-ide-diff-threshold)
5214                    ;; Large changes: open ediff via original handler
5215                    (funcall orig-fn arguments)
5216                  ;; Small changes: auto-accept and send MCP response
5217                  (if session
5218                      (claude-code-ide-mcp-complete-deferred
5219                       session "openDiff"
5220                       (list mrb/claude-code-ide-diff-accepted
5221                             `((type . "text") (text . ,new-content)))
5222                       tab-name)
5223                    ;; Fallback if session lookup fails: use ediff instead
5224                    (progn
5225                      (message "claude-code-ide: session lookup failed, falling back to ediff")
5226                      (funcall orig-fn arguments)))))
5227            (error
5228             ;; If anything goes wrong, fall back to ediff review
5229             (message "claude-code-ide: error in diff router (%s), falling back to ediff" (error-message-string err))
5230             (funcall orig-fn arguments))))
5231  
5232        ;; Wire up the smart router
5233        (advice-remove 'claude-code-ide-mcp-handle-open-diff #'mrb/claude-code-ide-diff-smart-router)
5234        (advice-add 'claude-code-ide-mcp-handle-open-diff :around #'mrb/claude-code-ide-diff-smart-router))
5235  
5236  #+end_src
5237  ** Ansible
5238  Not really a development setting, but closely related to it, as eventually the programs written need to be deployed. Most of the time, while deploying, ansible plays a role in this and it's now worth having a dedicated configuration for editing the YAML files for it.
5239  
5240  For now, just the ansible package, and some access to its documentation will suffice
5241  #+begin_src emacs-lisp
5242    (use-package ansible
5243      :after yaml-mode
5244      :init
5245      (use-package ansible-doc
5246        :hook (yaml-mode . ansible-doc-mode))
5247      :hook (yaml-mode . (lambda () (ansible 1))))
5248  #+end_src
5249  * Finale
5250  When we are all done with this, provide it.
5251  
5252  #+begin_src emacs-lisp
5253    (provide 'mrb)
5254    ;;; mrb ends here
5255  #+end_src
5256  
5257  # -*- eval: (git-auto-commit-mode 1)
5258  # Local Variables:
5259  # writefreely-post-id: "wf83bq5jwz"
5260  # writefreely-post-token: nil
5261  # End: