Skip to content

akoen/emacs

Repository files navigation

Alex Koen’s Emacs configuration

Note: README.org is a symlink to config.org

Most of the time Emacs feels like piloting an inter-galactic starship through the turbulence of spacetime, free from the sluggishness of earthbound life. The rest of time, I feel like printing out my config and lighting it on fire. Regardless, it is tremendous fun and a worthy distraction from my homework.

Highlights:

  1. Fully custom org-agenda renderer
  2. Native org-mode src-block editing with Jupyter integration
  3. Make edebug tolerable

If you’re a fellow Emacs junkie, feel free to say hi over at https://alexkoen.com. I have very diverse interests.

Alright, let’s do this:

Usage

Use-package

Usage of :preface, :init, :config

(use-package example
  ;; Note that errors are never trapped in the preface, since doing so would hide definitions from the byte-compiler.
  :preface (message "I'm here at byte-compile and load time")
  :init (message "I'm always here at startup")
  :config
  (message "I'm always here after the package is loaded"))

Initialization

Lexical Binding

Lexical binding improves performance.

;;; init.el --- The personal Emacs configuration of Alex Koen -*- lexical-binding: t; -*-

Debugging

The following blocks may be tangled to debug certain features:

(setq use-package-verbose t
      use-package-compute-statistics t)

(setq use-package-always-demand t)

To see what’s loading a package:

(with-eval-after-load 'ob (debug))

Or a function

(debug-on-entry 'set-cursor-color)

Or a variable

(debug-on-variable-change 'evil-want-Y-yank-to-eol)

Or a message

(setq debug-on-message "Bad timestamp")

Performance Optimizations

A lot happens in early-init.el.

(use-package gcmh
  :hook (after-init . gcmh-mode))
(add-function :after after-focus-change-function
              (defun +garbage-collect-maybe ()
                (unless (frame-focus-state)
                  (garbage-collect))))

Benchmarking

https://github.com/jschaf/esup

(use-package esup
  :commands esup
  :custom
  (esup-depth 2))
(use-package explain-pause-mode
  :defer t
  :custom
  (explain-pause-alert-style 'silent))

Personal Information

(setq user-full-name "Alex Koen"
      user-mail-address "alex@koen.ca")

Macros & Functions

use-feature from https://github.com/raxod502/radian/blob/develop/emacs/radian.el. Tells straight not to look for a package.

(defmacro use-feature (name &rest args)
  "Like `use-package', but with `straight-use-package-by-default' disabled.
NAME and ARGS are as in `use-package'."
  (declare (indent defun))
  `(use-package ,name
     :straight nil
     ,@args))

Sometimes you mistakenly add advice to a function. This function removes all advice.

(defun my--advice-unadvice (sym)
  "Remove all advices from symbol SYM."
  (interactive "aFunction symbol: ")
  (advice-mapc (lambda (advice _props) (advice-remove sym advice)) sym))

Exec Path From Shell

This sets the variable exec-path to the normal shell’s PATH variable. This doesn’t normally get set in daemon mode.

https://www.reddit.com/r/emacs/comments/f8xwau/hack_replace_execpathfromshell/fioa62n/

(use-package exec-path-from-shell
  :if (daemonp)
  :config
  (setq exec-path-from-shell-arguments (delete "-i" exec-path-from-shell-arguments) 
        exec-path-from-shell-variables (nconc exec-path-from-shell-variables '("PASSWORD_STORE_DIR SSH_AUTH_SOCK")))
  (exec-path-from-shell-initialize))

Set Directories

Note that system directories are set in init.el

(defconst docs-dir "~/Docs/"
  "The root Drive directory, where all documents are stored.")

(defconst org-dir "~/Org/"
  "Org directory")

(defconst journal-dir "~/Journal/"
  "Org directory")

(defconst brain-dir "~/Brain/org/"
  "Braindump directory")

(defconst dotfiles-dir "~/.dotfiles/"
  "Dotfiles directory")

Native comp

(setq native-comp-async-report-warnings-errors nil)

Secrets

(load-file (concat emacs-dir "secrets.el"))

Better Defaults

Asynchronously tangle literate config

Tangling this org file on save. This code is borrowed from https://github.com/rememberYou/.emacs.d/blob/master/config.org

(use-package async)
(defun my--async-babel-tangle (org-file)
  "Tangles the org file asynchronously."
  (let ((init-tangle-start-time (current-time))
        (file (buffer-file-name))
        (async-quiet-switch "-q"))
    (async-start
     `(lambda ()
        (require 'org)
        (org-babel-tangle-file ,org-file))
       `(lambda (result)
          (if result
              (message "SUCCESS: %s successfully tangled (%.2fs)."
                       ,org-file
                       (float-time (time-subtract (current-time)
                                                  ',init-tangle-start-time)))
            (message "ERROR: %s as tangle failed." ,org-file))))))

Misc

(setq vc-follow-symlinks t           ; don't ask for confirmation when opening symlinked file
      ring-bell-function 'ignore     ; silent bell when you make a mistake
      x-select-enable-clipboard t    ; allow pasting selection outside of emacs
      suggest-key-bindings nil
      kill-buffer-query-functions nil
      initial-major-mode 'emacs-lisp-mode
      initial-scratch-message nil
      uniquify-buffer-name-style 'reverse
      uniquify-separator ""
      save-interprogram-paste-before-kill t
      default-input-method 'TeX
      debugger-stack-frame-as-list t
      compilation-scroll-output 'first-error
      compilation-skip-threshold 2   ; don't stop on info or warnings
      delete-by-moving-to-trash t    ; move files to trash when deleting
      sentence-end-double-space nil) ; end sentences with a single space

(setq-default truncate-lines t)

Backups

By default, Emacs saves backups relative to the current directory. This is abhorrent. We change this.

(setq make-backup-files t ; make backups file even when in version controlled dir
      create-lockfiles nil ; they cause problems
      backup-directory-alist (list (cons "." (concat emacs-cache-dir "backups/")))
      backup-by-copying t  ; Don't delink hardlinks
      version-control t  ; Use version numbers on backups
      delete-old-versions t  ; Automatically delete excess backups
      kept-new-versions 20  ; how many of the newest versions to keep
      kept-old-versions 5  ; and how many of the old
      auto-save-list-file-prefix (concat emacs-cache-dir "autosave/"))

Bookmarks

(use-feature bookmark
  :custom
  (bookmark-set-fringe-mark nil))

Tabs

Use spaces instead of tabs.

(setq-default tab-width 2
              evil-shift-width tab-width
              indent-tabs-mode nil)

Whitespace

(use-package ws-butler
  :hook (prog-mode . ws-butler-mode)
  :custom
  (ws-butler-keep-whitespace-before-point nil))

Revert

Automatically reload buffers on file change

(global-auto-revert-mode 1)

Helpful

(use-package helpful
  :commands helpful--read-symbol
  :init
  (global-set-key [remap describe-function] #'helpful-callable)
  (global-set-key [remap describe-command]  #'helpful-command)
  (global-set-key [remap describe-variable] #'helpful-variable)
  (global-set-key [remap describe-key]      #'helpful-key)
  (global-set-key [remap describe-symbol]   #'helpful-symbol)

  :config
  ;; By default, evil shadows helpful keys
  (evil-define-key 'normal helpful-mode-map "q" 'quit-window)

  ;; Always select help window when opened
  (setq help-window-select t))

Which Key

A small buffer which shows the list of commands you can execute next.

(use-package which-key
  :demand t
  :custom
  ;; Activate manually using C-h
  (which-key-show-early-on-C-h t)
  (which-key-idle-delay 10000)
  (which-key-idle-secondary-delay 0.05)
  (which-key-separator " ")
  (which-key-prefix-prefix "+")
  :config
  (which-key-mode 1))

Custom

;; FIXME General must be loaded first
;; (use-feature custom
;;   :general
;;   (general-nmap :keymaps 'custom-mode-map
;;     "<mouse-down-1>" 'widget-button-click))

No littering

(use-package no-littering
  :config
  (with-eval-after-load 'recentf
    (add-to-list 'recentf-exclude no-littering-var-directory)
    (add-to-list 'recentf-exclude no-littering-etc-directory))

  (setq auto-save-file-name-transforms
	`((".*" ,(no-littering-expand-var-file-name "auto-save/") t)))

  ;; Want to sync bookmarks
  (setq bookmark-default-file (concat docs-dir "bookmarks"))

  ;; no-littering changes default snippets directory, so I changed it back.
  (add-to-list 'yas-snippet-dirs
	       (expand-file-name "snippets" user-emacs-directory)))

Appearance

Theme

Doom-themes

(use-package doom-themes
  :defer t
  :custom-face
  (font-lock-comment-face ((t (:slant italic))))

  :config
  ;; (load-theme 'doom-spacegrey t)
  (doom-themes-org-config))

Text

Font

(defun my--configure-fonts (_)
  (set-face-attribute 'default nil
                      :font "PragmataPro"
                      :height 120)

  (set-face-attribute 'fixed-pitch nil
                      :font "PragmataPro Mono"
                      :height 120)

  (set-face-attribute 'variable-pitch nil
                      :font "PragmataPro"
                      :weight 'normal
                      :height 120)

 (remove-hook 'after-make-frame-functions #'my-configure-fonts))

;; In daemon mode, fonts must be set after frame creation.
(if (daemonp)
    (add-hook 'after-make-frame-functions #'my--configure-fonts)
  (my--configure-fonts t))

Frame

Startup Screen

(setq inhibit-startup-screen t)	; inhibit useless and old-school startup screen

Prompts

Make yes or no prompts be y or n prompts

(fset 'yes-or-no-p 'y-or-n-p)

Dashboard

(use-package dashboard
  :custom
  ;; FIXME causes errors when running emacs --eval
  (initial-buffer-choice (lambda () (get-buffer "*dashboard*")))
  (dashboard-startup-banner (concat emacs-dir "splash.svg"))
  (dashboard-footer-messages
   (list (seq-random-elt
          (with-temp-buffer
            (insert-file-contents (concat user-emacs-directory "messages"))
            (split-string (buffer-string) "\n" t)))))
  (dashboard-banner-logo-title nil)
  (dashboard-set-heading-icons t)
  (dashboard-set-file-icons t)
  (dashboard-center-content t)
  (dashboard-items nil)
  (dashboard-page-separator "\n")
  :init
  (add-hook 'after-init-hook
            (lambda ()
              (setq dashboard-init-info
                    (format
                     "Emacs ready in %.2f seconds"
                     (float-time
                      (time-subtract after-init-time before-init-time))))))
  :config
  (dashboard-setup-startup-hook))

Visual-line mode

(add-hook 'text-mode-hook #'visual-line-mode)
(add-hook 'message-mode-hook #'visual-line-mode)

Relative line numbers

(global-display-line-numbers-mode t)
(setq display-line-numbers-type 'relative
      display-line-numbers-grow-only t)

;; Prevent lines from being shifted when order of magnitude increases.
(setq display-line-numbers-width-start t)

;; Disable line numbers for some modes
(dolist (mode '(term-mode-hook
                shell-mode-hook
                vterm-mode-hook
                org-agenda-mode-hook
                pdf-view-mode-hook
                treemacs-mode-hook
                eshell-mode-hook))
  (add-hook mode (lambda () (display-line-numbers-mode 0))))

Highlight current line

(use-feature hl-line
  :hook ((prog-mode dired-mode) . hl-line-mode))

Show matching parentheses

(setq show-paren-delay 0)
(show-paren-mode 1)

Smooth Scrolling

Copied from Doom Emacs

(when (>= emacs-major-version 29)
  (pixel-scroll-precision-mode 1))

(setq hscroll-margin 2
      fast-but-imprecise-scrolling t
      hscroll-step 1
      ;; Emacs spends too much effort recentering the screen if you scroll the
      ;; cursor more than N lines past window edges (where N is the settings of
      ;; `scroll-conservatively'). This is especially slow in larger files
      ;; during large-scale scrolling commands. If kept over 100, the window is
      ;; never automatically recentered.
      scroll-conservatively 101
      scroll-margin 0
      scroll-preserve-screen-position t
      ;; Reduce cursor lag by a tiny bit by not auto-adjusting `window-vscroll'
      ;; for tall lines.
      auto-window-vscroll nil
      ;; mouse
      mouse-wheel-scroll-amount '(5 ((shift) . 2))
      mouse-wheel-progressive-speed nil)  ; don't accelerate scrolling

smartparens

Utility for managing parenthesis in Emacs

(use-package smartparens
  :init
  ;; Don't highlight - overly distracting
  (setq sp-highlight-pair-overlay nil
        sp-highlight-wrap-overlay nil
        sp-highlight-wrap-tag-overlay nil
        sp-escape-quotes-after-insert nil)
  (setq-default sp-autoskip-closing-pair nil)
  :config
  (require 'smartparens-config)

  (defun sp-point-before-whitespace-p (_id action _context)
    "Return t if point is followed by a whitespace character, nil
otherwise. This predicate is only tested on \"insert\" action."
    (when (eq action 'insert)
      (sp--looking-at-p "\\s-")))

  (defun sp-point-before-closing-brace-p (_id action _context)
    "Return t if point is followed by a closing brace character, nil
otherwise. This predicate is only tested on \"insert\" action."
    (when (eq action 'insert)
      (sp--looking-at-p "[])}]")))

  (dolist (brace '("(" "{" "["))
    (sp-pair brace nil
             ;; Expand {|} => SPC => { | }
             ;; Expand {|} => RET => {
             ;;   |
             ;; }
             :post-handlers '(("||\n[i]" "RET") ("| " "SPC"))
             ;; Don't autopair opening braces if before a non-whitespace
             ;; character. The rationale: it interferes with manual balancing of
             ;; braces, and is odd form to have s-exps with no whitespace in
             ;; between, e.g. ()()(). Insert whitespace if genuinely want to
             ;; start a new form in the middle of a word.
             :when '(sp-point-before-whitespace-p
                     sp-point-before-closing-brace-p
                     sp-point-before-eol-p)))

  ;; Don't do square-bracket space-expansion where it doesn't make sense to
  (sp-local-pair '(emacs-lisp-mode org-mode markdown-mode)
                 "[" nil :post-handlers '(:rem ("| " "SPC")))

  (smartparens-global-mode 1))

All the icons

(use-package all-the-icons
  :defer t)

Doom-modeline

A fancy and fast mode-line inspired by minimalistic design

(use-package doom-modeline
  :custom
  (doom-modeline-buffer-encoding nil)
  :init (doom-modeline-mode 1))

hl-todo

Highlights keywords in comments

(use-package hl-todo
:hook ((org-mode markdown-mode prog-mode) . hl-todo-mode)
:config
(setq hl-todo-highlight-punctuation ":"
      hl-todo-keyword-faces
      `(;; For things that need to be done, just not today.
        ("TODO" warning bold)
        ;; For problems that will become bigger problems later if not
        ;; fixed ASAP.
        ("FIXME" error bold)
        ;; For tidbits that are unconventional and not intended uses of the
        ;; constituent parts, and may break in a future update.
        ("HACK" font-lock-constant-face bold)
        ;; For things that were done hastily and/or hasn't been thoroughly
        ;; tested. It may not even be necessary!
        ("REVIEW" font-lock-keyword-face bold)
        ;; For especially important gotchas with a given implementation,
        ;; directed at another user other than the author.
        ("NOTE" success bold)
        ;; For things that just gotta go and will soon be gone.
        ("DEPRECATED" font-lock-doc-face bold))))

Doom-Popup

(use-package doom-popup
  :commands (set-popup-rule! set-popup-rules!)
  :straight (:host github :protocol ssh :repo "akoen/doom-popup")
  :custom
  (+popup-all-popups t)
  (windmove-allow-all-windows t)
  :config
  (+popup-mode +1)
  (add-hook '+window-quit-hook #'+popup-close-on-escape-h 'append)
  ;; General keyword doesn't seem to work with :straight
  (general-define-key :states '(normal insert motion)
                      :keymaps 'override
                      "C-<tab>"   #'+popup/toggle
                      ;; "C-~"   #'+popup/raise
                      "C-x p" #'+popup/other)

  (set-popup-rules!
    '(("\\*Async Shell Command\\*" :ttl nil))))

Keybindings

Unbind RET

Unbind return from evil, so that org-return-follows-link works.

(with-eval-after-load 'evil-maps
  (define-key evil-motion-state-map (kbd "RET") nil))

General

Space is the best leader key.

(use-package general
  :after which-key
  :config
  (general-override-mode)
  (general-evil-setup)
  (general-auto-unbind-keys)

  (general-create-definer leader-key
    :keymaps 'override
    :states '(normal insert hybrid visual motion operator emacs)
    :prefix "SPC"
    :non-normal-prefix "C-SPC")

  (leader-key
    ;; Misc
    "u" 'universal-argument

    ;; Buffer
    "b" '(:wk "buffer")
    "br" 'revert-buffer
    "bD" 'kill-buffer
    "`" 'evil-switch-to-windows-last-buffer

    "d" '(:wk "debug")

    "e" '(:wk "eval")
    "ef" 'load-file

    ;; Files
    "." 'find-file
    "ff" 'dired-jump
    "fs" 'save-buffer
    "fR" '((lambda (new-path)
             (interactive (list (read-file-name "Move file to: ")))
             (rename-file (buffer-file-name) (expand-file-name new-path)))
           :wk "move/rename")

    ;; Magit
    "g" '(:wk "git")
    "gg" 'magit-status
    "gfh" 'magit-log-buffer-file

    ;; Language
    "lg" 'writing-mode
    "ls" 'flyspell-mode
    "lb" 'ispell-buffer

    ;; Org mode
    "o" '(:wk "org")
    "oa" 'org-agenda
    "oc" 'org-capture
    "ojj" 'org-journal-new-entry
    "oje" 'org-journal-new-date-entry
    "ojs" 'org-journal-search-forever

    ;; Terminal
    "t" '(:wk "term")
    "tn" 'vterm-other-window
    ))

Hydra

Hydra is a package that allows for families of short keybindings to be defined.

Once you summon the Hydra through the prefixed binding (the body + any one head), all heads can be called in succession with only a short extension.

The Hydra is vanquished once Hercules, any binding that isn’t the Hydra’s head, arrives. Note that Hercules, besides vanquishing the Hydra, will still serve his original purpose, calling his proper command. This makes the Hydra very seamless, it’s like a minor mode that disables itself auto-magically.

(use-package hydra
  :general
  ("C-x C-=" 'hydra-zoom/body)
  (general-nmap "C-w" 'hydra-window/body)

  :config
  (defhydra hydra-window ()
    "Window"

    ("C-w" ace-select-window "select" :exit t)
    ("d" delete-window "delete" :exit t)

    ("C-h" evil-window-left "left" :exit t :column "Move")
    ("C-j" evil-window-down "down" :exit t)
    ("C-k" evil-window-up "up" :exit t)
    ("C-l" evil-window-right "right" :exit t)

    ("h" evil-window-left "left" :column "Move Stay")
    ("j" evil-window-down "down")
    ("k" evil-window-up "up")
    ("l" evil-window-right "right")

    ("H" shrink-window-horizontally "" :column "Resize")
    ("J" enlarge-window "")
    ("K" shrink-window "")
    ("L" enlarge-window-horizontally "")

    ("x" ace-delete-window "delete" :exit t :column "Operate")
    ("m" ace-delete-other-windows "maximize" :exit t)
    ("b" balance-windows "balance")

    ("sh" evil-window-split "horizontally" :column "Split" :exit t)
    ("sv" evil-window-vsplit "vertically" :exit t)

    ("q" quit-window "quit" :color blue))

  (defhydra hydra-zoom ()
    "zoom"
    ("+" text-scale-increase "in")
    ("=" text-scale-increase "in")
    ("-" text-scale-decrease "out")
    ("_" text-scale-decrease "out")
    ("0" (text-scale-adjust 0) "reset")
    ("q" nil "quit" :color blue))
  )

Undo

(use-package undo-fu
  :custom
  (undo-limit 400000)
  (undo-strong-limit 3000000)
  (undo-outer-limit 48000000))

(use-package undo-fu-session
  :custom
  (undo-fu-session-incompatible-files '("\\.gpg$" "/COMMIT_EDITMSG\\'" "/git-rebase-todo\\'"))
  :config
  (global-undo-fu-session-mode 1))

EVIL

Base configuration

Allows for traditional vim bindings inside of emacs

;; load evil
(use-package evil
  :general
  ;; quick replace
  (general-def :states 'normal
    "<SPC>;" (lambda ()
               (interactive)
               (evil-ex "%s/")))
  (general-def :states 'visual
    "<SPC>;" (lambda ()
               (interactive)
               (evil-ex "'<,'>s/")))
  :init
  ;; In general, evil-want- keybinds must be set on init.
  (setq evil-want-keybinding nil
        evil-want-C-u-scroll t
        evil-want-C-u-delete t
        evil-want-Y-yank-to-eol t
        evil-visual-state-cursor 'hollow
        evil-emacs-state-cursor (lambda () (set-cursor-color (face-foreground 'warning)))
        evil-normal-state-cursor (lambda () (set-cursor-color (face-foreground 'default))))

  :custom
  (evil-undo-system 'undo-fu)
  (evil-split-window-below t)
  (evil-vsplit-window-right t)
  ;; (evil-search-module 'evil-search)
  (evil-ex-substitute-global t)
  (evil-move-beyond-eol t)
  (evil-esc-mode nil) ;; performance. Only used for jj/jk type mappings
  ;; (evil-ex-search-vim-style-regexp t) Consider this
  (evil-jumps-cross-buffers nil)
  (evil-cross-lines t)
  :config
  (evil-define-operator my--evil-replace-with-kill-ring (beg end)
    "Replace text object with kill ring contents without replacing them."
    :move-point nil
    (interactive "<r>")
    (save-excursion
      (delete-region beg end)
      (goto-char beg)
      (call-interactively 'evil-paste-before 1)))

  (define-key evil-normal-state-map "go" 'my--evil-replace-with-kill-ring)

  ;; By default indenting moves the point to the beginning of the region. Quite
  ;; honestly, this is evil.
  (defadvice evil-indent (around evil-indent-advice activate)
    (save-excursion
      ad-do-it))

  <<custom text objects>>
  <<better line movement>>

  (evil-mode 1))

Better line movement

Especially when writing text, moving by visual lines instead of by numbered lines is the way to go. However, when using relative line numbers, this can make large relative jumps like 13k inaccurate. Here, we define j and k to only move by visual lines if there is no associated COUNT.

(evil-define-motion evil-next-visual-line-or-next-line (count)
  "Move the cursor 1 visual lines down or COUNT numbered lines down."
  :type exclusive
    (let ((line-move-visual (if count nil t)))
      (evil-line-move (or count 1))))

(evil-define-motion evil-previous-visual-line-or-previous-line (count)
  "Move the cursor 1 visual lines up or COUNT numbered lines up."
  :type exclusive
    (let ((line-move-visual (if count nil t)))
      (evil-line-move (- (or count 1)))))

  (define-key evil-normal-state-map (kbd "<remap> <evil-next-line>") 'evil-next-visual-line-or-next-line)
  (define-key evil-normal-state-map (kbd "<remap> <evil-previous-line>") 'evil-previous-visual-line-or-previous-line)
  (define-key evil-motion-state-map (kbd "<remap> <evil-next-line>") 'evil-next-visual-line-or-next-line)
  (define-key evil-motion-state-map (kbd "<remap> <evil-previous-line>") 'evil-previous-visual-line-or-previous-line)

(defun back-to-indentation-or-beginning ()
  (interactive)
  (let ((old-point (point)))
    (back-to-indentation)
    (when (eq (point) old-point)
      (beginning-of-line))))

(general-nvmap "0" 'back-to-indentation-or-beginning)

CamelCase word movement

(global-subword-mode +1)

Custom text objects

;; See https://github.com/emacs-evil/evil-surround#add-new-surround-pairs-through-creation-of-evil-objects
(defmacro define-and-bind-text-object (key start-regex end-regex)
  (let ((inner-name (make-symbol "inner-name"))
        (outer-name (make-symbol "outer-name")))
    `(progn
       (evil-define-text-object ,inner-name (count &optional beg end type)
         (evil-select-paren ,start-regex ,end-regex beg end type count nil))
       (evil-define-text-object ,outer-name (count &optional beg end type)
         (evil-select-paren ,start-regex ,end-regex beg end type count t))
       (define-key evil-inner-text-objects-map ,key (quote ,inner-name))
       (define-key evil-outer-text-objects-map ,key (quote ,outer-name)))))

(define-and-bind-text-object "/" "/" "/")
(define-and-bind-text-object "*" "*" "*")
(define-and-bind-text-object "~" "\\~" "\\~")
(define-and-bind-text-object "=" "=" "=")
(define-and-bind-text-object "$" "\\$" "\\$")
(define-and-bind-text-object "," "," ",")

;; LINE
;; from https://github.com/emacsorphanage/evil-textobj-line
(defun evil-line-range (count beg end type &optional inclusive)
  (if inclusive
      (evil-range (line-beginning-position) (line-end-position))
    (let ((start (save-excursion
                   (back-to-indentation)
                   (point)))
          (end (save-excursion
                 (goto-char (line-end-position))
                 (skip-syntax-backward " " (line-beginning-position))
                 (point))))
      (evil-range start end))))

(evil-define-text-object evil-a-line (count &optional beg end type)
  "Select range between a character by which the command is followed."
  (evil-line-range count beg end type t))

(evil-define-text-object evil-inner-line (count &optional beg end type)
  "Select inner range between a character by which the command is followed."
  (evil-line-range count beg end type))

(define-key evil-outer-text-objects-map "l" 'evil-a-line)
(define-key evil-inner-text-objects-map "l" 'evil-inner-line)

;; BUFFER
(defun evil-buffer-range (count beg end type)
      (evil-range (point-min) (point-max)))

(evil-define-text-object evil-i-buffer (count &optional beg end type)
  "Select range between a character by which the command is followed."
  (evil-buffer-range count beg end type))

(define-key evil-outer-text-objects-map "e" 'evil-i-buffer)
(define-key evil-inner-text-objects-map "e" 'evil-i-buffer)

;; DEFUN
(evil-define-text-object evil-i-defun (count &optinoal beg end type)
   (cl-destructuring-bind (beg . end)
      (bounds-of-thing-at-point 'defun)
    (evil-range beg end type)))

(define-key evil-outer-text-objects-map "d" 'evil-i-defun)
(define-key evil-inner-text-objects-map "d" 'evil-i-defun)

;; URL
(evil-define-text-object evil-inner-url (count &optional _beg _end type)
  "Text object to select the inner url at point.

This excludes the protocol and querystring."
  (cl-destructuring-bind (beg . end)
      (bounds-of-thing-at-point 'url)
    (evil-range
     (save-excursion
       (goto-char beg)
       (re-search-forward "://" end t))
     (save-excursion
       (goto-char end)
       (- (if-let (pos (re-search-backward "[?#]" beg t))
              pos
            end)
          (if (evil-visual-state-p)
              1
            0)))
     type)))

(evil-define-text-object evil-outer-url (count &optional _beg _end type)
  "Text object to select the whole url at point."
  (cl-destructuring-bind (beg . end)
      (bounds-of-thing-at-point 'url)
    (evil-range
     beg (- end (if (evil-visual-state-p) 1 0))
     type)))

(define-key evil-outer-text-objects-map "u" 'evil-outer-url)
(define-key evil-inner-text-objects-map "u" 'evil-inner-url)

;; NERD COMMENTER
(with-eval-after-load 'evil-nerd-commenter
  (define-key evil-outer-text-objects-map "c" 'evilnc-outer-commenter)
  (define-key evil-inner-text-objects-map "c" 'evilnc-inner-comment))

EVIL-Collection

(use-package evil-collection
  :after evil
  :config
  (evil-collection-init))

A more peaceful keyboard-quit

This code allows us to quit basically everything using ESC.

(setq evil-intercept-esc nil) ;; May need to be set to t

(defvar my--escape-hook nil
  "A hook run when esc is pressed")

(defun +edit-quit ()
  "Run `my--escape-hook'."
  (interactive)
  (cond
   ((or (evil-insert-state-p) (evil-visual-state-p))
    (evil-force-normal-state))
   ;; quit the minibuffer if open.
   ((minibuffer-window-active-p (minibuffer-window))
    (abort-recursive-edit))
   ;; Run all escape hooks. If any returns non-nil, then stop there.
   ((run-hook-with-args-until-success 'my--escape-hook))
   ;; don't abort macros
   ((or defining-kbd-macro executing-kbd-macro) nil)
   ;; Back to the default
   ((keyboard-quit))))

(defun +window-quit ()
  (interactive)
  (cond
   ((minibuffer-window-active-p (minibuffer-window))
    (abort-recursive-edit))
   ((run-hook-with-args-until-success '+window-quit-hook))
   ((org-src-edit-buffer-p)
    (org-edit-src-exit))))

;; We want this *everywhere*
(general-define-key :states '(insert normal visual motion) "<escape>" '+edit-quit)
;; Covers other places like the minibufer
(general-def :keymaps 'override "<escape>" '+edit-quit)
(general-def :states '(normal visual motion) :keymaps 'override
  "<DEL>" '+window-quit
  "<backspace>" '+window-quit)

evil-surround

This package emulates surround.vim by Tim Pope.

(use-package evil-surround
  :config
  (global-evil-surround-mode 1)
  ;; Do not add spaces inside parens
  (evil--add-to-alist
   'evil-surround-pairs-alist
   ?\( '("(" . ")")
   ?\[ '("[" . "]")
   ?\{ '("{" . "}")
   ?\) '("( " . " )")
   ?\] '("[ " . " ]")
   ?\} '("{ " . " }")))

evil-nerd-commenter

(use-package evil-nerd-commenter
  :general
  (general-nmap "gc" 'evilnc-comment-operator))

evil-numbers

(use-package evil-numbers
  :general
  (general-nmap
    "g=" 'evil-numbers/inc-at-pt
    "g-" 'evil-numbers/dec-at-pt))

evil-args

(use-package evil-args
  :commands evil-inner-arg evil-outer-arg
  :init
  (define-key evil-inner-text-objects-map "a" 'evil-inner-arg)
  (define-key evil-outer-text-objects-map "a" 'evil-outer-arg))

evil-matchit

(use-package evil-matchit
  :config
  (global-evil-matchit-mode 1))

evil-lion

(use-package evil-lion
  :general
  (general-nvmap
    "gl" 'evil-lion-left
    "gL" 'evil-lion-right))

evil-snipe

Allows for quick movement to 2-char sequences.

(use-package evil-snipe
  :config
  (evil-snipe-mode +1)
  (evil-snipe-override-mode +1))

evil-goggles

Gives a visual indication as to the region you just edited.

(use-package evil-goggles
  :config
  (evil-goggles-mode)
  (evil-goggles-use-diff-faces))

winner-mode

Pressing Q restores the window configuration to the last state. Useful for killing compilation buffers etc. Redo with C-c right

(use-feature winner
  :after evil
  :config
  ;; We do not want to defer since winner does not start logging until
  ;; it is loaded
  (bind-key "Q" #'winner-undo evil-normal-state-map)
  (winner-mode 1))

Avy

(use-package avy
  :general
  (general-nmap "C-s" 'evil-avy-goto-char-timer)
  :custom
  (avy-background t)
  (avy-all-windows nil))

Ace-window

(use-package ace-window
  :defer t
  :custom-face
  (aw-leading-char-face ((t (:inherit font-lock-keyword-face :bold t :height 3.0))))
  :custom
  (aw-keys '(?h ?j ?k ?l ?a ?s ?d ?f))
  (aw-dispatch-always t)
  (aw-scope 'frame))

Link-hint

(use-package link-hint
  :general
  (general-nmap
    :keymaps '(helpful-mode-map info-mode-map)
    "o" 'link-hint-open-link)
  (leader-key
    "sl" 'link-hint-open-link))

Expand-region

(use-package expand-region
  :general
  (general-vmap
    "v" 'er/expand-region
    "-" 'er/contract-region))

Hungry-delete

(use-package smart-hungry-delete
  :general
  (general-imap "<DEL>" 'smart-hungry-delete-backward-char)
  :config
  (smart-hungry-delete-add-default-hooks)

  (advice-add #'smart-hungry-delete-backward-char :around #'+smart-hungry-delete-compat-python)
  (defun +smart-hungry-delete-compat-python (orig-fn &rest arg)
    "Call `smart-hungry-delete-backward-char' unless point is directly after the indentation, in which case call `python-indent-dedent-line-backspace'."
    (if (and (eq major-mode 'python-mode)
             (looking-back "^[\t ]+"))
        (python-indent-dedent-line-backspace arg)
      (funcall orig-fn arg))))

Completion

Function

(defun +insert-filename ()
  "Prompt and insert filename at point."
  (interactive)
  (insert (read-file-name "File: ")))
(general-define-key "<f3>" '+insert-filename)

Minibuffer Completion

(use-package vertico
  :demand t
  :straight '(vertico :host github
                      :repo "minad/vertico"
                      :branch "main"
                      :files (:defaults "extensions/*")
                      :includes (vertico-directory vertico-quick))
  :general
  (:keymaps 'vertico-map
            "C-s" 'vertico-quick-insert)
  :custom
  (vertico-cycle t)
  (enable-recursive-minibuffers t)
  :config
  (vertico-mode 1))

(use-package vertico-directory
  ;; More convenient directory navigation commands
  :bind (:map vertico-map
              ("RET" . vertico-directory-enter)
              ("DEL" . vertico-directory-delete-char)
              ("M-DEL" . vertico-directory-delete-word))
  ;; Tidy shadowed file names
  :hook (rfn-eshadow-update-overlay . vertico-directory-tidy))

(use-feature savehist
  :custom
  (history-length 25)
  :config
  (savehist-mode 1))

(use-package orderless
  :custom
  (completion-styles '(orderless partial-completion))
  (orderless-matching-styles
   '(orderless-regexp
     orderless-initialism)))

(use-package consult
  :general
  (leader-key
    "fr" 'consult-recent-file
    "bb" 'consult-buffer
    "RET" 'consult-bookmark
    "yp" 'consult-yank
    "so" 'consult-outline
    "sO" 'consult-org-agenda
    "ss" 'consult-line
    "sf" 'consult-focus-lines)
  :custom
  (consult-narrow-key "<")
  (consult-project-root-function #'projectile-project-root)
  :init
  ;; Configure the register formatting. This improves the register
  ;; preview for `consult-register', `consult-register-load',
  ;; `consult-register-store' and the Emacs built-ins.
  (setq register-preview-delay 0
        register-preview-function #'consult-register-format))

(use-package consult-flycheck
  :commands consult-flycheck
  :general
  (leader-key
    "cf" 'consult-flycheck))

(use-package consult-dir
  :general
  (leader-key "fd" 'consult-dir)
  (:keymaps 'vertico-map
            "C-x C-d" 'consult-dir
            "C-x C-j" 'consult-jump-file))

(use-package marginalia
  :config
  (marginalia-mode +1))

(use-package embark
  :general
  ;; Bindings shadow evil-repeat-pop
  (:states '(normal insert)
           "C-." 'embark-act
           "M-." 'embark-dwim
           "C-h B" 'embark-bindings)
  (:keymaps 'vertico-map
            "C-." 'embark-act
            "M-." 'embark-dwim)
  :custom
  (prefix-help-command #'embark-prefix-help-command)
  :config
  (setq embark-action-indicator
        (lambda (map _target)
          (which-key--show-keymap "Embark" map nil nil 'no-paging)
          #'which-key--hide-popup-ignore-command)
        embark-become-indicator embark-action-indicator)

  (defmacro +embark-ace-action (fn)
    `(defun ,(intern (concat "+embark-ace-" (symbol-name fn))) ()
       (interactive)
       (with-demoted-errors "%s"
         (require 'ace-window)
         (let ((aw-dispatch-always t))
           (aw-switch-to-window (aw-select nil))
           (call-interactively (symbol-function ',fn))))))
  (define-key embark-file-map     (kbd "o") (+embark-ace-action find-file))
  (define-key embark-buffer-map   (kbd "o") (+embark-ace-action switch-to-buffer))
  (define-key embark-bookmark-map (kbd "o") (+embark-ace-action bookmark-jump)))

(use-package embark-consult
  :after (embark consult)
  :demand t
  :hook
  (embark-collect-mode . embark-consult-preview-minor-mode))

Corfu

(use-package corfu
  :demand t
  :hook (minibuffer-setup . corfu-mode)
  :general
  (:states '(insert) :keymaps 'corfu-map
           "C-n" 'corfu-next
           "C-p" 'corfu-previous)
  (:states '(insert)
           "C-;" 'completion-at-point)
  :custom
  (corfu-auto nil)
  (tab-always-indent 'complete)
  :config
  ;; (evil-make-overriding-map corfu-map)
  ;; (advice-add 'corfu--setup :after 'evil-normalize-keymaps)
  ;; (advice-add 'corfu--teardown :after 'evil-normalize-keymaps)
  (global-corfu-mode))

Yasnippet

A package with which you can insert code or text snippets based on templates.

We define a function to autocomplete snippets. See joaotavora/yasnippet#998

(use-package yasnippet
  :demand t
  :general
  (leader-key "cy" (defhydra hydra-yas (:color blue
                                               :hint nil)
		     "
_i_nsert    _n_ew       _v_isit
_r_eload    e_x_pand    _?_ list
"
		     ("i" yas-insert-snippet)
		     ("n" yas-new-snippet)
		     ("v" yas-visit-snippet-file)
		     ("r" yas-reload-all)
		     ("x" yas-expand)
		     ("?" yas-describe-tables)
		     ("q" nil "cancel" :color blue)))
  :config

  ;; Automatically expand snippet when # condition: 'auto is used in snippet header
  (defun my--yas-try-expanding-auto-snippets ()
    (when (and (boundp 'yas-minor-mode) yas-minor-mode)
      (let ((yas-buffer-local-condition ''(require-snippet-condition . auto)))
        (yas-expand))))

  (add-hook 'post-command-hook #'my--yas-try-expanding-auto-snippets)

  (with-eval-after-load 'warnings
    (add-to-list 'warning-suppress-types '(yasnippet backquote-change)))

  (setq yas-verbosity 2)
  (yas-global-mode 1))

Snippet

(use-package aas)
(use-package sfp
  :demand t
  :straight nil
  :hook (LaTeX-mode . aas-activate-for-major-mode)
  :config

  (defun +point-after-whitespace ()
    (or (bolp)
        (string-match (string (char-before)) "\s-")))

  (+snippet-def 'latex-mode
                :auto t
                :cond #'+point-after-whitespace
                "dm" "\\[\n$1\n.\\] $0"
                "mk" "\$$1\$$0")

  (+snippet-def 'latex-mode
                :cond #'texmathp
                :auto t
                "ooo" "\\infty"
                "neq" "\\ne"
                "leq" "\\le"
                "geq" "\\ge"
                "->"  "\\to"
                "==" "&="
                "EE" "\\exists"
                "td" "^{$0}"
                "dint" "\\int\_{${1:-\\infty}}^{${2:\\infty}}$0"
                "sum" "\\sum\_{${1:n=0}}^{${2:\\infty}}$0"))

Hippie

(use-feature hipie-exp
  :general
  (general-define-key "M-/" 'hippie-expand))

Project, Files, and System

For tools that deal with files.

Projectile

Project management.

(use-package projectile
  :demand t
  :general
  (leader-key
    "SPC" 'projectile-find-file
    "pd" 'projectile-find-dir
    "pp" 'projectile-switch-project
    "pi" 'projectile-invalidate-cache
    "pk" 'projectile-kill-buffers
    "pt" 'my--projectile-find-file-in-project-tasks
    "pc" 'my--projectile-find-file-in-project-config
    "cc" 'projectile-compile-project)

  :init
  (setq projectile-enable-caching nil
        projectile-require-project-root 'prompt
        projectile-sort-order 'recentf
        projectile-use-git-grep t) ; use git-grep for text searches

  ;; TODO - Temp fix for git repositories with submodules: see https://github.com/bbatsov/projectile/issues/1302#issuecomment-433894379
  (setq projectile-git-submodule-command nil)

  :config
  (projectile-mode +1)
  (add-to-list 'projectile-project-root-files-bottom-up ".project" t))

Projectile for specific projects

(defun my--projectile-find-file-in-project-config ()
  (interactive)
  (let ((default-directory emacs-dir))
    (projectile-find-file)))

(defun my--projectile-find-file-in-project-tasks ()
  (interactive)
  (let ((default-directory org-dir))
    (projectile-find-file)))

(defun my--projectile-find-file-in-project-dotfiles ()
  (interactive)
  (let ((default-directory dotfiles-dir))
    (projectile-find-file)))

Affe

Find and grep fuzzy searching, inspired by fzf.

(use-package affe
  :general
  (leader-key
    "sp" 'affe-grep
    ;; We handle file searching with projectile
    ;; "fz" 'affe-find
    ))

Magit

An inteface to Git. The mascot of Emacs packages.

(use-package magit
  :config
  (magit-wip-mode +1))

(use-package magit-todos
  :commands magit-status
  :custom
  (magit-todos-keyword-suffix "\\(?:([^)]+)\\)?:?") ; make colon optional
  :config
  (magit-todos-mode))

Comint

(use-feature comint
  :preface
  (defun comint-clear-buffer ()
    (interactive)
    (let ((comint-buffer-maximum-size 0))
      (comint-truncate-buffer)))
  :general
  (general-define-key
   :keymaps 'comint-mode-map
   "C-l" 'comint-clear-buffer))

Vterm

A fantastic terminal emulator for Emacs. Currently, there is no good way to use evil, and since I’m used to Emacs keybindings in other terminals anyways, we just set the default mode to evil.

(use-package vterm
  :commands vterm vterm-mode +vterm-run-command
  :general
  (general-emap
    :keymaps 'vterm-mode-map
    "<escape>" 'vterm-send-escape)
  :custom
  (vterm-kill-buffer-on-exit t)
  :config
  (set-popup-rule! "\\*vterm\\*" :ttl nil :size 0.4 :select t)

  (setq +vterm-run-command-buffer-name "vterm-command")
  (set-popup-rule! +vterm-run-command-buffer-name :ttl nil)

  (defun +vterm-run-command (command)
    "Execute string COMMAND in dedicated vterm buffer"
    (interactive "MCommand: ")
    (with-current-buffer
        (vterm
         (concat
          "*"
          +vterm-run-command-buffer-name
          "["
          (car (split-string command))
          "]*"))
      (rename-uniquely)
      (vterm-insert command)
      (vterm-send-return))))

(use-package vterm-toggle
  :commands vterm-toggle vterm-toggle-cd
  :bind
  (("C-`" . vterm-toggle))
  :custom
  (vterm-toggle-scope 'project))

Dired

Dired

(use-feature dired
  :commands dired-jump
  :general
  (general-nmap :keymaps 'dired-mode-map
    "=" 'my--diff-files
    "C-<return>" '+open-file-external)
  (:keymaps 'dired-mode-map
            "C-c C-p" 'wdired-change-to-wdired-mode)
  :config
  (setq dired-auto-revert-buffer t  ; don't prompt to revert; just do it
        dired-dwim-target t  ; suggest a target for moving/copying intelligently
        ;; Always copy/delete recursively
        dired-recursive-copies  'always
        dired-recursive-deletes 'top
        dired-clean-confirm-killing-deleted-buffers nil) ;; don't ask just do

  ;; Show directories before files + default -al
  (setq dired-listing-switches "-lAXh --group-directories-first")

  (defun +open-file-external (arg)
    "Open visited file in default external program.
When in dired mode, open file under the cursor.
With a prefix ARG always prompt for command to use."
    (interactive "P")
    (let* ((current-file-name
            (if (eq major-mode 'dired-mode)
                (dired-get-file-for-visit)
              buffer-file-name))
           (open (pcase system-type
                   (`darwin "open")
                   ((or `gnu `gnu/linux `gnu/kfreebsd) "xdg-open")))
           (program (if (or arg (not open))
                        (read-shell-command "Open current file with: ")
                      open)))
      (call-process program nil 0 nil current-file-name)))

  (with-eval-after-load 'embark
    (define-key embark-file-map (kbd "e") 'my--edebug-add-instrumentation))

  (defun +dired-drag-and-drop ()
    "Open dragon with the marked files or the file at point."
    (interactive)
    (make-process
     :name "dragon-drag-and-drop"
     :buffer "*dragon*"
     :command (append '("dragon-drag-and-drop") (dired-get-marked-files))
     :noquery t))

  ;; Show contents of .desc file in minibuffer
  (defun show-folder-description ()
    (interactive)
    (if (file-exists-p "./.desc")
        (let ((description
               (with-temp-buffer
                 (insert-file-contents "./.desc")
                 (buffer-string))))
          (message description))))

  (add-hook 'dired-after-readin-hook #'show-folder-description)

  (add-hook 'dired-mode-hook #'dired-hide-details-mode)

  (defun my--diff-files ()
    (interactive)
    (let ((files (dired-get-marked-files)))
      (if (<= (length files) 2)
          (let ((file1 (car files))
                (file2 (if (cdr files)
                           (cadr files)
                         (read-file-name
                          "File: "
                          (dired-dwim-target-directory)))))
            (if (file-newer-than-file-p file1 file2)
                (diff file2 file1)
              (diff file1 file2)))
        (error "Cannot diff more thatn 2 files"))))

  (defun my--dired-do-command (command)
    "Run COMMAND on marked files. Any files not already open will be opened.
After this command has been run, any buffers it's modified will remain
open and unsaved."
    (interactive "CRun on marked files M-x ")
    (save-window-excursion
      (mapc (lambda (filename)
              (find-file filename)
              (call-interactively command))
            (dired-get-marked-files))))


  (defun my--copy-file-to-clipboard (&rest @fnames)
    "Copy buffer/dired selection in the linux clipboard as URIs.
ALso accepts file-name strings as a paramater"
    (interactive)
    (let* (($file-list (cond (@fnames @fnames)
                             ((string-equal major-mode "dired-mode") (dired-get-marked-files))
                             ((buffer-file-name) (list (buffer-file-name)))
                             (t (user-error "Nothing selected!"))))
           (file-uri-list (cl-loop for file in $file-list collect (concat "file://" file)))
           (process-connection-type nil)
           (proc (start-process "xclip" nil "xclip" "-i" "-selection" "clipboard" "-t" "text/uri-list")))
      (process-send-string proc (mapconcat 'identity file-uri-list "\n"))
      (process-send-eof proc))))

Dired-x

Adds additional functionality on top of dired.

(use-feature dired-x
  :hook (dired-mode . dired-omit-mode)
  :general
  (general-define-key
   :states 'normal
   :keymaps 'dired-mode-map
   ")" 'dired-omit-mode)
  :config
  (setq dired-omit-verbose nil)

  (setq dired-omit-files "^\\.")

  ;; Set the default application when using ! or & on files.
  ;; Taken from doom emacs
  (setq dired-guess-shell-alist-user
	`(("\\.\\(?:docx\\|pdf\\|djvu\\|eps\\)\\'" "xdg-open")
	  ("\\.\\(?:jpe?g\\|png\\|gif\\|xpm\\)\\'" "xdg-open")
	  ("\\.\\(?:xcf\\)\\'" "xdg-open")
	  ("\\.csv\\'" "xdg-open")
	  ("\\.tex\\'" "xdg-open")
	  ("\\.\\(?:mp4\\|mkv\\|avi\\|flv\\|rm\\|rmvb\\|ogv\\)\\(?:\\.part\\)?\\'" "xdg-open")
	  ("\\.\\(?:mp3\\|flac\\)\\'" "xdg-open")
	  ("\\.html?\\'" "xdg-open")
	  ("\\.md\\'" "xdg-open"))))

Peep-Dired

(use-package peep-dired
  :general
  (general-nmap
    :keymaps 'dired-mode-map
    "M-RET" 'peep-dired)
  (general-nmap
    :keymaps 'peep-dired-mode-map
    "j" 'peep-dired-next-file
    "k" 'peep-dired-prev-file
    "C-n" 'peep-dired-next-file
    "C-p" 'peep-dired-prev-file
    "M-n" 'peep-dired-scroll-page-down
    "M-p" 'peep-dired-scroll-page-up
    "q" 'peep-dired)
  :config
  (add-hook 'peep-dired-hook 'evil-normalize-keymaps)
  :custom
  (peep-dired-cleanup-eagerly nil) ; breaks the mode if enabled!
  (peep-dired-ignored-extensions '("mkv mp4")))

Dired-Single

(use-package dired-single
  :disabled t
  :after dired
  :config
  (define-key dired-mode-map [remap dired-find-file]
    'dired-single-buffer)
  (define-key dired-mode-map [remap dired-mouse-find-file-other-window]
    'dired-single-buffer-mouse)
  (define-key dired-mode-map [remap dired-up-directory]
    'dired-single-up-directory))

Dired-Subtree

(use-package dired-subtree
  :after dired)

Dired-Narrow

(use-package dired-narrow
  :after dired
  :general
  (general-nmap
    :keymaps 'dired-mode-map
    "/" 'dired-narrow))

Trashed

(use-package trashed
  :commands trashed)

Recentf

Keeps a list of recently opened files.

(use-feature recentf
  :config
  (setq recentf-save-file (concat emacs-cache-dir "recentf"))
  (setq recentf-max-menu-item 300)
  (setq recentf-max-saved-item 300)
  (setq recentf-exclude
	'("recentf" ;; remove the recentf load file
	  ".*?autoloads.el$"
	  ".gitignore" ;; ignore `.gitignore' files in projects
	  "/tmp/" ;; ignore temporary files
	  "^/\\(?:ssh\\|su\\|sudo\\)?:" ;; ignore tramp/ssh files
	  ))
  (recentf-mode +1))

Prodigy

(use-package prodigy
  :defer t
  :general
  (leader-key
    "tp" 'prodigy)
  (general-nmap :keymaps 'prodigy-mode-map
    "+" 'prodigy-start
    "-" 'prodigy-stop)
  :config
  (prodigy-define-service
    :name "hugo"
    :command "hugo"
    :args '("server" "-D")
    :cwd "~/Programming/portfolio"))

NeoTree

Displays the folder tree

(use-package neotree
  :general
  (leader-key
    "ft" 'neotree)
  :init
  (setq neo-theme (if (display-graphic-p) 'icons 'arrow)))

Text & Organization

Org-mode

Configuration

Base

(use-package org
  :hook
  (org-mode . (lambda ()
                (setq paragraph-start "\\|[  ]*$"
                      paragraph-separate "[  ]*$")))
  
  (org-babel-after-execute . org-display-inline-images)
  :custom
  ;; Org knows how to handle its own buffers
  (org-directory org-dir)
  (org-use-fast-todo-selection t)
  (org-enforce-todo-dependencies t)
  (org-id-link-to-org-use-id 'create-if-interactive-and-no-custom-id)
  (org-id-locations-file (concat org-dir ".orgids"))
  (org-clone-delete-id t)
  (org-log-done 'time)
  (org-catch-invisible-edits 'show)
  (org-confirm-babel-evaluate nil)
  (org-return-follows-link t)
  (org-startup-indented nil)
  (org-hide-emphasis-markers t)
  (org-pretty-entities t)
  (org-startup-with-inline-images nil) ;; See hook in :config
  (org-export-with-smart-quotes t)
  (org-M-RET-may-split-line nil)
  (org-preview-latex-image-directory "/tmp/ltximg")

  :custom-face
  (org-default ((t (:family "PragmataPro" :height 1.0)))) ; Alternatively, Office Code Pro is second best
  (org-document-title ((t (:family "PragmataPro" :weight bold :height 2.0))))
  (org-document-info ((t (:family "PragmataPro"))))
  (org-done ((t (:strike-through t :weight bold))))
  (org-table-header ((t (:weight bold))))
  (org-headline-done ((t (:strike-through t))))
  (org-level-1 ((t (:height 1.2))))
  (org-level-2 ((t (:height 1.1))))
  (org-level-3 ((t (:height 1.1))))
  (org-link ((t (:underline t))))
  ;; FIXME Does not work at startup
  (org-block-end-line ((t (:inherit org-block-begin-line))))
  :config
  (set-popup-rule! "\\*Org" :ignore t)

  (run-with-idle-timer 60 t 'org-save-all-org-buffers)

  (setq org-format-latex-options (plist-put org-format-latex-options :scale 2))

  ;; Hide property drawers by default
  (add-hook 'org-cycle-hook #'org-cycle-hide-drawers)

  ;; See https://blog.tecosaur.com/tmio/2021-11-30-element.html
  (defun +org-image-resize-to-fit ()
    (when (derived-mode-p 'org-mode)
      (setq-local org-image-actual-width
                  (min 600 (window-width nil t)))
      (org-display-inline-images nil t)))

  (add-hook 'org-mode-hook #'+org-image-resize-to-fit)
  (add-hook 'olivetti-mode-hook #'+org-image-resize-to-fit)

  ;; Scrolling of inline images in org is a terrible experience, so we
  ;; allow them to be toggled individually with tab and C-c C-c. Alternatively,
  ;; https://github.com/casouri/lunarymacs/blob/master/site-lisp/iscroll.el
  ;; is a promising attempt to fix scrolling itself
  (defun org-toggle-inline-images-at-point ()
    (interactive)
    (when-let* ((link-region (org-in-regexp org-link-bracket-re 1)))
      (let ((org-inline-image-overlays-old org-inline-image-overlays))
        (save-restriction
          (narrow-to-region (car link-region) (cdr link-region))
          (if (-intersection (overlays-at (point)) org-inline-image-overlays)
              (mapc (lambda (ov)
                      (when (member ov org-inline-image-overlays)
                        (delete-overlay ov)
                        (setq org-inline-image-overlays (delete ov org-inline-image-overlays))))
                    (overlays-at (point)))
            (org-display-inline-images 'include-linked 'refresh))
          )
        (unless (equal org-inline-image-overlays org-inline-image-overlays-old) t)) ;; if overlays did not change, the link is not inline image
      ))

  ;; FIXME No longer running C-c C-c
  (add-hook 'org-tab-first-hook #'org-toggle-inline-images-at-point)
  (add-hook 'org-ctrl-c-ctrl-c-hook #'org-toggle-inline-images-at-point)

  ;; What's more, when using ipython etc. it is often hard to read
  ;; figure text since the background colour is dark.
  (defun create-image-with-background-color (args)
    "Specify background color of Org-mode inline image through modify `ARGS'."
    (if (eq major-mode 'org-mode)
        (let* ((file (car args))
               (type (cadr args))
               (data-p (caddr args))
               (props (cdddr args)))
          ;; get this return result style from `create-image'
          (append (list file type data-p)
                  (list :background "white")
                  props))
      args))
  (advice-add 'create-image :filter-args #'create-image-with-background-color))

Todo keywords

(setq org-todo-keywords
      (quote ((sequence "TODO(t)" "NEXT(n)" "|" "DONE(d)")
              (sequence "SOMEDAY(s)" "WAITING(w)" "HOLD(h)" "|" "CANCELLED(c)")))
      org-todo-state-tags-triggers
      (quote (("CANCELLED" ("CANCELLED" . t))
              ("WAITING" ("WAITING" . t))
              ("HOLD" ("WAITING") ("HOLD" . t))
              (done ("WAITING") ("HOLD"))
              ("TODO" ("WAITING") ("CANCELLED") ("HOLD"))
              ("NEXT" ("WAITING") ("CANCELLED") ("HOLD"))
              ("DONE" ("WAITING") ("CANCELLED") ("HOLD")))))

org-return-dwim

(with-eval-after-load 'org

  (defun unpackaged/org-element-descendant-of (type element)
    "Return non-nil if ELEMENT is a descendant of TYPE.
TYPE should be an element type, like `item' or `paragraph'.
ELEMENT should be a list like that returned by `org-element-context'."
    ;; MAYBE: Use `org-element-lineage'.
    (when-let* ((parent (org-element-property :parent element)))
      (or (eq type (car parent))
          (unpackaged/org-element-descendant-of type parent))))

  (defun +org-link-at-point-p ()
    "Return t if point is on a link."
    (member (org-element-type (org-element-context))
            '(citation citation-reference link)))

  (defun +org-return-dwim-normal ()
    "A helpful replacement for `org-return'.  With prefix, call `org-return'.
    ;; Inspired by John Kitchin: http://kitchingroup.cheme.cmu.edu/blog/2017/04/09/A-better-return-in-org-mode/"
    (interactive)
    (cond
     ;; Act depending on context around point.
     ((org-in-src-block-p) (org-babel-execute-src-block))
     ((+org-link-at-point-p) (org-open-at-point))
     ((org-at-item-checkbox-p)
      (org-toggle-checkbox))
     (t (org-return))))

  (defun +org-return-dwim-insert ()
    (interactive)
    (cond
     ((org-at-item-checkbox-p)
      ;; Checkbox: Insert new item with checkbox.
      (org-insert-todo-heading nil))
     ((org-in-item-p)
      ;; Plain list.
      (let ((context (org-element-context)))
        (if (or
             ;; List but not empty line
             (and (eq 'plain-list (car context))
                  (not (save-excursion
                         (beginning-of-line)
                         (looking-at "[ \t]*$"))))
             ;; Empty bullet
             (and (eq 'item (car context))
                  (not (eq (org-element-property :contents-begin context)
                           (org-element-property :contents-end context))))
             ;; Element in list item, e.g. a link
             (unpackaged/org-element-descendant-of 'item context))  
            (org-insert-item)
          ;; Empty item: Close the list.
          (delete-region (line-beginning-position) (line-end-position))
          (insert "\n"))))
     ((and (org-at-table-p)
           (save-excursion
             (beginning-of-line)
             ;; See `org-table-next-field'.
             (cl-loop with end = (line-end-position)
                      for cell = (org-element-table-cell-parser)
                      always (equal (org-element-property :contents-begin cell)
                                    (org-element-property :contents-end cell))
                      while (re-search-forward "|" end t))))
      ;; Empty row: end the table.
      (progn (delete-region (line-beginning-position) (line-end-position))
             (org-return)))
     (t
      (org-return))))

  (general-imap :keymaps 'org-mode-map
    "<RET>" '+org-return-dwim-insert)
  (general-nmap :keymaps 'org-mode-map
    "<RET>" '+org-return-dwim-normal))

Emphasis

Replace underline with highlight

(let* ((bg-dark (eq (frame-parameter nil 'background-mode) 'dark))
       (highlight-color (if bg-dark "#585858" "#fffbc2")))
  (setq org-emphasis-alist
        `(("*" bold)
          ("/" italic)
          ("_" (:background ,highlight-color))
          ("=" org-verbatim verbatim)
          ("~" org-code verbatim)
          ("+" (:strike-through t)))))

(disabled) Custom highlight face. From https://emacs.stackexchange.com/questions/38216/custom-faces-in-org-9-0. Here is possibly a better implementation: https://kitchingroup.cheme.cmu.edu/blog/2016/11/10/Persistent-highlighting-in-Emacs/

;;; Create highlighter face for marking up text in org-mode
(defface font-lock-highlight-face
  '((t (:inherit org-default :background "#585858")))
  "Face for highlighting text")
(defvar font-lock-highlight-face 'font-lock-highlight-face)

;;; Add keywords
(defun add-highlight-keywords()
  "adds custom keywords for highlighting text in org-mode."
  (font-lock-add-keywords nil
                          '(("\\(!\\)\\([^[:space:]][^\n\r\t]+[^[:space:]]\\)\\(!\\)" . 'font-lock-highlight-face ))))
(add-hook 'org-mode-hook 'add-highlight-keywords)

Pretty-symbols

(add-hook 'org-mode-hook (lambda ()
			   (push '("#+title: "        . "" ) prettify-symbols-alist)
			   (push '("#+author: "       . "" ) prettify-symbols-alist)
			   (push '("#+begin_src"      . "λ") prettify-symbols-alist)
			   (push '("#+end_src"        . "") prettify-symbols-alist)
			   (push '("#+results:"       . "") prettify-symbols-alist)
			   (push '(":results:"        . "") prettify-symbols-alist)
			   (push '("#+name:"          . "-") prettify-symbols-alist)
			   (push '("#+begin_example"  . "~") prettify-symbols-alist)
			   (push '("#+begin_example"  . "~") prettify-symbols-alist)
			   (push '("#+end_example"    . "~") prettify-symbols-alist)
			   (push '("#+end_example"    . "~") prettify-symbols-alist)
			   (push '("#+DOWNLOADED:"    . "") prettify-symbols-alist)
			   (push '("#+begin_verbatim" . "") prettify-symbols-alist)
			   (push '("#+end_verbatim"   . "") prettify-symbols-alist)
			   (push '("#+begin_verse"    . "") prettify-symbols-alist)
			   (push '("#+end_verse"      . "") prettify-symbols-alist)
			   (push '("#+begin_quote"    . "«") prettify-symbols-alist)
			   (push '("#+end_quote"      . "»") prettify-symbols-alist)
			   (push '("#+tblfm:"         . "") prettify-symbols-alist)
			   (push '("[X]"              . (?\[ (Br . Bl) ?✓ (Br . Bl) ?\])) prettify-symbols-alist)
			   (push '("\\\\"             . "") prettify-symbols-alist)
			   (prettify-symbols-mode)))

Capture

Set up capture templates. The backquoted list allows me to selectively evaluate parts of the list with a , (in this case the concat statement).

(setq org-capture-templates
      `(("t" "Todo" entry (file+headline ,(concat org-dir "refile.org") "Refile")
         "* TODO %?")
        ("p" "Project" entry (file+headline ,(concat org-dir "tasks.org") "Projects")
         "* %?")
        ("n" "Next" entry (file+headline ,(concat org-dir "refile.org") "Refile") 
         "* NEXT %? \n:PROPERTIES:\n:TRIGGER: next-sibling todo!(\"NEXT\") chain!(\"TRIGGER\") deadline!(cp)\n:END:\n")
        ("m" "mail" entry (file+olp ,(concat org-dir "refile.org") "Refile")
         "* TODO %? Link: %a")

        ("l" "Protocol" entry (file+headline ,(concat org-dir "refile.org") "Refile")
         "* TODO %? [[%:link][%(transform-square-brackets-to-round-ones \"%:description\")]]\n #+BEGIN_QUOTE\n%i\n#+END_QUOTE")	
        ("L" "Protocol Link" entry (file+headline ,(concat org-dir "refile.org") "Refile")
         "* TODO %? [[%:link][%(transform-square-brackets-to-round-ones \"%:description\")]]\n")))

;; Allow system-wide org-capture shortcut. Adapted from https://www.reddit.com/r/emacs/comments/74gkeq/system_wide_org_capture/

(defadvice org-switch-to-buffer-other-window
    (after supress-window-splitting activate)
  "Delete the extra window if we're in a capture frame"
  (if (equal "capture" (frame-parameter nil 'name))
      (delete-other-windows)))

(defadvice org-capture-finalize
    (after delete-capture-frame activate)
  "Advise capture-finalize to close the frame"
  (when (and (equal "capture" (frame-parameter nil 'name))
             (not (eq this-command 'org-capture-refile)))
    (delete-frame)))

(defadvice org-capture-refile
    (after delete-capture-frame activate)
  "Advise org-refile to close the frame"
  (when (equal "capture" (frame-parameter nil 'name))
    (delete-frame)))

(defun my--activate-capture-frame ()
  "Capture a todo in a new frame."
  (select-frame-by-name "capture")
  (switch-to-buffer (get-buffer-create "*scratch*"))
  (org-capture nil "t")
  (evil-insert-state))

;; System-wide org-agenda
(defadvice org-agenda-quit
    (after delete-capture-frame activate)
  "Advise capture-finalize to close the frame"
  (when (equal "agenda" (frame-parameter nil 'name))
    (delete-frame)))

Refile configuration

(use-feature org-refile
  :after org
  :init
  (setq org-refile-targets (quote ((nil :maxlevel . 3)
                                   (org-agenda-files :maxlevel . 3))))
  (setq org-refile-use-outline-path t)
  (setq org-outline-path-complete-in-steps nil)
  (setq org-refile-allow-creating-parent-nodes (quote confirm))
  (setq org-indirect-buffer-display 'current-window)
  :config
  (defun bh/verify-refile-target ()
    "Exclude todo keywords with a done state from refile targets"
    (not (member (nth 2 (org-heading-components)) org-done-keywords)))

  (setq org-refile-target-verify-function 'bh/verify-refile-target)

  ;; FIXME apply: Wrong number of arguments: (0 . 0), 3
  (defun +org-refile-save-org-buffers (&rest ignore)
    (org-save-all-org-buffers))
  (advice-add 'org-refile :after '+org-refile-save-org-buffers))

Agenda

Settings
(use-feature org-agenda
  :after org
  :commands org-agenda
  :hook (org-agenda-mode . (lambda ()
                             ;; (org-agenda-entry-text-mode)
                             (hide-mode-line-mode)))
  :general
  (general-def "<f1>" (lambda ()
                        (interactive)
                        (org-agenda nil "g")))
  :custom
  (org-agenda-sticky t)
  (org-agenda-remove-tags t)
  (org-agenda-breadcrumbs-separator "")
  (org-agenda-format-date "")
  (org-habit-graph-column 90)
  :custom-face
  ;; Distinguish scheduled items with overdue items
  (org-scheduled-today ((t (:foreground nil))))
  :config
  (setq org-deadline-warning-days 7)
  (setq org-agenda-dim-blocked-tasks nil)
  ;; (setq org-agenda-compact-blocks t)
  (setq org-agenda-block-separator nil)
  (setq org-agenda-entry-text-maxlines 1)

  ;; Use current window for agenda buffer
  (setq org-agenda-window-setup 'only-window)

  ;; Use all org files in org-dir
  (setq org-agenda-files (list org-dir))
  (setq org-agenda-skip-unavailable-files t)

  (setq org-agenda-skip-scheduled-if-done t)
  (setq org-agenda-skip-deadline-if-done t)

  ;; align tags to right side
  (setq org-agenda-tags-column -100)

  ;; Performance
  ;; https://orgmode.org/org.html#Speeding-Up-Your-Agendas
  (setq org-agenda-inhibit-startup t)
  (setq org-agenda-skip-unavailable-files t)

  (use-package gtd-agenda
    :straight (:host github :repo "akoen/gtd-agenda.el"))
  
  (setq org-agenda-span 'day)
  (setq org-agenda-custom-commands
        '(("g" "Get Things Done (GTD)"
           (
            (org-ql-block '(path "today")
                          ((org-ql-block-header "\nTODAY\n")))
            (agenda ""
                    ((org-deadline-warning-days 0)
                     (org-agenda-overriding-header "")))
            (+agenda-projects-block nil)
            (org-ql-block '(and (todo "NEXT") (not (ancestors (todo))))
                          ((org-ql-block-header "\nISOLATED TASKS\n")))
            (agenda nil
                    ((org-agenda-entry-types '(:deadline))
                     (org-agenda-format-date "")
                     (org-deadline-warning-days 7)
                     (org-agenda-overriding-header "\nDEADLINES\n")))
            (org-ql-block
             '(and (todo "TODO")
                   (or
                    (tags "refile")
                    ;; Catch lost tasks that are neither active nor part of a
                    ;; project.
                    (not
                     (or
                      (tags "noagenda")
                      (scheduled)
                      (deadline)
                      (ancestors (todo))
                      (children (or (todo) (done)))))))
             ((org-ql-block-header "\nREFILE\n")))
            (org-ql-block '(or (closed :on 0) (log-done :on 0))
                          ((org-ql-block-header "\nCOMPLETED TODAY\n")))))))

  (defun my--check-sync-conflicts ()
    (when (directory-files org-dir nil "conflict")
      (message "Warning: Sync conflicts")))

  (add-hook 'org-agenda-finalize-hook #'my--check-sync-conflicts)

  ;; Save all org buffers before exiting agenda
  (advice-add 'org-agenda-quit :before 'org-save-all-org-buffers))
org-ql
(use-package org-ql
  :defer t
  :config

  (setq org-ql-views
        '(("Deadlines"
           :buffers-files org-agenda-files
           :query (and (todo) (deadline auto)))))
  
  (defconst org-ql-log-done-regexp
    (rx bol "- State \"DONE\"" (1+ blank) "from" (1+ blank) (group (1+ not-newline))))

  (org-ql-defpred log-done (&key from to _on)
    "Return non-nil if current entry contains DONE state change in given period. "
    :normalizers ((`(,predicate-names ,(and num-days (pred numberp)))
                   ;; (clocked) and (closed) implicitly look into the past.
                   (let* ((from-day (* -1 num-days))
                          (rest (list :from from-day)))
                     (org-ql--normalize-from-to-on
                      `(log-done :from ,from))))
                  (`(,predicate-names . ,rest)
                   (org-ql--normalize-from-to-on
                    `(log-done :from ,from :to ,to))))
    :preambles ((`(,predicate-names ,(pred numberp))
                 (list :regexp org-ql-log-done-regexp :query t))
                (`(,predicate-names)
                 (list :regexp org-ql-log-done-regexp :query t)))
    :body
    (org-ql--predicate-ts :from from :to to :regexp org-ql-log-done-regexp :match-group 1)))

org-clock

(use-feature org-clock
  :after org
  :config
  (setq org-clock-out-remove-zero-time-clocks t))

org-modules

(use-feature org-install
  :after org
  :custom
  (org-modules '(org-habit))
  :config
  (org-load-modules-maybe t))
Habits
(use-feature org-habit
  :after org
  :config
  (setq org-log-repeat 'time
        org-log-into-drawer t)

  (defun org-habit-streak-count ()
    (save-excursion
      (goto-char (point-min))
      (while (not (eobp))
        ;;on habit line?
        (when (get-text-property (point) 'org-habit-p)
          (let ((streak 0)
                (counter (+ org-habit-graph-column (- org-habit-preceding-days org-habit-following-days))))
            (move-to-column counter)
            ;;until end of line
            (while (= (char-after (point)) org-habit-completed-glyph)
              (setq streak (+ streak 1))
              (setq counter (- counter 1))
              (backward-char 1))
            (end-of-line)
            (insert (concat "" (number-to-string streak)))))
        (forward-line 1))))
  (add-hook 'org-agenda-finalize-hook 'org-habit-streak-count))

SRC blocks

(use-feature ob
  :after org
  :general
  (general-nmap :keymaps 'override
    :predicate '(eq (org-element-type (org-element-at-point)) 'src-block)
    "S-<return>" 'org-babel-execute-src-block)

  :custom
  (org-edit-src-content-indentation 0)
  (org-src-preserve-indentation t)

  ;; See https://org-babel.readthedocs.io/en/latest/header-args/
  (org-babel-default-header-args '((:session . "none")
                                   (:results . "replace")
                                   (:exports . "both")
                                   (:cache . "no")
                                   (:noweb . "no")
                                   (:hlines . "no")
                                   (:tangle . "no"))))

We do not run org-babel-do-load-languages because it eagerly loads packages. See https://blog.d46.us/advanced-emacs-startup/.

(use-feature ob-python
  
  :commands (org-babel-execute:python))

;; See https://jordiinglada.net/wp/2015/03/25/scripting-in-c-2/
(use-feature ob-C
  :commands (org-edit-special org-babel-execute:C++ org-babel-execute:C))

(use-feature ob-matlab
  :commands (org-babel-execute:matlab)
  :custom
  (org-babel-default-header-args:matlab '((:session . "*MATLAB*"))))

(use-feature ob-java
  :commands (org-babel-execute:java))

Circular Bullets

Make bullets circular

(font-lock-add-keywords 'org-mode
                        '(("^ *\\([-]\\) "
                           0 (prog1 () (compose-region (match-beginning 1) (match-end 1) "")))
                          ("\\(->\\)"
                           0 (prog1 () (compose-region (match-beginning 1) (match-end 1) "")))))

Autosort

(defun my--org-entry-has-subentries ()
  "Any entry with subheadings"
  (let ((subtree-end (save-excursion (org-end-of-subtree t))))
    (save-excursion
      (org-back-to-heading)
      (forward-line 1)
      (when (< (point) subtree-end)
        (re-search-forward "^\*+ " subtree-end t)))))

(defun my--org-entry-sort-by-property nil
  (let ((property (org-entry-get (point) "SORT" 'INHERIT)))
    (when (and (not (seq-empty-p property))
               (my--org-entry-has-subentries))
      (funcall #'org-sort-entries nil (string-to-char property) nil nil nil)))
  (let ((property_second (org-entry-get (point) "SORT_AFTER" 'INHERIT)))
    (when (and (not (seq-empty-p property_second))
               (my--org-entry-has-subentries))
      (funcall #'org-sort-entries nil (string-to-char property_second) nil nil nil))))

(defun my--org-buffer-sort-by-property (&optional MATCH)
  (interactive)
  (org-map-entries #'my--org-entry-sort-by-property MATCH 'file)
  (org-set-startup-visibility))

                                        ;(add-hook 'org-mode-hook #'my--org-buffer-sort-by-property)

Packages

EVIL-Org

(use-package evil-org
  :straight (:host github :repo "hlissner/evil-org-mode")
  :after org
  :hook (org-mode . evil-org-mode)
  :hook (org-agenda-mode . evil-org-agenda-mode)
  :custom
  ;; Defines a bullet as the beginning of a sentence. Note that this regex
  ;; removes some of the complexity of the original definition, and might cause
  ;; problems.
  (sentence-end "\\(^\s*?- \\)\\|\\.\s*")
  :init
  ;; ;; HACK Should be done by evil-org earlier
  (evil-set-initial-state 'org-agenda-mode 'motion)
  :config
  ;; FIXME This does not get called initially if org loaded by calling org-agenda
  (evil-org-set-key-theme)
  (require 'evil-org-agenda)
  (evil-org-agenda-set-keys)

  (general-nmap :keymaps 'evil-org-mode-map
    "C-<return>" (lambda ()
                   (interactive)
                   (org-insert-heading-after-current)
                   (evil-append-line 0))))

Good-scroll

Better alternative to pixel-scroll-mode. Makes scrolling over images in org mode tolerable. Another promising alternative is https://github.com/casouri/iscroll.

(use-package good-scroll
  :disabled t
  :hook (org-mode . good-scroll-mode))

org-Bullets

Make the header bullets look prettier

(use-package org-superstar
  :hook (org-mode . org-superstar-mode)
  :custom
  ;; superstar hides leading stars itself
  (org-hide-leading-stars nil)
  (org-indent-mode-turns-on-hiding stars)

  (org-superstar-leading-bullet ?\s)
  (org-superstar-headline-bullets-list '("" "" "" "" ""))
  (org-superstar-item-bullet-alist '((?- . ?•)
                                     (?* . ?*)
                                     (?+ . ?+)))
  (org-ellipsis ""))

org-calfw

(use-package calfw
  :commands cfw:open-calendar-buffer
  :config
  ;; better frame. Taken from doom-emacs
  (setq cfw:face-item-separator-color nil
        cfw:render-line-breaker 'cfw:render-line-breaker-none
        cfw:fchar-junction ?╋
        cfw:fchar-vertical-line ?┃
        cfw:fchar-horizontal-line ?━
        cfw:fchar-left-junction ?┣
        cfw:fchar-right-junction ?┫
        cfw:fchar-top-junction ?┯
        cfw:fchar-top-left-corner ?┏
        cfw:fchar-top-right-corner ?┓))

(use-package calfw-org
  :general (leader-key "ot" 'cfw:open-org-calendar)
  :commands (cfw:open-org-calendar
             cfw:org-create-source
             cfw:open-org-calendar-withkevin
             my-open-calendar))

org-checklist

Allows for resetting of checkboxes when item is marked DONE.

(use-feature org-checklist
  :disabled t
  :after org)

org-download

Automatically insert images via drag-and-drop

(use-package org-download
  :after org
  :config
  (defun my--org-download-annotate-function (link)
    "Do not annotate link"
    "\n")

  (setq org-download-annotate-function #'my--org-download-annotate-function)
  (setq org-download-screenshot-method "maim -u -s %s")


  (defun my-org-download-method (link)
    "This is a helper function for org-download.
It creates a folder in the root directory named after the
org filename (sans extension) and puts all images from that file in there.
Inspired by https://github.com/daviderestivo/emacs-config/blob/6086a7013020e19c0bc532770e9533b4fc549438/init.el#L701"
    (let ((filename
	   (file-name-nondirectory
	    (car (url-path-and-query
		  (url-generic-parse-url link)))))
	  ;; Create folder name with current buffer name, and place in root dir
	  (dirname (concat "./img/"
			   (replace-regexp-in-string " " "_" (downcase (file-name-base buffer-file-name))))))

      ;; Add timestamp to filename
      (setq filename-with-timestamp (format "%s%s.%s"
					    (file-name-sans-extension filename)
					    (format-time-string org-download-timestamp)
					    (file-name-extension filename)))
      ;; Create folder if necessary
      (unless (file-exists-p dirname)
	(make-directory dirname))
      (expand-file-name filename-with-timestamp dirname)))
  (setq org-download-method 'my-org-download-method))

org-edna

Advanced dependency management

(use-package org-edna
  :after org
  :config
  (org-edna-mode))

org-journal

(use-package org-journal
  :after org
  :init
  (setq org-journal-cache-dir emacs-cache-dir
        org-journal-date-prefix "#+title: "
        org-journal-dir journal-dir
        org-journal-file-format "%Y-%m-%d.org"
        org-journal-date-format "%A %Y-%m-%d"
        org-journal-time-format "" ))

org-oxclip

Let you copy formatted org-mode content to the clipboard. Requires the package xclip to run.

(use-package htmlize
  :after org)

(use-package ox-clip
  :general
  (leader-key
    "oy" 'ox-clip-formatted-copy)
  :after org)

org-pomodoro

(use-package org-pomodoro
  :commands org-pomodoro
  :general
  (leader-key
    "op" 'org-pomodoro)
  (general-nmap
    :keymaps 'org-agenda-mode-map
    "P" 'org-pomodoro)

  :config
  ;; prefer PulseAudio to ALSA
  (setq org-pomodoro-audio-player (or (executable-find "paplay") org-pomodoro-audio-player)))

org-Protocol

Allows for external applications to trigger custom actions without external dependencies

(use-feature org-protocol
  :after org
  :config
  (defun transform-square-brackets-to-round-ones(string-to-transform)
    "Transforms [ into ( and ] into ), other chars left unchanged."
    (concat 
     (mapcar #'(lambda (c) (if (equal c ?\[) ?\( (if (equal c ?\]) ?\) c))) string-to-transform))))

org-transclusion

(use-package org-transclusion
  :disabled t
  :straight (:host github :branch "main" :repo "nobiot/org-transclusion")
  :hook (org-mode . org-transclusion-mode)
  :custom
  (org-transclusion-activate-persistent-message nil)
  (org-transclusion-include-first-section t))

org-appear

(use-package org-appear
  :hook (org-mode . org-appear-mode))

org-fragtog

(use-package org-fragtog
  :hook (org-mode . org-fragtog-mode))

Zettelkasten

org-roam

(use-package org-roam
  :hook (org-roam-mode . visual-line-mode)
  :general
  (leader-key
    "or" 'org-roam-buffer-toggle
    "oi" 'org-roam-node-insert
    "of" 'org-roam-node-find)
  :init
  (setq org-roam-v2-ack t)
  :custom
  (org-roam-directory brain-dir)
  (org-roam-capture-templates
   '(("d" "default" plain
      "%?"
      :if-new (file+head "${slug}.org"
                         "#+setupfile: ../hugo_setup.org
#+title: ${title}\n")
      :immediate-finish t
      :unnarowed t)
     ("b" "book" plain
      "%?"
      :if-new (file+head "${slug}.org"
                         "#+setupfile: ../hugo_setup.org
#+filetags: books
#+title: ${title}

- author ::
- tags ::

* The Book in 3 Sentences

* Impressions

* How the Book Changed Me

* Top 3 Highlights")
      :immediate-finish t
      :unnarowed t)))
  (org-roam-capture-ref-templates
   '(("r" "ref" plain
      "%?"
      :if-new (file+head "${slug}.org"
                         "#+title: ${title}\n")
      :unnarrowed t)))
  :config
  ;; Setting :select t in the popup-rule seems to break functionality
  (set-popup-rule! "\\org-roam" :side 'bottom :size 0.4 :ttl nil)
  (org-roam-db-autosync-mode 1))

org-roam-UI

Visualize the Zettelkasten.

(use-package org-roam-ui
  :after org-roam
  :straight
  (:host github :repo "org-roam/org-roam-ui" :branch "main" :files ("*.el" "out"))
  :general
  ;; This is annoyingly verbose, but I have not found a better way.
  (leader-key
    "og" (lambda () (interactive)(browse-url (concat "localhost:" (number-to-string org-roam-ui-port)))))
  :custom
  (org-roam-ui-sync-theme t)
  (org-roam-ui-follow t)
  (org-roam-ui-update-on-save t)
  (org-roam-ui-open-on-start nil)
  :config
  (org-roam-ui-mode 1))

Citations

bibtex
(setq +zotero-bibliography (concat docs-dir "zotero-bib.bib"))


(use-feature oc
  :after org
  :general
  (general-nmap :keymaps 'org-mode-map
    ;; FIXME: Does not work
    "C-c [" 'org-cite-insert))

(use-package citar
  :defer t
  :general
  (leader-key
    "ob" 'citar-open)
  :custom
  (citar-bibliography (list +zotero-bibliography))
  (org-cite-global-bibliography (list +zotero-bibliography))
  (org-cite-insert-processor 'citar)
  (org-cite-follow-processor 'citar)
  (org-cite-activate-processor 'citar)
  :config
  (with-eval-after-load 'embark
    (add-to-list 'embark-target-finders 'citar-citation-key-at-point)
    (add-to-list 'embark-keymap-alist '(bib-reference . citar-map))
    (add-to-list 'embark-keymap-alist '(citation-key . citar-buffer-map))))

Reviews

Daily review inspired by Ali Abdaal.

(defun +daily-review ()
  (interactive)
  (let ((file (concat org-dir "Reviews/daily/" (format-time-string "%Y-%m-%d") ".org"))
        (template (concat org-dir "Templates/daily-review.org")))
        (find-file file)
        (unless (file-exists-p file)
          (goto-char (point-max))
          (insert-file-contents template))))

(defun +weekly-review ()
  (interactive)
  (let ((org-capture-templates `(("w" "weekly review" entry (file+olp+datetree (lambda () (concat org-dir "Reviews/reviews_" (format-time-string "%Y") ".org")))
                                  (file ,(concat org-dir "Templates/weekly-review.org"))))))
    (progn
      (org-capture nil "w")
      (org-capture-finalize t)
      (org-speed-move-safe 'outline-up-heading)
      (org-narrow-to-subtree))))

(defun +monthly-review ()
  (interactive)
  (let ((org-capture-templates `(("m" "Monthly Review" entry (file+olp+datetree (lambda () (concat org-dir "Reviews/reviews_" (format-time-string "%Y") ".org")))
                                  (file ,(concat org-dir "Templates/monthly-review.org"))))))
    (progn
      (org-capture nil "m")
      (org-capture-finalize t)
      (org-speed-move-safe 'outline-up-heading)
      (org-narrow-to-subtree))))

LaTeX

(use-package latex
  :straight auctex
  :defer t
  :hook (LaTeX-mode . outline-minor-mode)
  :config
  ;; FIXME Not idempotent
  (add-hook 'LaTeX-mode-hook
            (lambda ()
              (prettify-symbols-mode)
              (setq-local TeX-command-default "+latexmk-file")))

  (setq TeX-save-query nil)
  (setq TeX-auto-save t)
  (setq Tex-parse-self t)
  (setq TeX-PDF-mode t)

  ;; Set up pdf viewer
  (setq TeX-view-program-list '(("Evince" "evince --page-index=%(outpage) %o")))
  (setq TeX-view-program-selection '((output-pdf "Evince")))
  ;; Method for enabling forward and inverse search 
  (setq TeX-source-correlate-method 'synctex)
  ;; inhibit the question to start a server process
  (setq TeX-source-correlate-start-server t)

  (defun +latexmk-file ()
    (interactive)
    (+vterm-run-command (concat "latexmk -pdf -pvc --interaction=nonstopmode '" buffer-file-name "'")))

  (defun +latex-insert-inkscape-figure ()
    (interactive)
    (let ((title (buffer-substring-no-properties (line-beginning-position) (line-end-position))))
      (kill-whole-line)
      (call-process "inkscape-figures" nil '(t nil) nil
                    "create"
                    title
                    (concat default-directory "figures/"))))

  (defun +latex-edit-inkscape-figure ()
    (interactive)
    (call-process "inkscape-figures" nil nil nil "edit" (concat default-directory "figures/")))

  (general-imap :keymaps 'TeX-mode-map
    "C-c C-f" #'+latex-edit-inkscape-figure)

  (general-nmap :keymaps 'TeX-mode-map
    "C-f" #'+latex-edit-inkscape-figure)
  )

CdLaTeX

(use-package cdlatex
  :after (:any latex org)
  :hook ((LaTeX-mode . cdlatex-mode)
	 (org-mode . org-cdlatex-mode))
  :custom
  (cdlatex-math-modify-prefix 59) ;; semicolon
  :config
  (add-to-list 'cdlatex-math-modify-alist
               '(66 "\\mathbb" nil t nil nil)))

RefTeX

Allow RefTeX to plug into AUCTeX

(use-package reftex
  :after latex
  :hook (LaTeX-mode . turn-on-reftex)
  :custom
  (reftex-plug-into-AUCTeX t)
  (reftex-extra-bindings t)
  (reftex-cite-format 'default)
  (reftex-use-external-file-handlers t))

evil-tex

(use-package evil-tex
  :hook (LaTeX-mode . evil-tex-mode))

Markdown

(use-package markdown-mode
  :general
  (general-nmap
    :keymaps 'markdown-mode-map
    :predicate '(markdown-link-p)
    "RET" 'markdown-follow-link-at-point)
  :init
  (setq-default markdown-hide-urls t)
  :custom
  (markdown-url-compose-char ""))

Writing

olivetti

(use-package olivetti
  :defer t
  :config
  (setq olivetti-body-width 80))

langtool

(use-package langtool
  :commands (langtool-check
             langtool-check-done
             langtool-show-message-at-point
             langtool-correct-buffer)
  :general
  (leader-key
    "llb" 'langtool-check-buffer
    "lld" 'langtool-check-done)
  :init
  (setq langtool-default-language "en-CA")
  :config
  (setq langtool-java-classpath "/usr/share/languagetool:/usr/share/java/languagetool/*"))

writegood

Checks for signs of bad writing.

(use-package writegood-mode
  :general
  (leader-key
    "lw" 'writegood-mode))

Focus

(use-package focus
  :defer t
  :custom
  (focus-mode-to-thing '((prog-mode . defun) (text-mode . paragraph))))

Writing Function

(define-minor-mode writing-mode
  "Mode for distraction-free writing."
  :init-value nil
  (if writing-mode
      (progn
        (olivetti-mode t)
        (display-line-numbers-mode 0)
        (variable-pitch-mode 1) ;; All fonts with variable pitch.
        (text-scale-increase 0.5)
        (hide-mode-line-mode 1)
        (focus-mode 1))
    (progn
      (olivetti-mode -1)
      (display-line-numbers-mode 1)
      (hide-mode-line-mode -1)
      (focus-mode 0)
      (variable-pitch-mode 0) ;; All fonts with variable pitch.
      (text-scale-decrease 0))))

Flyspell

;; find aspell and hunspell automatically
(use-package flyspell
  :hook (((org-mode markdown-mode TeX-mode rst-mode notmuch-message-mode git-commit-mode) . flyspell-mode)
         (prog-mode . flyspell-prog-mode))
  :custom
  (flyspell-prog-text-faces '(font-lock-comment-face font-lock-doc-face))
  :general
  (general-imap :keymaps '(org-mode-map markdown-mode-map)
    "C-'" 'flyspell-auto-correct-previous-word)
  :config
  (setq ispell-program-name "aspell"
        ispell-dictionary "ca"
        ispell-silently-savep t
        flyspell-issue-welcome-flag nil
        ;; Significantly speeds up flyspell, which would otherwise print
        ;; messages for every word when checking the entire buffer
        flyspell-issue-message-flag nil)

  ;; REVIEW There is likely a better way to preserve undo
  ;; Ensure that change is pushed to buffer-undo-list
  (defun +flyspell-correct-with-undo (orig-fn)
    (evil-start-undo-step)
    (funcall orig-fn)
    (evil-end-undo-step))
  (advice-add 'flyspell-auto-correct-word :around #'+flyspell-correct-with-undo))

(use-package flyspell-correct
  :general
  (general-nmap
    "z=" 'flyspell-correct-wrapper))

PDF

(use-package pdf-tools
  ;; REVIEW waiting on politza/pdf-tools#588
  :mode ("\\.[pP][dD][fF]\\'" . pdf-view-mode)
  :magic ("%PDF" . pdf-view-mode)
  :config

  (setq pdf-view-display-size 'fit-page)

  ;; HiDPI support
  (setq pdf-view-use-scaling t
	pdf-view-use-imagemagick nil)

  ;; revert pdf automatically after latex compilation completes in auctex
  (add-hook 'TeX-after-compilation-finished-functions #'TeX-revert-document-buffer)

  ;; automatically annotate highlights
  (setq pdf-annot-activate-created-annotations t)

  ;; add history for PDF files
  (add-hook 'pdf-view-mode-hook #'pdf-history-minor-mode))

(use-feature pdf-view
  :after pdf-tools
  :config
  (setq pdf-view-display-size 'fit-page)

  ;; HiDPI support
  (setq pdf-view-use-scaling t
	pdf-view-use-imagemagick nil))

(disabled) Math-preview

This is a very promising package, but it is not yet polished.

(use-package math-preview
  :disabled t
  :when (executable-find "math-preview")
  :straight (math-preview :type git
			  :host gitlab
			  :repo "matsievskiysv/math-preview"))

Email

Reading Email

Configuration adapted from Doom Emacs and https://github.com/rememberYou/.emacs.d/blob/master/config.org. Make sure install package mu!

(use-package mu4e
  :disabled t
  :commands mu4e mu4e-compose-new
  :hook (((mu4e-compose-mode mu4e-view-mode) . writing-mode)
         (dired-mode . turn-on-gnus-dired-mode)
         (mu4e-main-mode . mu4e-update-index))
  :general
  (leader-key
    "mm" 'mu4e
    "mc" 'mu4e-compose-new)
  (general-nmap :keymaps 'mu4e-main-mode-map
    "u" 'mu4e-update-index
    "U" 'mu4e-update-mail-and-index)
  :custom-face
  (mu4e-view-body-face ((t (:inherit default)))) ; Not variable-pitch
  :init
  (add-to-list 'load-path "/usr/share/emacs/site-lisp/mu4e")

  (setq mu4e-maildir "~/.mail")
  (setq mu4e-attachment-dir "~/Downloads")
  :config
  (setq mu4e-get-mail-command "mbsync -a")

  ;; Fixes duplicate UID errors
  (setq mu4e-change-filenames-when-moving t)

  (setq mu4e-compose-format-flowed t ; visual-line-mode + auto-fill upon sending
        fill-flowed-encode-column 998
        mu4e-view-show-addresses t
        mu4e-sent-messages-behavior 'sent
        mu4e-hide-index-messages t
        mu4e-headers-precise-alignment t
        ;; try to show images
        mu4e-view-show-images t
        mu4e-view-image-max-width 800
        mu4e-compose-dont-reply-to-self t
        mu4e-completing-read-function 'completing-read
        ;; Do not reply to self
        mu4e-compose-dont-reply-to-self t
        ;; set user agent
        mail-user-agent 'mu4e-user-agent
        ;; no need to ask
        mu4e-confirm-quit nil)

  ;; Folders
  (setq mu4e-sent-folder "/Sent"
        mu4e-drafts-folder "/Drafts"
        mu4e-trash-folder "/Trash"
        mu4e-refile-folder "/Archive"
        mu4e-compose-signature  "Alex Koen\nhttps://alexkoen.com")

  ;; Use fancy icons
  (setq mu4e-use-fancy-chars t
        mu4e-headers-draft-mark '("D" . "")
        mu4e-headers-flagged-mark '("F" . "")
        mu4e-headers-new-mark '("N" . "")
        mu4e-headers-passed-mark '("P" . "")
        mu4e-headers-replied-mark '("R" . "")
        mu4e-headers-seen-mark '("S" . "")
        mu4e-headers-trashed-mark '("T" . "")
        mu4e-headers-attach-mark '("a" . "")
        mu4e-headers-encrypted-mark '("x" . "")
        mu4e-headers-signed-mark '("s" . "")
        mu4e-headers-unread-mark '("u" . ""))

  ;; Auto update
  (setq mu4e-index-update-in-background t)

  ;; Saved queries
  (setq mu4e-bookmarks
        '(( :name  "Unread messages"
            :query "flag:unread AND NOT flag:trashed"
            :key ?u)
          ( :name "All in inbox"
            :query "maildir:/Inbox OR maildir:/Inbox/*"
            :key ?i)
          ( :name "Messages with images"
            :query "mime:image/*"
            :key ?p)))

  (add-to-list 'org-capture-templates
               `("m" "Mail" entry (file+olp ,(concat org-dir "refile.org") "Refile")
                 "* TODO %? Link: %a"))


  (defun +mu4e-attach-file (&optional file-to-attach)
    "Prompt for FILE-TO-ATTACH to message."
    (interactive "p")
    (if (eq major-mode 'mu4e-compose-mode)
        (let ((mail-buffer (current-buffer))
              (location (read-file-name "Attach: ")))
          (if (not (file-directory-p location))
              (save-excursion
                (goto-char (point-max))
                (unless (eq (current-column) 0)
                  (insert "\n\n")
                  (forward-line 2))
                (mail-add-attachment location)))
          (message "Cannot attach directory."))
      (message "Must attach from a compose buffer.")))

  (define-key mu4e-headers-mode-map (kbd "C-c c") 'mu4e-org-store-and-capture)
  (define-key mu4e-view-mode-map    (kbd "C-c c") 'mu4e-org-store-and-capture)

  ;; After refiling message immediately refresh buffer.
  (advice-add 'mu4e-mark-execute-all :after #'mu4e-headers-rerun-search)

  ;; Set from address based on address that message was sent to. See https://www.djcbsoftware.nl/code/mu/mu4e/Compose-hooks.html
  (defun my--set-from-address ()
    "Set the From address based on the To address of the original."
    (let ((msg mu4e-compose-parent-message)) ;; msg is shorter...
      (when msg
        (let ((to-addr (mu4e-message-contact-field-matches-me msg :to))
              (cc-addr (mu4e-message-contact-field-matches-me msg :cc)))
          ;; TODO Would be better not to permanently set user-mail-address
          (setq user-mail-address
                (cond
                 (to-addr (cdr to-addr))
                 (cc-addr (cdr cc-addr))
                 (t "akoen@mailbox.org")))))))

  (add-hook 'mu4e-compose-pre-hook #'my--set-from-address)


  ;; Smart refile: move message sent by me to /Sent and messages sent by others
  ;; to "/Archive"
  (defun +mu4e-get-refile-folder (msg)
    (if (mu4e-message-sent-by-me msg)
        "/Sent"
      "/Archive"))
  (setq mu4e-refile-folder '+mu4e-get-refile-folder)

  ;; Remove the +T flag from trash. This emulates the behaviour of GUI clients. Emails are otherwise deleted when moving to trash.
  (setf (alist-get 'trash mu4e-marks)
        (list :char '("d" . "")
              :prompt "dtrash"
              :dyn-target (lambda (target msg)
                            (mu4e-get-trash-folder msg))
              :action (lambda (docid msg target)
                        ;; Here's the main difference to the regular trash mark,
			                  ;; no +T before -N so the message is not marked as
			                  ;; IMAP-deleted:
			                  (mu4e~proc-move docid (mu4e~mark-check-target target) "-N"))))

  ;; Html mails might be better rendered in a browser
  (add-to-list 'mu4e-view-actions '("View in browser" . mu4e-action-view-in-browser))

  (set-popup-rule! "\\*mu4e" :ignore t))

Sending Mail

Create ~/.authinfo.gpg

machine imap.mailbox.com login alex@koen.ca password <password> port 993
machine smtp.mailbox.com login alex@koen.ca password <password> port 587
(use-feature epa
  :custom
  (epa-pinentry-mode 'loopback)
  :config
  (epa-file-enable)
  (require 'auth-source)
  (auth-source-forget-all-cached))

(use-feature sendmail
  :custom
  (send-mail-function 'sendmail-send-it)
  (sendmail-program (executable-find "msmtp"))
  :config
  (set-popup-rule! "sendmail errors"))

(use-feature message
  :custom
  (message-sendmail-envelope-from 'header)
  (message-send-mail-function 'sendmail-send-it)
  (message-signature "Alex Koen\nhttps://alexkoen.com")
  (message-kill-buffer-on-exit t) ; close after sending
  :config
  (defun +message-check-for-subject ()
    "Prevent user from sending email without a subject."
    (unless (message-field-value "Subject")
      (message-goto-subject)
      (user-error "Attempting to send mail without a subject line")))
  (add-hook 'message-send-hook #'+message-check-for-subject))
  

Tools

Calc

(use-feature calc
  :general
  (leader-key
    "=" '(:wk "calc")
    "==" 'calc
    "=q" 'quick-calc))
  

Development

Tools

Diff

(use-feature diff
  :config
  (set-popup-rule! "\\*Diff\\*" :size 0.4 :select t))

Edebug

(use-feature edebug
  :custom
  (edebug-save-windows nil)
  :config
  (defun +edebug-add-instrumentation (fun)
    "Interactively call edebug-instrument-function."
    (interactive "aFunction symbol: ")
    (edebug-instrument-function fun))
  (with-eval-after-load 'embark
    (define-key embark-command-map (kbd "e") 'my--edebug-add-instrumentation)))

(require 'edebug-x)

Eldoc

(use-feature eldoc
  :custom
  (eldoc-idle-delay 0.1))

Eval

Framework, heavily inspired by Doom Emacs, for evaluating buffer code in major-mode specific “runner” functions.

(defvar +eval-runners '()
  "Alist mapping major-modes to runner functions which evaluate buffer code.")

(defun +eval/region (beg end)
  "Eval region with major-mode runner."
  (let ((runner (alist-get major-mode +eval-runners)))
    (if runner (funcall runner beg end)
      (message "No runner found for %s" major-mode))))

(evil-define-operator +eval:region (beg end)
  "Evaluate selection with current major-mode runner."
  :move-point nil
  (interactive "<r>")
  (+eval/region beg end))

(general-nvmap :keymaps 'override "gr" '+eval:region)

Lookup

(defvar +lookup-documentation-handlers '()
  "Mode-functions to run when looking up documentation.")

(defun +lookup-documentation ()
  (interactive)
  (let ((handler (alist-get major-mode +lookup-documentation-handlers)))
    (if handler (funcall handler)
      (message "No documentation handler found for %s. See `+lookup-documentation-handlers'." major-mode))))

(general-nvmap :keymaps 'override
  "K" '+lookup-documentation)

LSP

Eglot

(use-package eglot)

Folding

;; Adapted from doom emacs
(defun increase-selective-display ()
  (interactive)
  (if (eq selective-display nil)
      (set-selective-display tab-width)
      (set-selective-display (+ selective-display tab-width))))

(defun decrease-selective-display ()
  (interactive)
  (cond ((eq selective-display tab-width) (set-selective-display nil))
         ((eq selective-display nil))
         (t (set-selective-display (- selective-display tab-width)))))

(general-nmap "C->" 'increase-selective-display)
(general-nmap "C-<" 'decrease-selective-display)

(use-feature hideshow
  :hook (prog-mode . hs-minor-mode)
  :config

  (defun display-code-line-counts (ov)
    (when (eq 'code (overlay-get ov 'hs))
      (overlay-put ov 'display
                   (format "\t[%d...]"
                           (count-lines (overlay-start ov)
                                        (overlay-end ov))))))

  ;; Do not fold comments
  (setq hs-hide-comments-when-hiding-all nil)
  (setq hs-set-up-overlay #'display-code-line-counts)
  (add-hook 'prog-mode-hook 'hs-minor-mode))

Smerge

(use-feature smerge-mode
  :general
  (general-nmap
    :keymaps 'smerge-mode-map
    "RET" 'smerge-keep-current))

Flycheck

(use-package flycheck
  :hook (prog-mode . flycheck-mode)
  :custom
  (flycheck-indication-mode nil)
  :config
  ;; Suppress display of flycheck errors in insert state
  (advice-add #'flycheck-display-error-at-point :around
              (defun +flycheck-suppress-in-insert-mode (orig-fn)
                (when (not (eq evil-state 'insert))
                  (funcall orig-fn)))))

grep

(use-package wgrep
  :general
  (:keymaps 'wgrep-mode-map
            "C-c C-c" 'wgrep-finish-edit
            "C-c C-k" 'wgrep-abort-changes)
  (:keymaps 'grep-mode-map
            "C-c C-p" 'wgrep-change-to-wgrep-mode)
  :custom
  (wgrep-auto-save-buffer t))

Comment auto-fill

(use-feature simple
  :hook (prog-mode . (lambda () (auto-fill-mode) (display-fill-column-indicator-mode)))
  :general
  (general-nmap "M-SPC" 'cycle-spacing)
  :custom
  (comment-auto-fill-only-comments t)
  (fill-column 80))

EditorConfig

(use-package editorconfig
  :config
  (editorconfig-mode 1))

Direnv

The best way to manage virtual environment.

For example, to create a new python project:

echo 'layout python3' > .envrc
direnv allow
pip install <packages>
(use-package direnv
  :config
  (direnv-mode 1))

rainbow-mode

rainbow-mode shows hex code colours in buffers.

(use-package rainbow-mode)

Symex

(use-package symex
  :general
  (general-nmap :keymaps 'emacs-lisp-mode-map
    "C-;" 'symex-mode-interface)
  :custom
  (evil-symex-state-cursor "green")
  :config
  (symex-initialize))

Lang

C/C++

To configure a new C program:

  1. Create a CMakeLists.txt file. Eg:
cmake_minimum_required(VERSION 3.10)

# set the project name
project(Tutorial)

# add the executable
add_executable(Tutorial tutorial.cxx)
  1. Run the following commands:
cmake -H. -BDebug -DCMAKE_BUILD_TYPE=Debug -DCMAKE_EXPORT_COMPILE_COMMANDS=YES
ln -s Debug/compile_commands.json

Where -BDebug specifies the build directory (Debug)

CMake

(use-package cmake-mode
  :mode ("CMAKELists\\.txt\\'" "\\.cmake\\'"))

(use-package cmake-font-lock
  :after (cmake-mode)
  :hook (cmake-mode . cmake-font-lock-activate))

Clojure

(use-package clojure-mode
  :hook (clojure-mode . eglot))

(use-package cider
  :general
  (leader-key :keymaps 'cider-mode-map
    "'" 'cider-jack-in-clj

    "eb" 'cider-load-buffer
    "ed" 'cider-eval-defun-at-point
    "ee" 'cider-eval-last-sexp

    "rn" 'cider-repl-set-ns)
  :hook (clojure-mode . cider-mode)
  :init
  (set-popup-rules!
    '(("^\\*cider-repl"         :vslot 2 :select t :ttl nil :size 0.4)
      ("^\\*cider-error*"       :vslot 3 :select t)
      ("^\\*cider-repl-history" :vslot 4 :ttl nil)
      ("^\\*cider-profile"      :side 'right)))
  :custom
  (cider-repl-result-prefix ";; => ")
  (cider-repl-display-help-banner nil)
  :config

  (add-to-list '+eval-runners '(clojure-mode . cider-eval-region))

  ;; Evil collection does not support certain functionality in cider-debug, so
  ;; we disable it and use the standard bindings.
  (add-hook 'cider--debug-mode-hook
            (defun my--clojure--cider-setup-debug ()
              "Setup cider debug to override evil keys cleanly."
              (evil-make-overriding-map cider--debug-mode-map 'normal)
              (evil-normalize-keymaps))))

(use-package clj-refactor
  :after clojure-mode)

;; Requires clj-kondo system package
(use-package flycheck-clj-kondo
  :after clojure-mode)

Common Lisp

(use-package sly
  :general
  (general-nmap
    :keymaps
    'sly-mode-map
    ;; sly-mrepl default to switch-to-buffer
    "gz" (lambda () (interactive) (sly-mrepl 'pop-to-buffer)))
  (leader-key
    :keymaps 'lisp-mode-map
    "'"  'sly
    "cc" 'sly-compile-file
    "cC" 'sly-compile-and-load-file
    "cf" 'sly-compile-defun
    "cl" 'sly-load-file
    "cn" 'sly-remove-notes
    "cr" 'sly-compile-region)
  :custom
  (inferior-lisp-program "sbcl")
  :init
  (set-popup-rules!
    '(("^\\*sly-mrepl"       :vslot 2 :select t :size 0.4 :ttl nil)
      ("^\\*sly-compilation" :vslot 3 :ttl nil)
      ("^\\*sly-traces"      :vslot 4 :ttl nil)
      ("^\\*sly-description" :vslot 5 :size 0.3 :ttl 0)
      ;; Do not display debugger or inspector buffers in a popup window. These
      ;; buffers are meant to be displayed with sufficient vertical space.
      ("^\\*sly-\\(?:db\\|inspector\\)" :ignore t)))

  :config
  (add-to-list '+eval-runners '(lisp-mode . sly-eval-region))

  (defun +common-lisp-lookup-documentation ()
    (command-execute #'sly-describe-symbol))
  (add-to-list '+lookup-documentation-handlers '(lisp-mode . +common-lisp-lookup-documentation)))

CSharp

(use-package csharp-mode
  :mode "\\.cs\\'"
  :hook (csharp-mode . eglot))

(use-package shader-mode
  :mode ("\\.shader\\'"
          "\\.compute\\'"))

Using omnisharp with Unity

For some reason, the OmniSharp language server uses a bundled version of mono instead of the global one. In VSCode, you can change this by setting the option useGlobaMono to true. In Emacs, edit the run shell script as shown here:

# mono_cmd=${bin_dir}/mono
mono_cmd=mono

Docker

(use-package dockerfile-mode
  :mode "\\Dockerfile\\'")

Elisp

(use-feature elisp
  :general
  (leader-key :keymaps 'emacs-lisp-mode-map
    "ed" 'eval-defun
    "el" 'load-library
    "ee" 'pp-eval-last-sexp)

  :init
  (defun +eval-emacs-lisp (beg end)
    "Runner for emacs-lisp."
    (eval (read (format "(progn %s)"
                        (buffer-substring-no-properties beg end)))))
  (add-to-list '+eval-runners '(emacs-lisp-mode . +eval-emacs-lisp)))

(use-feature ielm
  :init
  (set-popup-rule! "\\*ielm\\*" :ttl nil))

(use-package eros
  :hook (emacs-lisp-mode . eros-mode)
  :config
  (defun +eval-emacs-lisp-overlay (orig-fn &rest args)
    (eros--eval-overlay
     (apply orig-fn args)
     (point)))
  (advice-add #'+eval-emacs-lisp :around #'+eval-emacs-lisp-overlay))

GLSL

For OpenGl shader programming.

(use-package glsl-mode
  :mode ("\\.vs\\'"
         "\\.fs\\'"
         "\\.glsl\\'"
         "\\.geom\\'"
         "\\.frag\\'"
         "\\.vert\\'"))

Haskell

(use-package haskell-mode
  :mode "\\.hs\\'")
  ;; :hook (haskell-mode . lsp-deferred))

Java

(use-package gradle-mode
  :hook (java-mode . gradle-mode)
  :general
  (leader-key
    :keymaps 'java-mode-map
    "cb" 'gradle-build))

Javascript

js2-mode

;; Mostly borrowed from https://github.com/CSRaghunandan/.emacs.d/blob/master/setup-files/setup-js.el
(use-package js2-mode
  :mode "\\.m?js\\'"
  :interpreter "node"
  :init 
  (with-eval-after-load 'projectile
    (add-to-list 'projectile-globally-ignored-directories "node_modules"))
  :config
  ;; have 2 space indentation by default
  (setq js-indent-level 2
        js2-basic-offset 2
        js-chain-indent t)

  ;; Try to highlight most ECMA built-ins
  (setq js2-highlight-level 3)
  ;; have a shorter idle time delay
  (setq js2-idle-timer-delay 0.1)

  ;; turn off all warnings in js2-mode
  (setq js2-mode-show-parse-errors t
        js2-mode-show-strict-warnings nil
        js2-strict-missing-semi-warning nil
        js2-strict-trailing-comma-warning nil))

prettier-js

(use-package prettier-js
  :hook ((js2-mode rjsx-mode json-mode) . prettier-js-mode)
  :custom (prettier-js-args '("--print-width" "100"
                              "--single-quote" "true"
                              "--trailing-comma" "all")))

rjsx-mode

(use-package rjsx-mode
  :mode "components/.+\\.js$")

Json

(use-package json-mode
  :mode "\\.json\\'")

Julia

(use-package julia-mode
  :mode "\\.jl\\'")


(setq +julia-math-symbol-alist
      '((?a "\\alpha" "\\bfalpha")
        (?b "\\beta" "\\bfbeta")
        (?d "\\delta" "\\bfdelta")
        (?e "\\epsilon" "\\bfepsilon")
        (?f "\\phi" "\\bfphi")
        (?g "\\gamma" "\\bfgamma")
        (?h "\\eta" "\\bfeta")
        (?k "\\kappa" "\\bfkappa")
        (?l "\\lambda" "\\bflambda")
        (?m "\\mu" "\\bfmu")
        (?n "\\nu" "\\bfnu")
        (?o "\\omega" "\\bfomega")
        (?p "\\pi" "\\bfpi")
        (?q "\\theta" "\\bftheta")
        (?r "\\rho" "\\rho")
        (?s "\\sigma" "\\bfsigma")
        (?t "\\tau" "\\bftau")
        (?u "\\upsilon" "\\bfupsilon")
        (?w "\\xi" "\\bfxi")
        (?x "\\chi" "\\bfchi")
        (?y "\\psi" "\\bfpsi")
        (?z "\\zeta" "\\bfzeta")))

(defun +julia-insert-math-symbol ()
  "Substitute a julia latex math symbol using cdlatex."
  (interactive)
  (require 'cdlatex)
  (let* ((cell (cdlatex-read-char-with-help
                +julia-math-symbol-alist
                1 2
                "Latex symbol level %d of %d: "
                "AVAILABLE MATH SYMBOLS.  [%c]=next level "
                cdlatex-math-symbol-prefix
                (get 'cdlatex-math-symbol-alist-comb 'cdlatex-bindings)))
         (char (car cell))
         (level (cdr cell))
         (entry (assoc char +julia-math-symbol-alist))
         (symbol (nth level entry)))
    (if (or (not symbol)
            (not (stringp symbol))
            (equal symbol ""))
        (error "No such math symbol %c on level %d" char level))
    (insert symbol)
    (julia-latexsub)))

Lua

(use-package lua-mode
  :mode "\\.lua\\'"
  :interpreter "lua")

MATLAB

I used to dislike MATLAB, but the effort they put in to supporting Linux and Emacs earns my admiration. Karthink has a great set of modifications in his config for adding functionality.

(use-package matlab-mode
  :mode "\\.m\\'"
  :general
  (:states 'normal :keymaps 'matlab-mode-map "gz" 'matlab-shell)
  :hook (matlab-mode . flycheck-mode)
  :custom
  ;; Works in org. Other methods seem to need a file
  (matlab-shell-run-region-function 'matlab-shell-region->commandline)
  (matlab-shell-use-emacs-toolbox nil)
  :init
  (setq-default mlint-show-warnings t)
  (with-eval-after-load 'flycheck
    (flycheck-define-checker matlab
      "Checker for matlab"
      ;; FIXME This should REALLY be a variable
      ;; :command ((eval mlint-program))
      :command ("/usr/local/MATLAB/R2020b/bin/glnxa64/mlint" source)
      :error-patterns
      ((warning line-start "L " line " (C " (1+ digit)  "): " (message) line-end))
      ;; ((warning line-start "L " line " (C " column "-" column "): " (id (* alnum)) ":" (message))
      ;; (warning line-start "L " line " (C " column "): " (id (* alnum)) ":" (message)))
      :modes matlab-mode)
    (push 'matlab flycheck-checkers))

  (set-popup-rules! '(("\\*MATLAB\\*" :ttl nil)
                      (".*?MATLAB Help.*")))

  :config
  (matlab-cedet-setup))

Python

Arch packages:

  1. flake8 for flycheck
  2. python-black for style
  3. python-language-server for lsp
(use-feature python
  :mode ("/Pipfile\\'" . conf-mode)
  :custom
  (python-indent-guess-indent-offset-verbose nil)
  ;; https://github.com/gregsexton/ob-ipython/issues/89
  (python-shell-prompt-detect-failure-warning nil)
  :config
  ;; Must be called manually to initiate jupyter :config block
  (set-popup-rule! "\\*Python\\*" :ttl nil))

(use-package pyenv-mode
  :after python
  :hook ((python-mode . pyenv-mode)
         (projectile-switch-project . projectile-pyenv-mode-set))
  :custom (pyenv-mode-set "3.8.5")
  :preface
  (defun projectile-pyenv-mode-set ()
    "Set pyenv version matching project name."
    (let ((project (projectile-project-name)))
      (if (member project (pyenv-mode-versions))
          (pyenv-mode-set project)
        (pyenv-mode-unset))))

  :config
  ;; Pyenv overrides the org-schedule keybind
  (define-key pyenv-mode-map (kbd "C-c C-s") nil)
  (define-key pyenv-mode-map (kbd "C-M-s") 'pyenv-mode-set))


(use-package blacken
  :commands blacken-mode
  ;; :hook (python-mode . blacken-mode)
  )

(use-package sphinx-doc
  :after python
  :hook (python-mode . sphinx-doc-mode))

(use-package python-docstring
  :hook ((python-mode . python-docstring-mode)))

(use-package python-pytest
  :general
  (leader-key
    :keymaps 'python-mode-map
    "ct" 'python-pytest-dispatch)
  :commands python-pytest-dispatch)

Sxhkd

Pretty niche

(use-package sxhkd-mode
  :mode "sxhkdrc\\'"
  :custom
  (sxhkd-mode-reload-config t)
  :straight (:host github :repo "xFA25E/sxhkd-mode"))

Web

web mode

;; Config mostly stolen from https://github.com/raxod502/radian/blob/develop/emacs/radian.el
(use-package web-mode
  :mode (("\\.phtml\\'" . web-mode)
         ("\\.tpl\\.php\\'" . web-mode)
         ("\\.[agj]sp\\'" . web-mode)
         ("\\.as[cp]x\\'" . web-mode)
         ("\\.erb\\'" . web-mode)
         ("\\.mustache\\'" . web-mode)
         ("\\.djhtml\\'" . web-mode)
         ("\\.html?\\'" . web-mode))
  :config
  (setq web-mode-enable-auto-closing t
        web-mode-auto-close-style 2 ;; 2 is a nice number I suppose
        web-mode-enable-auto-quoting nil ;; messes with jsx
        web-mode-enable-auto-pairing nil
        web-mode-markup-indent-offset 2
        web-mode-code-indent-offset 2
        web-mode-css-indent-offset 2)
  (add-to-list 'web-mode-content-types-alist
               '("jsx" . "\\.js[x]?\\'"))

  (setq web-mode-engines-alist
        '(("go" . "layouts/.+\\.html$")))

  ;; Fix comments in javascript mode
  (let ((types '("javascript" "jsx")))
    (setq web-mode-comment-formats
          (cl-remove-if (lambda (item)
                          (member (car item) types))
                        web-mode-comment-formats))
    (dolist (type types)
      (push (cons type "//") web-mode-comment-formats))))

(use-package gulp-task-runner
  :defer t)

restclient

;; Make sure to use comments (#) as separators
(use-package restclient
  :mode (("\\.rest" . restclient-mode)))

Yaml

(use-package yaml-mode
  :mode "\\.yml\\'")

Functions

Align Comments in Region

(defun my--align-comments (beginning end)
  "Align comments in region"
  (interactive "*r")
  (align-regexp beginning end (concat "\\(\\s-*\\)"
                                      (regexp-quote comment-start)) nil 2))
(leader-key
  "c=c" 'my--align-comments)

About

Emacs configuration

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published