Skip to content

Latest commit

 

History

History
5284 lines (4352 loc) · 152 KB

config.org

File metadata and controls

5284 lines (4352 loc) · 152 KB

Doom Emacs configuration

;; -*- lexical-binding: t -*-

Code Style

Variable Naming

All my private functions have the prefix +my. If I extend a module I will prefix it with the +module.

Furthermore the naming schema follows these rules:

  • +my:variable: Variable
  • +my/function: A private function
  • +my|function: An interactive function

Code style

Most of the code here will follow a functional mindset using dash.el and s.el. I will prefer those functions over built in functions if they are nicer to use.

I often use my helper function (template) for template strings.

Packages

;; -*- no-byte-compile: t; -*-

General Packages

Libraries

noflet

Since flet was deprecated, I’m using this for now. Pretty much only used in Expand snippet by name.

(package! noflet)

System

Udiskie

(package! udiskie :recipe (:host gitlab :repo "floscr/udiskie.el"))

Systemd daemons

Manage systemd from emacs

(package! daemons)

Evil

Evil Plugin provides some nice addons for Evil Mode

(package! evil-plugin :recipe (:host github :repo "tarao/evil-plugins"))

Evil motions

(package! evil-replace-with-register)
(package! evil-text-objects-javascript :recipe (:host github :repo "urbint/evil-text-objects-javascript"))

UI

Narrow to an indirect buffer

(package! narrow-indirect :recipe (:host github :repo "emacsmirror/narrow-indirect"))

Colorized hex strings

(package! rainbow-mode)

Centered buffers, doom does not support this anymore.

(package! visual-fill-column)

Ivy Avy

Move to ivy candidate with C-'

(package! ivy-avy)

Doom Themes

(package! doom-themes
  :recipe (:host github :repo "floscr/emacs-doom-themes" :files ("*.el" "themes/*.el"))
  :pin nil)

External

Edit the current chrome input field directly in emacs

(package! atomic-chrome)

Dired

Filter dired buffers

(package! dired-narrow)
(package! dired-filter)

Open subtrees directly in the current view

(package! dired-subtree)

Programming

(package! edbi)

Etc

Cheat.sh

(package! cheat-sh)

Narrow Proced Buffers

(package! proced-narrow)

Transmission Interface

(package! transmission)

Language Packages

Elisp

Symex for editing emacs lisp by structure.

(package! symex)

Beancount

(package! beancount :recipe (:host github :repo "cnsunyour/beancount.el"))

Javascript

Indium: Javascript debugging environment

(package! indium)

Impatient Mode: Live editing of html

(package! impatient-mode)

Eslintd Fix: Autofixing that isn’t slow

(package! eslintd-fix)

JS Import: Package importing

(package! js-import :recipe (:host github :repo "floscr/js-import"))

Jest: Test Runner

(package! jest :recipe (:host github :repo "floscr/emacs-jest"))

Graphql

(package! graphql)

Git

Show changes in current branch

(package! git-lens)

Quickly download and browser repositories from github

(package! elescope)

Ebooks

(package! nov)

Literate Calc Mode

Inline Calculation

(package! literate-calc-mode)

Overrides

Doom Snippets

(package! doom-snippets :ignore t)
(package! my-doom-snippets
  :recipe (:host github
           :repo "floscr/doom-snippets"
           :files ("*.el" "*")))

Calfw

Continuous events are broken in the current calfw source, also it seems it isn’t maintained anymore. This Commit fixes the behavior.

(package! calfw :recipe (:host github :repo "floscr/emacs-calfw") :pin "e3d04c253230ed0692f161f527d4e42686060f62")
(package! calfw-org :recipe (:host github :repo "floscr/emacs-calfw") :pin "e3d04c253230ed0692f161f527d4e42686060f62")
(package! calfw-ical :pin "e3d04c253230ed0692f161f527d4e42686060f62")
(package! calfw-cal :disable t)
(package! org-gcal :disable t)

Disabled Packages

Remove those annoying LSP interface plugins

(package! lsp-ui :disable t)
(package! merlin-eldoc :disable t)

Configuration

External Files

Secrets

Config files that I don’t want to share with the world. They will be stored in here:

(defvar +my:secrets-config-file nil
  "My private config file.")
(setq +my:secrets-config-file "~/.config/secrets.el")

And I will load them on system start:

(defun +my/load-secrets-config ()
  (-some->> +my:secrets-config-file
    (-id-when 'file-exists-p)
    (load-library)))

(+my/load-secrets-config)

Defaults

Garbage Collection

Set it to 32 MiB.

(setq doom-gc-cons-threshold 33554432)

Lookup search sources

(setq +lookup-provider-url-alist
  '(("DuckDuckGo"         "https://duckduckgo.com/?q=%s")
    ("DuckDuckGo Lucky"   "https://duckduckgo.com/?q=\\%s")
    ("Github Code"        "https://github.com/search?search&q=%s&type=Code")
    ("Code grepp.app"     "https://grep.app/search?q=%S")
    ("Google"             "https://google.com/search?q=%s")
    ("Google images"      "https://google.com/images?q=%s")
    ("Google maps"        "https://maps.google.com/maps?q=%s")
    ("NPM"                "https://npmjs.com/search?q=%s")
    ("Hoogle"             "https://www.haskell.org/hoogle/?hoogle=%s")
    ("Project Gutenberg"  "http://www.gutenberg.org/ebooks/search/?query=%s")
    ("Explain Shell"      "https://explainshell.com/explain?cmd=%s")
    ("StackOverflow"      "https://stackoverflow.com/search?q=%s")
    ("Github"             "https://github.com/search?ref=simplesearch&q=%s")
    ("Youtube"            "https://youtube.com/results?aq=f&oq=&search_query=%s")
    ("Wolfram alpha"      "https://wolframalpha.com/input/?i=%s")
    ("Wikipedia"          "https://wikipedia.org/search-redirect.php?language=en&go=Go&search=%s")))

Evil Fine undo

Whether evil actions like cw are undone in several steps. This is sometimes annoying, as it might need you to press u multiple times. But I prefer the fine grained control, as I’m often staying longer in insert mode, and don’t want one single undo action for the whole “session”.

(setq evil-want-fine-undo t)

Move items to trash on delete

(setq
 trash-directory "~/.Trash/"
 delete-by-moving-to-trash t)

Automatically reload tags files

(setq tags-revert-without-query 1)

Safe Local Variables

Variables that I want to safely set from .dir-locals files.

(put '+file-templates-dir 'safe-local-variable #'stringp)

Directories

(defcustom downloads-dir "~/Downloads/"
  "Downloads Directory"
  :type 'string)

(defcustom screenshot-dir "~/Pictures/Screenshots"
  "Screenshots Directory"
  :type 'string)

Disable Blinking Cursor

Never blink the cursor, it’s too distracting.

(blink-cursor-mode -1)
(setq blink-matching-paren nil)
(setq visible-cursor nil)

Buffer naming

(setq uniquify-buffer-name-style 'forward)

Disable bookmarks

(setq bookmark-save-flag nil)

;; Can't set to nil as bookmarks are still set up to hooks
;; Instead I'll keep it in this file which will be trashed on every reboot
(setq bookmark-file "/tmp/emacs-bookmarks-file")

Default Browser

Why does this not work out of the box?

(pcase (getenv "BROWSER")
    ("chromium" (setq browse-url-browser-function 'browse-url-chromium))
    ("firefox" (setq browse-url-browser-function 'browse-url-firefox)))

Authentication

(add-to-list 'auth-sources "~/.config/gnupg/.authinfo.gpg")

Custom Functions & Modes

Unfill Paragraph

Fix a paragraph that was formatted to a fill column.

(defun +my|unfill-paragraph ()
  "Fix a paragraph that was formatted to a fill column."
  (interactive)
  (let ((fill-column (point-max)))
    (fill-paragraph nil)))

Jump to definition for tangled files

Take me to the literate source file when using find-function etc.

(defadvice! +org|try-org-babel-tangle-jump-to-org (&optional arg1)
  :after '(find-function
           find-variable)
  (ignore-errors
    (org-babel-tangle-jump-to-org)))

Hydras

Line Spacing

Change and reset line-spacing for all buffers.

(defvar +ui-default-line-spacing line-spacing)
(defvar +ui-default-line-spacing-increment 1)
(defvar +ui-big-line-spacing-increment 10)

(defun +ui/set-line-spacing (&optional increment)
  "Set the line spacing
When no line spacing is given is the default-line-spacing"
  (setq-default line-spacing (+ (or increment +ui-default-line-spacing-increment) line-spacing)))

(defun +ui|reset-line-spacing ()
  (interactive)
  (setq-default line-spacing +ui-default-line-spacing))

(defun +ui|increase-line-spacing ()
  (interactive)
  (+ui/set-line-spacing))

(defun +ui|decrease-line-spacing ()
  (interactive)
  (+ui/set-line-spacing (- +ui-default-line-spacing-increment)))

(defun +ui|increase-line-spacing-big ()
  (interactive)
  (+ui/set-line-spacing +ui-big-line-spacing-increment))

(defun +ui|decrease-line-spacing-big ()
  (interactive)
  (+ui/set-line-spacing (- +ui-big-line-spacing-increment)))

(evil-define-key 'normal 'global (kbd "]z") #'+line-spacing/step/body)

;;;###autoload (autoload '+common-lisp/macrostep/body "lang/common-lisp/autoload/hydras" nil nil)
(defhydra +line-spacing/step (:exit nil :hint nil :foreign-keys run :color pink)
  "
Macro Expansion
^^Definitions                           ^^Compiler Notes             ^^Stickers
^^^^^^─────────────────────────────────────────────────────────────────────────────────────
[_r_] Reset
[_]_] Expand
[_[_] Collapse
[_}_] Expand Big
[_{_] Collapse Big
"
  ("r" +ui|reset-line-spacing)
  ("]" +ui|increase-line-spacing)
  ("[" +ui|decrease-line-spacing)
  ("}" +ui|increase-line-spacing-big)
  ("{" +ui|decrease-line-spacing-big)
  ("q" nil "cancel" :color blue))

Expand Region

Expand visual region using a hydra. Double press v to enable.

(defhydra hydra-expand-region ()
   "region: "
   ("f" er/mark-defun "defun")
   ("v" er/expand-region "expand")
   ("V" er/contract-region "contract"))

(evil-define-key 'visual 'global (kbd "v") #'hydra-expand-region/body)

Custom Modes

Evil edit register

Edit registers with +evil-edit-register|counsel. Mostly used to edit the macro registers q.

(defvar +evil-edit-register:register "")
(defvar +evil-edit-register-mode-map (make-sparse-keymap))

(define-minor-mode +evil-edit-register-mode
  "Edit evil register and save it back to the register."
  :keymap +evil-edit-register-mode-map)

(map! :map +evil-edit-register-mode-map
      "C-c C-c" #'+evil-edit-register|save-and-exit
      "C-c C-k" #'kill-buffer-and-window)

(defun +evil-edit-register|save-and-exit (&optional arg)
  "Save the buffer content back to the register register"
  (interactive)
  (evil-set-register
   (string-to-char +evil-edit-register:register)
   (buffer-substring-no-properties (point-min) (point-max)))
  (kill-buffer-and-window))

(defun +evil-edit-register|counsel (register-string)
  "Edit evil register in register"
  (require 'noflet)
  (interactive (noflet ((counsel-evil-registers-action (x) x))
                 (list (counsel-evil-registers))))
  (-when-let* ((register-string (substring-no-properties register-string))
               (buffer (generate-new-buffer (t! "*Evil Register Edit: <<register-string>>*")))
               ((_ reg register) (s-match "^\\[\\(.\\)\\]: \\(.*\\)$" register-string)))
    (pop-to-buffer buffer)
    (with-current-buffer buffer
      (+evil-edit-register-mode 1)
      (setq-local +evil-edit-register:register reg)
      (setq header-line-format "Edit, then exit with 'C-c C-c', abort with 'C-c C-k'.")
      (save-excursion
        (insert register)))))

UI

Zen Mode & Variable Pitch Fonts

(setq +zen-text-scale 1.5)

(let ((height 140)
      (size 23))
  (setq doom-variable-pitch-font (font-spec :family "IBM Plex Sans" :size size)
        doom-serif-font (font-spec :family "IBM Plex Sans" :size size)))

Frame padding

(add-hook! '(window-setup-hook after-make-frame-functions)
  (defun +ui/init-frame-ui (&optional frame)
    "Re-enable menu-bar-lines in GUI frames."
    (when-let (frame (or frame (selected-frame)))
      (when (display-graphic-p frame)
        (set-frame-parameter frame 'internal-border-width 15)))))

System specific window modifications

Resize the window font size etc according to the system. This will be disabled in terminal mode.

(when (display-graphic-p) (+ui|adjust-ui-to-display))

Theme Toggle

Toggle between a light and a dark theme. Bound to SPC t t.

(defconst light-theme 'doom-one)
(defconst dark-theme  'doom-one-light)

(defun +doom|toggle-theme ()
  "Toggle between light and dark themes."
  (interactive)
  (cond ((eq doom-theme dark-theme)
         (message "Toggling to light-theme: %s" light-theme)
         (setq doom-theme light-theme)
         (doom/reload-theme))
        ((eq doom-theme light-theme)
         (message "Toggling to dark-theme: %s" dark-theme)
         (setq doom-theme dark-theme)
         (doom/reload-theme))
        (t (message "Toggling theme is not possible. Theme is not currently light-theme (%s) or dark-theme (%s)." light-theme dark-theme))))

Theme Modifications

General

(add-hook 'doom-load-theme-hook #'*doom-themes-custom-set-faces)
Function Start
(defun *doom-themes-custom-set-faces ()
  (set-face-attribute 'fringe nil
                      :foreground (face-background 'default)
                      :background (face-background 'default))
  (custom-set-faces!
Dired Output

Remove the rainbow colors from dired.

'(diredfl-read-priv :foreground "#80899E")
'(diredfl-write-priv :foreground "#80899E")
'(diredfl-exec-priv :foreground "#80899E")
'(diredfl-other-priv :foreground "#80899E")

'(all-the-icons-dired-dir-face :foreground "#80899E")

'(diredfl-dir-priv :foreground "#282C34")
'(diredfl-k-modified :foreground "#FF8E90")

'(diredfl-number :foreground "#80899E")
'(diredfl-date-time :foreground "#49505F")
`(diredfl-dir-name :foreground "#2DADF2")
Mu4E

Switch the highlight.

'(mu4e-highlight-face :inherit mu4e-unread-face)
Org Mode

Remove the ugly grey background

'(org-column :background nil)
Function End
))

Hooks

Diff Highlighting
(add-hook! '(diff-hl-margin-minor-mode-hook)
  (progn
    (set-face-attribute 'smerge-refined-added nil
                        :foreground (doom-blend "#98be65" "#3e493d" 0.15)
                        :background (doom-lighten "#98bb5d" 0.2))
    (set-face-attribute 'smerge-refined-removed nil
                        :foreground (doom-blend "#ff6c6b" "#4f343a" 0.2)
                        :background (doom-lighten "#ff6c6b" 0.1))))

(add-hook! '(magit-status-mode-hook magit-diff-mode-hook)
           (progn
                   (set-face-attribute 'diff-refine-added nil
                                       :foreground (doom-blend "#98be65" "#3e493d" 0.15)
                                       :background (doom-lighten "#98bb5d" 0.2))
                   (set-face-attribute 'diff-refine-removed nil
                                       :foreground (doom-blend "#ff6c6b" "#4f343a" 0.2)
                                       :background (doom-lighten "#ff6c6b" 0.1))))

Scrolloff

Start scrolling X lines before the end of a screen. Disable for certain modes (terminal & ivy) where the window is to small.

(setq scroll-conservatively 10)
(setq scroll-margin 10)

(add-hook 'term-mode-hook (cmd! (setq-local scroll-margin 0)))
(add-hook 'ivy-mode-hook (cmd! (setq-local scroll-margin 0)))

Popups

Other Popup overrides

(set-popup-rules!
  '(("^\\*Org Agenda" :side right :size 0.55 :select t :modeline t :ttl nil :quit nil)
    ("^\\*Org Src" :ignore t)
    ("^\\*Org QL View: \\(Work \\)?Projects*" :side right :size 0.55 :select t :modeline t :ttl nil :quit nil)
    ("^\\*PDF-Occur*" :side right :size 0.5 :select t :modeline t)
    ("^\\*WoMan " :side right :size 0.5 :select t :modeline t :ttl nil :quit nil)
    ("^\\*helm" :vslot -100 :size 0.32 :ttl nil)
    ("^\\*helpful command" :side right :size 0.5 :select t :modeline t :ttl nil :quit nil)
    ("^\\*nodejs" :side right :size 0.55 :select t :modeline t :ttl nil :quit nil)
    ("^\\*jest" :select nil)
    ("^\\*projectile-files-errors" :ignore t)
    ("^\\*elfeed-entry" :modeline t :ttl nil)))

(Visual) Fill Column

(setq-default fill-column 110)
(setq fill-column 110)
(setq visual-fill-column-width fill-column)

(setq visual-fill-column-center-text t
      visual-fill-column-width fill-column)

Disable trailing whitespace warning

(setq-hook! 'prog-mode-hook show-trailing-whitespace nil)

Fix underline

Draw the underline at the bottom of the text, not at the end of line-spacing.

(setq x-underline-at-descent-line nil)

Features / Custom Packages

Lisp Navigation ly

A navigation system for lisp languages that I’m comfortable with, like evil but with a more sexp based navigation.

Alternatives I’ve used:

  • countvajhula/symex.el My main inspiration. Too tree based for my taste, I after have to think about the code structure before I can execute actions.

Minor Mode

(set-frame-parameter nil 'internal-border-width 15)
(defvar +ly-mode-map (make-sparse-keymap))

(define-minor-mode +ly-mode
  "Open org headlines in an indirect window buffer."
  :keymap +ly-mode-map)

(map! :map +ly-mode-map
      :n "gj"     #'evil-next-line
      :n "gk"     #'evil-previous-line
      :n "M-j"    #'+ly|forward-line
      :n "M-k"    #'+ly|backward-line
      :n "M-u"    #'+ly|up-atom
      :n "M-f"    #'+ly|forward-atom
      :n "M-b"    #'+ly|backward-atom
      :n "M-l"    #'+ly|forward-brace
      :n "M-h"    #'+ly|backward-brace
      :n "M-,"    #' symex-shift-backward
      :n "M-."    #'symex-shift-forward
      :n "M-o"    #'symex-open-line-after
      :ni "<C-return>" #'+ly|add-atom
      :n "M-RET" (cmd!
                  (parinfer-toggle-mode)
                  (parinfer-lispy:newline)
                  (parinfer-toggle-mode))
      :n "M-Y" (cmd!
                (symex-yank)
                (symex-paste-after)
                (symex-insert-newline)))

;; HACK: map not working intially
(defun +ly/activate-mode-hook ()
  (require 'symex)
  (evil-insert-state)
  (evil-normal-state))

;; Mappings only get applied after reinserting normal mode
(add-hook '+ly-mode-hook #'+ly/activate-mode-hook)

Utils

Sexp Navigation
(setq +ly:brace-regexp "([^() ]")
(setq +ly:atom-regexp "[^() ]")

(defun +ly|forward-atom ()
  "Go to next atom."
  (interactive)
  (require 'symex)
  (symex-traverse-forward)
  (while (or (eq ?\( (char-after)))
    (symex-traverse-forward)))

(defun +ly|backward-atom ()
  "Go to next atom."
  (interactive)
  (require 'symex)
  (symex-traverse-backward)
  (while (or (eq ?\( (char-after)))
    (symex-traverse-backward)))

(defun +ly|forward-brace ()
  "Go to next sexp brace."
  (interactive)
  (require 'symex)
  (symex-go-up)
  (search-forward-regexp +ly:brace-regexp nil t)
  (backward-char 2))

(defun +ly|backward-brace ()
  "Go to previous sexp brace."
  (interactive)
  (require 'symex)
  (unless (eq ?\( (char-after))
    (symex-go-down))
  (search-backward-regexp +ly:brace-regexp nil t))

(defun +ly|up-atom ()
  "Go up the tree"
  (interactive)
  (require 'symex)
  (symex-go-down))
Line Navigation
(defun +ly|forward-line ()
  "Function docstring"
  (interactive)
  (next-line 1)
  (goto-char (point-at-bol))
  (search-forward-regexp "\s*[^\s]" nil t)
  (backward-char 1))

(defun +ly|backward-line ()
  "Function docstring"
  (interactive)
  (previous-line 1)
  (goto-char (point-at-bol))
  (search-forward-regexp "\s*[^\s]" nil t)
  (backward-char 1))
List Modification
Add to list
(defun +ly/inside-string? ()
  "Returns non-nil if inside string, else nil.
Result depends on syntax table's string quote character."
  (nth 3 (syntax-ppss)))

(defun +ly|add-atom (&optional same-line?)
  "Add atom in the current sexp. If the cursor is under a string match the type and insert another string."
  (interactive "P")
  (let ((was-parinfer-list-mode (eq 'paren parinfer--mode)))
    (up-list 1 t)
    (unless same-line?
      (parinfer-lispy:newline))
    (when same-line? (insert " "))
    (cond ((+ly/inside-string?)
           (insert (t! "\"\""))
           (backward-char 1)))
    (evil-insert-state)))

Hydra

(defhydra +ly|hydra ()
   "Ly Navigation"
   ("f" (+ly|forward-brace) "Forward ()" :column "Navigate")
   ("b" (+ly|backward-brace) "Backward ()")
   ("f" (+ly|forward-brace) "forward ()" :column "Navigate")
   ("b" (+ly|backward-brace) "Backward ()")
   ("j" (+ly|forward-line) "Next Line")
   ("k" (+ly|backward-line) "Backward Line"))

Indirect Indent Mode

Adds minor mode for editing indented source code in an indirect buffer, with the indentation reset to 0. Saving and committing keeps the indentation in the source buffer.

(defvar-local +indirect-indent 0)

(defvar +indirect-indent-mode-map (make-sparse-keymap))

(define-minor-mode +indirect-indent-mode
  "Editing indented source code without the indent in an indirect buffer."
  :keymap +indirect-indent-mode-map)

(map! :map +indirect-indent-mode-map
      :gni "s-s" #'edit-indirect-save)

(add-hook! +indirect-indent-mode
           (defun +indirect-indent/init-hook ()
             (setq header-line-format
                   "Edit, then exit with 'C-c C-c', abort with 'C-c C-k'.")))

(advice-add #'edit-indirect-commit :before #'+indirect-indent/restore-indentation)
(advice-add #'edit-indirect-save :after #'+indirect-indent/remove-indentation)
(advice-add #'edit-indirect-save :before #'+indirect-indent/restore-indentation)

(defun +indirect-indent/restore-indentation ()
  (when (and (bound-and-true-p +indirect-indent-mode)
             (not (eq +indirect-indent 0)))
    (without-undo
      (indent-rigidly (point-min) (point-max) (+ +indirect-indent)))))

(defun +indirect-indent/remove-indentation ()
  (when (and (bound-and-true-p +indirect-indent-mode)
             (not (eq +indirect-indent 0)))
    (without-undo
      (indent-rigidly (point-min) (point-max) (- +indirect-indent)))))

(defun +indirect-indent|edit (beg end &optional with-mode)
  "Edit script in an indirect buffer."
  (interactive)
  (edit-indirect-region beg end t)
  (let ((indent (indent-rigidly--current-indentation (point-min) (point-max))))
    (unless (eq indent 0)
      (without-undo
       (indent-rigidly (point-min) (point-max) (- indent)))
      ;; Local variables get undone when calling a mode
      ;; So we have to define the major mode before
      (funcall with-mode)
      (+indirect-indent-mode 1)
      (setq +indirect-indent indent))))

Scanning

(defvar +docs:dir nil
  "The main directory to store the docs in.")
(defvar +docs:index-file nil
  "The main file to store all scans.")

(setq +docs:dir (f-expand "~/Documents/Scans"))
(setq +docs:index-file (f-join +docs:dir "Scans.org"))

(defun +docs/add-entry ()
  "Adds an entry in the main index file."
  (interactive)
  (find-file (f-expand +docs:index-file))
  (goto-char (point-max))
  (newline)
  (goto-char (point-max))
  (yas/insert-by-name "Scan Template")
  (+org|add-created-property))

(defun +docs/get-attachment-path ()
  "Get the attachment path from the current entry."
  (require 'org-ml)
  (->>
   (org-ml-parse-this-headline)
   (org-ml-headline-get-node-property "DOCUMENT")
   (+org/link-url-or-original)
   (s-replace-regexp "^attachment:" "")))

(defun +docs/join-attachment-pdfs (original add)
  "Foo"
  (shell-command-to-string (t! "concat-pdfs <<original>> <<add>>")))

(defun +docs|scan-document ()
  "Scan a document and attach it to a new entry in the main docs file."
  (interactive)
  (require 'org-download)
  (let ((org-scan-path (f-expand +docs:index-file)))
    (+docs/add-entry)
    (let ((marker (point))
          (org-attach-id-dir "./.attach")
          (org-attach-directory "./.attach")
          (org-attach-dir-relative t))
      (deferred:$
        (deferred:process "scan")
        (deferred:nextc it
          `(lambda (x)
             (save-excursion
               (find-file ,org-scan-path)
               (goto-char ,marker)
               (let* ((old-path
                       (->> x
                            (s-split "\n")
                            (-butlast)
                            (-last-item)))
                      (new-path (expand-file-name (org-download--fullname old-path))))
                 (progn
                     (copy-file old-path new-path)
                     (org-set-property "DOCUMENT" (funcall org-download-link-format-function new-path)))))))))))

(defun +docs|scan-document-append ()
  "Scan a document and attach it to a new entry in the main docs file."
  (interactive)
  (require 'org-download)
  (let ((org-scan-path (f-expand +docs:index-file)))
    (let ((marker (point))
          (org-attach-id-dir "./.attach")
          (org-attach-directory "./.attach")
          (org-attach-dir-relative t))
      (deferred:$
        (deferred:process "scan")
        (deferred:nextc it
          `(lambda (x)
             (save-excursion
               (find-file ,org-scan-path)
               (goto-char ,marker)
               (let* ((old-path
                       (->> x
                            (s-split "\n")
                            (-butlast)
                            (-last-item))))
                 (+docs/join-attachment-pdfs (f-join +docs:dir (+docs/get-attachment-path)) old-path)
                 (-log "Merging scanned documents finished.")))))))))

(defun +docs|dired-attach-document ()
  "Function docstring"
  (interactive)
  (let ((old-path (->> (dired-file-name-at-point)
                       (f-expand))))))

Language Config

Javascript

Config

(after! js2-mode
  :config
  (setq
   flycheck-javascript-eslint-executable (executable-find "eslint_d")
   flycheck-disabled-checkers '(javascript-jshint javascript))
  (add-hook 'js2-mode-hook #'eslintd-fix-mode))

Utils

(defun +js/match-const-function-name (line)
  "Matches a line to the word after the declaration"
  (nth 2 (s-match
          "\\(const\\|let\\|class\\)\s\\(.+?\\)\s"
          line)))

(defun +js/const-function-at-point ()
  "Returns the current function name at the current line"
  (+js/match-const-function-name (thing-at-point 'line t)))

Functions

Export default variable
(defun js2r-export-default ()
  "Exports the current declaration at the end of the file"
  (interactive)
  (save-excursion
    (let* ((name (+js/const-function-at-point)))
      (goto-char (point-max))
      (insert "\n")
      (insert (template "export default <<name>>;")))))
Extract constant to file

Extract the const under the cursor into a new file.

(defun js2r-extract-const-to-file ()
  "Extracts function to external file"
  (interactive)
  (let* ((name (+js/const-function-at-point))
         (path (concat "./" name ".js")))
    (evil-digit-argument-or-evil-beginning-of-line)
    (js2r-kill)
    (f-write-text "" 'utf-8 path)
    (find-file path)
    (yank)))
Generate index.js file index

Generate a file index in the current file for every other file in the current directory.

(defun +js/index-file-names (&optional dir)
  "Get filenames from current buffers directory."
  (let ((fs (directory-files (or dir default-directory) nil ".*\\.js")))
    (mapcar 'f-no-ext
            (remove "index.js" fs))))

(defun +js|generate-index (&optional dir ignore-list)
  "Generate an index import file for files in directory.
Pass DIR for directory, falls back to default-directory
Pass IGNORE-LIST for a list of files "
  (interactive)
  (erase-buffer)
  (let* ((dir (or dir default-directory))
         (fs (-->
              (+js/index-file-names dir)
              (if ignore-list
                  (--remove (-contains? ignore-list it) it)
                it))))
    (message "%s" fs)
    (mapc (lambda (f) (insert "import " f " from './" f "';\n")) fs)
    (insert "\n")
    (insert "export default {\n")
    (mapc (lambda (f) (insert "    " f ",\n")) fs)
    (insert "};")))
Convert expression into template string

Converts an expression into a template string.

Example: When you would call the function on the foo inside the console.log, It would wrap it like this console.log(`${foo}`).

const foo = 'Foo'
console.log(`${foo}`)
(defun +js|convert-sexp-to-template-string ()
  "Wrap sexp into a template string"
  (interactive)
  (kill-sexp)
  (insert (concat "`${" (substring-no-properties (car kill-ring)) "}`"))
  (pop kill-ring))
Split / Join Tag Elisp
(defvar +rjsx:closing-tag-regexp nil)
(setq +rjsx:closing-tag-regexp "[^=]\\\\?>")

(defun +rjsx|split-join-tag ()
  "Split or join the xml attributes list in the current tag."
  (interactive)
  (if-let ((closing-tag (save-excursion
                          (rjsx-jump-opening-tag)
                          (re-search-forward +rjsx:closing-tag-regexp (point-at-eol) t))))
      (save-excursion
        (rjsx-jump-opening-tag)
        (evil-forward-WORD-begin 1)
        (while (< (point) closing-tag)
          (newline-and-indent)
          (evil-forward-WORD-begin 1))
        (previous-line)
        (re-search-forward +rjsx:closing-tag-regexp (point-at-eol) t)
        (backward-char 1)
        (newline-and-indent))
    (save-excursion
      (rjsx-jump-opening-tag)
      (while (not (re-search-forward +rjsx:closing-tag-regexp (point-at-eol) t))
        (next-line)
        (join-line))
      (search-forward-regexp +rjsx:closing-tag-regexp (point-at-eol) t)
      (backward-char 2)
      (when (= (char-after) ?\s)
        (delete-char 1)))))
Slit / Join Tag Node

Stolen from the vscode extension: dannyconnell/vscode-split-html-attributes I don’t want to translate this to elisp, and js works as well.

const useSpacesForTabs = true;
const tabSize = 4;
const sortOrder = [];
const closingBracketOnNewLine = true;

let input = process.argv[2]

// count the number of lines initally selected
let lineCount = input.split('\n').length
let lineText = input

// get the initial white space at the start of the line
let initialWhitespaceRegex = /\s*/i
let initialWhitespace = lineText.match(initialWhitespaceRegex)[0]

// get the opening tag
let openingTagRegex = /^[^\s]+/
let openingTag = input.match(openingTagRegex)[0]

// remove opening tag and trim
input = input.replace(openingTagRegex, '')
input = input.trim()

// get the ending bracket (if it's a "/>")
let endingBracket = ''
if (input.endsWith('/>')) {
    endingBracket = '/>'
}
else {
    endingBracket = '>'
}

// remove ending bracket and trim
if (endingBracket == '/>') {
    input = input.replace('/>', '')
}
else {
    input = input.substring(0, input.length - 1)
}
input = input.trim()

// create the indentation string
let indentationString
if (useSpacesForTabs == false) {
    indentationString = '\t'
}
else {
    indentationString = ' '.repeat(tabSize)
}

// regex to select all spaces that aren't within quotes
let spacesRegex = /\s+(?=([^"]*"[^"]*")*[^"]*$)/g

// get attributes into an array
let attributesString = input.replace(spacesRegex, '\n')
let attributesArray = attributesString.split('\n')

// sort the attributes array
let sortedAttributesArray = []
if (sortOrder.length) {

    // loop through sortOrder array
    sortOrder.forEach(search => {
        // loop through attributesArray
        let itemsToMove = []
        attributesArray.forEach((item, index) => {
            if (item.match(search)) {
                itemsToMove.push(index)
                // attributesArray.splice(index, 1)
            }
        })
        // move matched items from attributesArray to sortedAttributesArray (and sort them)
        let tempMatchedItems = []
        itemsToMove.forEach(indexItem => {
            tempMatchedItems.push(attributesArray[indexItem])
        })
        tempMatchedItems.sort()
        sortedAttributesArray.push(...tempMatchedItems)

        // remove matched items from attributesArray
        for (var i = itemsToMove.length - 1; i >= 0; --i) {
            attributesArray.splice(itemsToMove[i], 1)
        }
    })

    // sort remaining attributes and add to sortedAttributesArray
    attributesArray.sort()
    sortedAttributesArray.push(...attributesArray)
}
else {
    sortedAttributesArray = attributesArray
}

// add the opening tag
let result = openingTag

// set the join character based on number of lines initially selected
// (newLine if one line, space if more)
let joinCharacter = lineCount > 1 ? ' ' : '\n'

// if there are no attributes, set joinCharacter to ''
if (sortedAttributesArray.length == 1 && sortedAttributesArray[0] == '') {
    joinCharacter = ''
}

// add the sorted attributes to the textSplit string
if (lineCount > 1) {
    sortedAttributesArray.forEach(item => {
        result += joinCharacter + item
    })
}
else {
    sortedAttributesArray.forEach(item => {
        result += joinCharacter + initialWhitespace + indentationString + item
    })
}

// configure ending bracket (new line or not new line)
if (lineCount > 1) {
    if (endingBracket == '/>') {
        endingBracket = ' ' + endingBracket
    }
}
else {
    if (closingBracketOnNewLine) {
        endingBracket = '\n' + initialWhitespace + endingBracket
    }
    else if (endingBracket == '/>') {
        endingBracket = ' ' + endingBracket
    }
}

// add the ending bracket
result = result + endingBracket

console.log(result)
(defun +rjsx/split-join ()
  "Function docstring"
  (interactive)
  (let ((input
         (->>
          (buffer-substring-no-properties (region-beginning) (region-end))
          (s-replace "\n" "\\n"))))
    (->>
     (shell-command-to-string (t! "node <<doom-private-dir>>/.cache/split-join-tag.js  $'<<input>>'"))
     (insert))))
Expand self closing tag

Converts self closing JSX tags to closing tags. <Foo /> -> <Foo>|</Foo>

(defun +rjsx|expand-insert-self-closing-tag ()
  "Opens the current tag at any position of the cursor and starts insert mode"
  (interactive)
  (search-forward "/>")
  (evil-backward-char)
  (call-interactively #'delete-backward-char)
  (call-interactively #'rjsx-electric-gt)
  (newline)
  (call-interactively #'evil-indent-line)
  (call-interactively #'evil-open-above))
Extract Props from function arguments to body
(defun +js|extract-props ()
  "Extract props object under the cursor."
  (interactive)
  (save-excursion
    (let* ((point-start (search-backward "{"))
           (point-end (search-forward "}"))
           (text (buffer-substring-no-properties point-start point-end)))
      (delete-region point-start point-end)
      (insert "props")
      (evil-open-below 1)
      (insert (template "const <<text>> = props;"))
      (search-backward "}")
      (js2r-expand-node-at-point)))
  (evil-normal-state))
Company Files

Remove the js extension for company-files.

(defun company-js-files (command &optional arg &rest ignored)
  "Company complete path. Remove extension after completion"
  (interactive (list 'interactive))
  (require 'company)
  (cl-case command
    (interactive (company-begin-backend 'company-js-files))
    (prefix (company-files--grab-existing-name))
    (candidates (company-files--complete arg))
    (location (cons (dired-noselect
                     (file-name-directory (directory-file-name arg))) 1))
    (post-completion (when (s-matches? "\.js$" arg) (delete-backward-char 3)))
    (sorted t)
    (no-cache t)))

(map! :map js2-mode-map
      :i "C-x C-f" #'company-js-files)
Import JS File
(defun +js/import-file (file)
  (let ((cursor-postion (point))
        (filename (f-no-ext file)))
    (insert (template "import  from '<<filename>>';"))
    (goto-char cursor-postion)
    (forward-char 7)
    (evil-insert-state)))

(defun +js|ivy-import-file (&optional action)
  (interactive)
  (let* ((local-files
          (-->
           (-concat (list find-program) counsel-file-jump-args)
           (string-join it " ")
           shell-command-to-string
           split-string))
         (node-packages
          (-->
           (concat "jq -r '.dependencies | keys | .[]' " (concat (projectile-project-root) "package.json"))
           shell-command-to-string
           split-string))
         (imports (append local-files node-packages)))
    (ivy-read "Import file " imports :action (or action '+js/import-file))))
Switch Ternary
(defun js2r-ternary-switch-statements ()
  "Switch expressions in a ternary."
  (interactive)
  (js2r--guard)
  (js2r--wait-for-parse
   (save-excursion
     (let* ((ternary (js2r--closest 'js2-cond-node-p))
            (test-expr (js2-node-string (js2-cond-node-test-expr ternary)))
            (true-expr (js2-node-string (js2-cond-node-true-expr ternary)))
            (false-expr (js2-node-string (js2-cond-node-false-expr ternary)))
            (stmt (js2-node-parent-stmt ternary))
            (stmt-pre (buffer-substring (js2-node-abs-pos stmt) (js2-node-abs-pos ternary)))
            (stmt-post (s-trim (buffer-substring (js2-node-abs-end ternary) (js2-node-abs-end stmt))))
            (beg (js2-node-abs-pos stmt)))
       (goto-char beg)
       (delete-char (js2-node-len stmt))
       (insert "return " test-expr)
       (newline)
       (insert "? " false-expr)
       (newline)
       (insert ": " true-expr ";")
       (indent-region beg (point))))))
Ignore Flycheck error on line
(defun +js|eslint-fix-ignore-error ()
  "Adds an ignore with the current flycheck error."
  (interactive)
  (if-let ((error-id (flycheck-copy-errors-as-kill (point) #'flycheck-error-id)))
    (save-excursion
      (previous-line)
      (end-of-line)
      (newline-and-indent)
      (insert (template "// eslint-disable-next-line <<error-id>>")))))
Find node_modules package

Go to a package from the node_modules directory.

(defun +javascript/find-npm-package-goto (package)
  "Go to directory by package name"
  (-some->> (f-join (projectile-project-root) "node_modules")
    (-id-when #'f-exists?)
    (-f-join package)
    (find-file)))

(defun +javascript|find-npm-package ()
  "Find package in node_modules directory."
  (interactive)
  (-when-let* ((json (-some->> (f-join (projectile-project-root) "package.json")
                       (-id-when #'f-exists?)
                       (json-read-file)))
               ((&alist 'dependencies dependencies
                        'devDependencies devDependencies) json)
               (packages (->> (-concat dependencies devDependencies)
                              (-map #'car))))
    (ivy-read "Go to package directory: " packages
              :action #'+javascript/find-npm-package-goto)))

Bindings

(map!
 :after js2-mode
 :map js2-mode-map
 :desc "Goto parent function" :n "gh" (cmd! (js2-beginning-of-defun))
 :localleader
 :desc "Goto NPM Package" "m" #'+javascript|find-npm-package
 (:prefix-map ("c" . "Create")
  :desc "Import File" "i" #'js-import))

(map!
 :after rjsx-mode
 :map rjsx-mode-map
 :localleader
 (:desc "Open Self-Closing Tag" :n ">" #'+rjsx|expand-insert-self-closing-tag)
 (:desc "Open Self-Closing Tag" :n "<" #'rjsx-rename-tag-at-point))
Evil Function Text Object Motion

Adds text objects for functions in javascript. So you can press daf to delete a function.

(add-hook! js-mode
  (require 'evil-text-objects-javascript)
  (evil-text-objects-javascript/install))
Evil Method Text Object Motion

Overwrite evil-text-objects-javascript to also accepts methods.

I mainly changed the function marking helper.

- (call-interactively #'mark-defun)
+ (call-interactively #'js2-mark-defun)
(after! evil-text-objects-javascript
  (evil-define-text-object
    evil-inner-javascript-function (count &optional beg end type)
    "inner text object for all javascript functions."
    (call-interactively #'js2-mark-defun)
    (narrow-to-region (region-beginning) (region-end))
    (goto-char (point-min))
    (let* ((beg (save-excursion
                  (search-forward "(")
                  (backward-char)
                  (evil-jump-item)
                  (search-forward-regexp "[({]")
                  (point)))
           (end (save-excursion
                  (goto-char beg)
                  (evil-jump-item)
                  (point))))
      (evil-range beg end type)))

  (evil-define-text-object
    evil-outer-javascript-function (count &optional beg end type)
    "Outer text object for all Javascript functions."
    (call-interactively #'js2-mark-defun)
    (narrow-to-region (region-beginning) (region-end))
    (goto-char (point-min))
    (let* ((beg (save-excursion
                  (when (looking-at "[[:space:]]")
                    (evil-forward-word-begin))
                  (point)))
           (end (save-excursion
                  (goto-char beg)
                  (search-forward "(")
                  (backward-char)
                  (evil-jump-item)
                  (search-forward-regexp "[({]")
                  (evil-jump-item)
                  (forward-char)
                  (if (save-excursion
                        (progn
                          (forward-char)
                          (when (looking-at ",") (point))))
                    (point)))))
      (evil-range beg end type))))

Nim

Dotfiles compilation command

(def-project-mode! +dotfiles-nim-compile-mode
  :modes '(nim-mode)
  :when (string= (doom-project-root) "/etc/dotfiles/")
  :on-load
  (setq-local nim-compile-default-command '("c" "-r" "--verbosity:0" "--hint[Processing]:off" "--excessiveStackTrace:on" "--outdir:../dst")))

Package Config

js-import

(put 'js-import-alias-map 'safe-local-variable (lambda (x) t))

projectile

Ignored Projects

Dont add packages inside ~~.emacs.d~ to projectile, as I often browse the source for a package, but I dont want them added to my projectile-known-projects.

(use-package! projectile
  :init
  (setq projectile-ignored-projects '("~/"
                                      "/tmp"
                                      "~/.emacs.d/.local/straight/repos/")))

Scan directory for repositories

(use-package! projectile
  :init
  (setq projectile-project-search-path '("~/Code/Dotfiles"
                                         "~/Code/Repositories"
                                         "~/Code/Projects"
                                         "~/Code/Tools"
                                         "~/Code/Meisterlabs"
                                         "~/Code/Meisterlabs/mindmeister-web/.worktrees"))
  :config
  ;; Auto discover when running switch project for the first time
  (add-transient-hook! 'counsel-projectile-switch-project
    (projectile-cleanup-known-projects)
    (projectile-discover-projects-in-search-path)))

elescope

(use-package elescope
  :commands (elescope-checkout)
  :config
  (setq elescope-root-folder "~/Code/Repositories")
  :init
  (defalias '+git|clone 'elescope-checkout))

daemon

(use-package daemons
  :commands (daemons))

writegood mode

Config

Disable doom defined hooks for writegood mode
(use-package! writegood-mode
  :hook 'nil)

Disable writegood-mode with spell-fu mode

(defun +spell-fu/activate-writegood-mode-hook ()
  "Toggle writegood mode with spell-fu mode."
  (cond
   (spell-fu-mode (writegood-mode 1))
   (t (writegood-mode -1))))

(add-hook! 'spell-fu-mode-hook :after #'+spell-fu/activate-writegood-mode-hook)

Cheat.sh

Config

(use-package cheat-sh
  :commands (cheat-sh)
  :config
  (setq cheat-sh-topic-mode-map
        '((awk-mode . "awk")
          (c++-mode . "cpp")
          (c-mode . "c")
          (clojure-mode . "clojure")
          (clojurescript-mode . "clojure")
          (dockerfile-mode . "docker")
          (emacs-lisp-mode . "elisp")
          (fish-mode . "fish")
          (go-mode . "go")
          (haskell-mode . "haskell")
          (hy-mode . "hy")
          (java-mode . "java")
          (js-jsx-mode . "javascript")
          (js-mode . "javascript")
          (js-mode . "js")
          (lisp-interaction-mode . "elisp")
          (lisp-mode . "lisp")
          (objc-mode . "objectivec")
          (pike-mode . "pike")
          (powershell-mode . "powershell")
          (python-mode . "python")
          (rust-mode . "rust")
          (sh-mode . "bash")
          (nim-mode . "nim"))))

Set Mode Hook

(defun cheat-sh (thing)
  "Look up THING on cheat.sh and display the result."
  (interactive (list (cheat-sh-read "Lookup: ")))
  (let ((result (cheat-sh-get thing)))
    (if result
        (let ((temp-buffer-window-setup-hook
               (cons 'cheat-sh-mode-setup temp-buffer-window-show-hook)))
          (with-temp-buffer-window "*cheat.sh*" nil 'help-window-setup
            (with-current-buffer "*cheat.sh*"
              (+cheat-sh/set-mode thing))
            (princ result)))
      (error "Can't find anything for %s on cheat.sh" thing))))

(defun cheat-sh-mode-setup ()
  "Set up the cheat mode for the current buffer."
  (setq buffer-read-only nil))

(defun +cheat-sh/set-mode (x)
  (-when-let* ((lang
                (-some->> x
                  (s-split "/")
                  (nth 0)))
               (mode (--find (string= lang (cdr it)) cheat-sh-topic-mode-map))
               (mode (car mode)))
    (funcall mode)))

Edbi

Bridge

Add nix-shell headers to the bridge

#! /usr/bin/env nix-shell
#! nix-shell -i perl -p mysql -p perlPackages.DBDmysql perlPackages.RPCEPCService perlPackages.DBDPg perlPackages.DBI

use strict;
use RPC::EPC::Service;
use Data::Dumper;
use DBI;

our $dbh = undef;
our $sth = undef;

sub edbi_connect {
  my ($args) = @_;
  my ($data_source,$username,$auth) = @$args;

  # No input _probably_ means "no password" rather than empty string
  $auth = undef if($auth eq "");

  if ($dbh) {
    eval {
      $dbh->disconnect();
    }
  }

  our $dbh = DBI->connect($data_source, $username, $auth)
      or die("Could not connect to database:
Data Source ($data_source)
User Name: ($username):
DBI error: ($DBI::errstr)
");

  return $dbh->get_info(18);
}

sub edbi_do {
  return undef unless $dbh;
  my ($args) = @_;
  my ($sql, $params) = @$args;
  my $rows = $dbh->do($sql, undef, @$params);
  return $rows;
}

sub edbi_select_all {
  return undef unless $dbh;
  my ($args) = @_;
  my ($sql, $params) = @$args;
  my $rows = $dbh->selectall_arrayref($sql, undef, @$params);  return $rows;
}

sub edbi_prepare {
  return undef unless $dbh;
  $sth->finish() if $sth;
  my ($sql) = @_;
  our $sth = $dbh->prepare($sql) or return undef;
  return 'sth';
}

sub edbi_execute {
  return undef unless $sth;
  my ($params) = @_;
  return $sth->execute(@$params) or return undef;
}

sub edbi_fetch_columns {
  return undef unless $sth;
  return $sth->{NAME};
}

sub edbi_fetch {
  return undef unless $sth;
  my ($num) = @_;
  if ($num eq undef) {
    return $sth->fetchall_arrayref();
  } else {
    my $ret = [];
    for (my $i = 0; $i < $num; $i++) {
      my $row = $sth->fetchrow_arrayref();
      last if $row eq undef;
      push @$ret, [@$row];
    }
    return $ret;
  }
}

sub edbi_auto_commit {
  return undef unless $dbh;
  my ($flag) = @_;
  if ($flag eq "true") {
    $dbh->{AutoCommit} = 1;
    return 1;
  } else {
    $dbh->{AutoCommit} = 0;
    return 0;
  }
}

sub edbi_commit {
  return undef unless $dbh;
  $dbh->commit();
  return 1;
}


sub edbi_rollback {
  return undef unless $dbh;
  $dbh->rollback();
  return 1;
}

sub edbi_disconnect {
  return undef unless $dbh;
  $dbh->disconnect();
  return 1;
}

sub edbi_status {
  return undef unless $dbh;
  return [$dbh->err, $dbh->errstr, $dbh->state];
}

sub edbi_type_info_all {
  return undef unless $dbh;
  my $ret = $dbh->type_info_all;
  print STDERR Dumper $ret;
  return $dbh->type_info_all;
}

sub edbi_table_info {
  return undef unless $dbh;
  eval {
    $sth->finish() if $sth;
  };
  my ($args) = @_;
  my ($catalog, $schema, $table, $type) = @$args;
  $sth = $dbh->table_info( $catalog, $schema, $table, $type );
  return [$sth->{NAME}, $sth->fetchall_arrayref()];
}

sub edbi_column_info {
  return undef unless $dbh;
  eval {
    $sth->finish() if $sth;
  };
  my ($args) = @_;
  my ($catalog, $schema, $table, $column) = @$args;
  $sth = $dbh->column_info( $catalog, $schema, $table, $column );
  return [[],[]] unless $sth;
  return [$sth->{NAME}, $sth->fetchall_arrayref()];
}

sub edbi_primary_key_info {
  return undef unless $dbh;
  eval {
    $sth->finish() if $sth;
  };
  my ($args) = @_;
  my ($catalog, $schema, $table) = @$args;
  $sth = $dbh->primary_key_info( $catalog, $schema, $table );
  return undef unless $sth;
  return [$sth->{NAME}, $sth->fetchall_arrayref()];
}

sub edbi_foreign_key_info {
  return undef unless $dbh;
  eval {
    $sth->finish() if $sth;
  };
  my ($args) = @_;
  my ($pkcatalog, $pkschema, $pktable, $fkcatalog, $fkschema, $fktable) = @$args;
  $sth = $dbh->foreign_key_info( $pkcatalog, $pkschema, $pktable,
                                 $fkcatalog, $fkschema, $fktable );
  return undef unless $sth;
  return [$sth->{NAME}, $sth->fetchall_arrayref()];
}

sub main {
  my $methods =
    {
     'connect' => [\&edbi_connect,"data_source, username, auth", ""],
     'do' => [\&edbi_do, "sql, params", ""],
     'select-all' => [\&edbi_select_all, "sql, params", ""],
     'prepare' => [\&edbi_prepare, "sql", ""],
     'execute' => [\&edbi_execute, "params", ""],
     'fetch-columns' => [\&edbi_fetch_columns, "", ""],
     'fetch' => [\&edbi_fetch, "[number]", ""],
     'auto-commit' => [\&edbi_auto_commit, "false/true", ""],
     'commit' => [\&edbi_commit, "", ""],
     'rollback' => [\&edbi_rollback, "", ""],
     'disconnect' => [\&edbi_disconnect, "", ""],
     'status' => [\&edbi_status, "", ""],
     'type-info-all' => [\&edbi_type_info_all, "", ""],
     'table-info' => [\&edbi_table_info, "catalog, schema, table, type", ""],
     'column-info' => [\&edbi_column_info, "catalog, schema, table, column", ""],
     'primary-key-info' => [\&edbi_primary_key_info, "catalog, schema, table", ""],
     'foreign-key-info' => [\&edbi_foreign_key_info, "pk_catalog, pk_schema, pk_table, fk_catalog, fk_schema, fk_table", ""],
    };
    my $server = RPC::EPC::Service->new(0, $methods);
    $server->start;
}

main;
(after! edbi
  (setq edbi:driver-info
        (list
         "nix-shell" "-p"
         "perl"
         "-p" "perlPackages.DBI"
         "-p" "perlPackages.RPCEPCService"
         "-p" "perlPackages.DBDPg"
         "-p" "perlPackages.DBDmysql"
         "--run"
         (f-join doom-private-dir "edbi-bridge.pl"))))

Pass

Disable dcl mode for password files

Since it has the regex matching .com it’s enabled for all my password files, which I name after the matching url.

(setq auto-mode-alist (rassq-delete-all 'dcl-mode auto-mode-alist))

Bookmarks

Config

Main Bookmarks
(setq
 +bookmarks:main
 '(((file . "~/.config/doom/modules/private/work/config.org")
    (name . "Config: Work")
    (goto . "* Configuration")
    (goto-bol . t)
    (disable-relocation . t))
   ((file . "~/.config/doom/autoload.org")
    (name . "Config: Autoloads"))
   ((file . "~/.config/doom/modules/private/org/config.org")
    (name . "Config: Org"))
   ((file . "~/.config/doom/config.org")
    (name . "Bookmarks: Main")
    (goto . "*** Main Bookmarks")
    (goto-bol . t))
   ((fn . (lambda () (find-file (getenv "HISTFILE"))))
    (name . "Shell History")
    (goto-bol . t))
   ((file . "~/.config/doom/modules/private/work/config.org")
    (name . "Bookmarks: Work")
    (goto . "*** Bookmarks")
    (goto-bol . t))
   ((file . "/etc/dotfiles/modules/desktop/common.nix")
    (name . "Mimeapps")
    (goto . "### Mimeapps")
    (goto-bol . t))
   ((file . "~/.config/weechat/xfer")
    (name . "Weechat Xfer"))
   ((file . "/run/media/floscr/tolino")
    (name . "Tolino"))
   ((fn . org-capture-goto-last-stored)
    (name . "Capture: Last Stored Marker"))))

Utils

Main Bookmarks Function
(defun +bookmarks|main ()
  (interactive)
  (+bookmarks (-concat +bookmarks:main (+bookmarks/local/list))))

Transmission

(use-package! transmission
  :commands (transmission)
  :config
  (map! :map transmission-files-mode-map
        :n "<C-return>" (cmd! (->> (transmission-files-file-at-point)
                                   (f-parent)
                                   (dired)))))

PDF

(map!
 :map pdf-occur-buffer-mode-map
 :gn [tab] (cmd! (pdf-occur-goto-occurrence t)))

Flycheck

Disabled Checkers

Elisp
(add-hook! 'emacs-lisp-mode-hook :append
  (setq flycheck-disabled-checkers '(emacs-lisp-checkdoc)))
Nim
(add-hook! 'nim-mode-hook :append
  (setq flycheck-disabled-checkers '(nim-nimsuggest))
  (flycheck-select-checker 'nim))

Writeroom

Setup

The setup function

(defun +writeroom/writeroom-setup ()
  (if writeroom-mode
      (+writeroom/writeroom-load)
    (+writeroom/writeroom-unload)))

(defun +writeroom/writeroom-load ()
  (setq-local line-spacing 0.4)
  (setq-local +writeroom:faceremaps
              (list
               (face-remap-add-relative
                'org-link `(:foreground ,(face-attribute 'default :foreground)
                            :underline ,(face-attribute 'default :foreground)
                            :weight normal))
               (face-remap-add-relative
                'default `(:foreground ,(doom-blend
                                         (face-attribute 'default :background)
                                         (face-attribute 'default :foreground)
                                         0.1))))))

(defun +writeroom/writeroom-unload ()
  (kill-local-variable 'line-spacing)
  (mapc #'face-remap-remove-relative +writeroom:faceremaps)
  (kill-local-variable '+writeroom:faceremaps))

and the hooks:

(add-hook! 'writeroom-mode-hook #'+writeroom/writeroom-setup)

Beancount

Config

(use-package! beancount
  :defer t
  :mode
  ("\\.bean\\(?:count\\)?\\'" . beancount-mode)
  :config
  (setq beancount-accounts-files
        (directory-files "~/Documents/Beancount"
                         'full
                         (rx ".beancount.gpg" eos))))

Functions

Helpers
(defun +beancount/collect-accounts ()
  "Collect accounts from beancount files."
  (beancount-collect beancount-account-regexp 0 beancount-accounts-files))

(defvar +beancount:income-accounts nil
  "Prefixes for expense accounts.")
(setq +beancount:income-accounts '("Assets" "Liabilities"))

(defun +beancount/collect-expense-accounts (accounts)
  "Collect accounts from list ACCOUNTS."
  (-filter (lambda (x) (--find (s-starts-with? it x) +beancount:income-accounts)) accounts))

(defun +beancount/date-string ()
  "Function docstring"
  (format-time-string "%Y-%m-%d"))
Open Main File
(defun +beancount|open-main ()
  "Open the main beancount-accounts-files file."
  (interactive)
  (find-file (car beancount-accounts-files)))
Balance
(defun +beancount|balance ()
    "Show the current balances."
    (interactive)
    (let ((compilation-read-command nil))
      (beancount--run "bean-report"
                      (file-relative-name buffer-file-name) "bal")))
Add expense

Manually add an expense to the Cash Book.

(defun +beancount|add-expense ()
  "Add expense to the bottom of the file."
  (interactive)
  (and-let* ((accounts (+beancount/collect-accounts))
             (from (ivy-read "From: " (+beancount/collect-expense-accounts accounts)))
             (to (ivy-read "To: " accounts))
             (subject (read-string "Subject: "))
             (amount (read-number "Amount: ")))
      (goto-char (point-max))
      (newline 1)
      (insert (template "<<(+beancount/date-string)>> * \"<<subject>>\"
  <<from>> -<<amount>> EUR
  <<to>> <<amount>> EUR
"))
      (beancount-align-to-previous-number)))

Bindings

(map! :map beancount-mode-map
      :localleader
      :n "a" #'+beancount|add-expense
      :n "c" #'beancount-check
      :n "q" #'beancount-query
      :n "b" #'+beancount|balance)

RSS with Elfeed

Utils

Open
(defun +rss|open ()
  (interactive)
  (unless (get-buffer "*elfeed-search*")
    (setq elfeed-search-filter +rss:default-search-filter))
  (elfeed)
  (+rss|hydra/body))
Custom Filters
All unread
(defun +rss|search-unread ()
  "Show elfeed articles tagged with unread"
  (interactive)
  (elfeed-search-set-filter "@6-months-ago +unread"))
Visit

Open the current entry:

  • With the browser
  • Or if it’s a youtube feed, open with mpv
(defun +rss|visit-dwim ()
  "Either open the current entry in eww or play it in mpv."
  (interactive)
  (message "")
  (let ((entry (if (eq major-mode 'elfeed-show-mode) elfeed-show-entry (elfeed-search-selected :single)))
        (patterns +rss:mpv-patterns))
    (while (and patterns (not (string-match (car +rss:mpv-patterns) (elfeed-entry-link entry))))
      (setq patterns (cdr patterns)))
    (if patterns
        (+rss|play-with-mpv)
      (if (eq major-mode 'elfeed-search-mode)
          (elfeed-search-browse-url)
        (elfeed-show-visit)))))
Open with EWW
(defun +rss|open-eww ()
  "Open the current entry with eww."
  (interactive)
  (add-hook 'eww-after-render-hook 'eww-readable nil t)
  (let ((buffer (save-window-excursion
                  (eww (elfeed-entry-link elfeed-show-entry))
                  (current-buffer))))
    (switch-to-buffer buffer)))
Wrap Elfeed
(defun +rss|elfeed-wrap ()
  "Enhances an elfeed entry's readability by wrapping it to a width of
`fill-column' and centering it with `visual-fill-column-mode'."
  (let ((inhibit-read-only t)
        (inhibit-modification-hooks t))
    (setq-local truncate-lines nil)
    (setq-local shr-width 100)
    (setq-local +zen-text-scale 0.9)
    (mixed-pitch-mode)
    (writeroom-mode)
    (set-buffer-modified-p nil)))

(add-hook 'elfeed-show-mode-hook #'+rss|elfeed-wrap)
Elfeed open video with MPV

Play youtube entries via MPV

(defvar +rss:mpv-patterns
  '("youtu\\.?be")
  "List of regexp to match against elfeed entry link to know
whether to use mpv to visit the link.")

(defun +rss|play-with-mpv ()
  "Play entry link with mpv."
  (interactive)
  (let ((entry (if (eq major-mode 'elfeed-show-mode) elfeed-show-entry (elfeed-search-selected :single)))
        (url (elfeed-entry-link entry)))
    (+mpv/play-external-url url)))
Capture Elfeed Entries
(defun +rss/stringify-entry (entry)
  (let ((title (->> (elfeed-entry-title entry)
                    ;; Remove braces, they're just confusing to org links
                    (s-replace "[" "")
                    (s-replace "]" "")))
        (url (elfeed-entry-link entry))
        (author (->> (elfeed-meta entry :authors)
                     car
                     ((lambda (x) (plist-get x :name)))))
        (tags (-some--> (elfeed-entry-tags entry)
                (-remove-item 'unread it)
                (-map #'symbol-name it)
                (s-join ":" it)
                (s-wrap it ":" ":"))))
    (template "[[<<url>>][<<author>> - <<title>>]] <<(when tags tags)>>")))

(defun +rss|capture ()
  "Capture current entry of elfeed."
  (interactive)
  (let* ((entry (if (eq major-mode 'elfeed-show-mode)
                   elfeed-show-entry
                 (elfeed-search-selected :single)))
         (org-entry (+rss/stringify-entry entry)))
       (kill-new org-entry)
       (if (s-contains? org-entry "youtube.com")
           (org-capture nil "ew")
        (org-capture nil "er"))))

;; (map!
;;  :after elfeed
;;  :map (elfeed-show-mode-map elfeed-show-mode-map)
;;  :local
;;  :gn "Co" #'+rss|capture)
Update feeds when saving org-elfeed file

When I save the elfeed org-mode file I want to automatically update the feed list. Update function taken from elfeed package.

(add-hook 'after-save-hook (defun +org-efleed/update-feeds ()
                             (when (and (eq major-mode 'org-mode)
                                        (string= buffer-file-name (car rmh-elfeed-org-files)))
                                (require 'elfeed)
                                (rmh-elfeed-org-process rmh-elfeed-org-files rmh-elfeed-org-tree-id))))
Hydra
(defun +rss/toggle-filter (tag)
  "Toggle elfeed filter TAG or append to filters if non-existent."
  (->> (or (-when-let* (((_ modi) (s-match (t! "\\_<\\([+-]\\)<<tag>>\\_>") elfeed-search-filter))
                        (new-modi (if (string= modi "+") "-" "+")))
             (s-replace-regexp (t! "\\_<\\<<modi>><<tag>>\\_>") (t! "<<new-modi>><<tag>>") elfeed-search-filter))
           (s-append (t! " +<<tag>>") elfeed-search-filter))
       (--tap (progn (setq elfeed-search-filter it)
                     (elfeed-search-update--force)
                     (-log (t! "Filter changed to: <<it>>"))))))

(defun +rss|counsel-filter (&optional arg)
  "Toggles a filter in elfeed."
  (interactive "P")
  (ivy-read (t! "Filter: <<elfeed-search-filter>>") (elfeed-db-get-all-tags)
            :action #'+rss/toggle-filter
            :caller #'+rss|counsel-filter))

(defhydra +rss|hydra ()
   "Elfeed"
   ("j" (evil-next-line) "Next item" :column "Navigate")
   ("k" (evil-previous-line) "Previous item")
   ("t" (+rss|counsel-filter) "Tags" :column "Filter")
   ("0" (elfeed-search-set-filter "@2-weeksago +unread") "Unread" :column "Favorites")
   ("f" (elfeed-search-set-filter "@2-weeksago +unread -YOUTUBE -REDDIT") "Feeds")
   ("y" (elfeed-search-set-filter "@2-weeksago +unread +YOUTUBE") "Youtube")
   ("e" (elfeed-search-set-filter "@2-weeksago +unread +REDDIT =emacs") "Reddit: Emacs")
   ("n" (elfeed-search-set-filter "@2-weeksago +unread +REDDIT =nixos") "Reddit: Nixos")
   ("U" (elfeed-update) "Update" :column "actions")
   ("u" (elfeed-search-untag-all-unread) "Mark as unread")
   ("s" (elfeed-db-save) "Save DB")
   ("v" (+rss|visit-dwim) "Visit")
   ("q" nil "Quit" :color blue))

Config

Org Elfeed File
(setq rmh-elfeed-org-files (list (+org/expand-org-file-name "Elfeed/Elfeed.org")))
Disable sliced images

These don’t work if you have a big line-height.

(setq +rss-enable-sliced-images nil)
Default Search
(setq +rss:default-search-filter "@2-week-ago +unread -YOUTUBE")
(setq-default elfeed-search-filter +rss:default-search-filter)

Bindings

Search Mode Bindings
(map!
 :after elfeed
 :map elfeed-search-mode-map
 :gn "r" #'elfeed-update
 (:prefix-map ("g" . "Go")
  :desc "Youtube" :gn "y" (cmd! (elfeed-search-set-filter "@2-week-ago +YOUTUBE +unread"))
  :desc "Normal"  :gn "n" (cmd! (elfeed-search-set-filter "@2-week-ago -YOUTUBE +unread"))
  :desc "Month"   :gn "m" (cmd! (elfeed-search-set-filter "@1-month-ago +unread"))))
Show Mode Bindings
(map!
 :after elfeed
 :map elfeed-search-mode-map
 :gn "r" #'elfeed-update)
LocalLeader
(map! :map (elfeed-search-mode-map elfeed-show-mode-map)
      :localleader
      :desc "Filter Hydra" "f" #'+rss|hydra/body
      :desc "Capture"      "c" #'+rss|capture
      :desc "Open Eww"     "o" #'+rss|open-eww
      :desc "Visit"        "v" #'+rss|visit-dwim
      (:prefix-map ("s" . "Search")
       :desc "Unread" "u" #'+rss|search-unread))

IRC

(after! circe
  (set-irc-server! "chat.freenode.net"
                   `(:tls t
                          :port 6697
                          :nick "floscr"
                          :sasl-username ,(+pass-get-user "Irc/freenode.net")
                          :sasl-password (lambda (&rest _) (+pass-get-secret "Irc/freenode.net"))
                          :channels ("#emacs" "#nixos"))))

Literate Calc Mode

(use-package! literate-calc-mode
  :commands (literate-calc-mode literate-calc-minor-mode))

Calendar

Utils

Filtered Calendars
Personal (Without Work)
(defun +calendar|personal (&rest args)
  (interactive)
  (let ((org-agenda-skip-function '(+org/agenda-skip-without-match "-WORK")))
      (call-interactively #'=calendar)))
Personal (Just Family)
(defun +calendar|just-family (&rest args)
  (interactive)
  (let ((org-agenda-skip-function '(+org/agenda-skip-without-match "+FAMILY|+BIRTHDAY")))
      (call-interactively #'=calendar)))
Personal (Just Birthdays)
(defun +calendar|birthdays (&rest args)
  (interactive)
  (let ((org-agenda-files `(,(+org/expand-org-file-name "Main/contacts.org")))
        (org-agenda-skip-function '(+org/agenda-skip-without-match "+BIRTHDAY")))
      (call-interactively #'=calendar)))
Personal (Without Family)
(defun +calendar|no-family (&rest args)
  (interactive)
  (let ((org-agenda-skip-function '(+org/agenda-skip-without-match "-FAMILY-WORK")))
      (call-interactively #'=calendar)))
Work
(defun +calendar|work (&rest args)
  (interactive)
  (let ((org-agenda-skip-function '(+org/agenda-skip-without-match "+WORK")))
      (call-interactively #'=calendar)))

Config

Calendars

Emacs comes with a lot of custom calendars that I don’t want. This is mostly copied from How to set up a german emacs calendar.

General Holidays
(after! calfw
  :config
  (setq general-holidays
        '((holiday-fixed 1 1 "New Years")
          (holiday-fixed 5 1 "1st Mai"))))
Austrian Holidays
(after! calfw
  :config
  (setq austrian-holidays
        `((holiday-fixed 1 6 "Heilige drei Könige")
          (holiday-fixed 5 1 "Staatsfeiertag")
          (holiday-fixed 8 15 "Mariä Himmelfahrt")
          (holiday-fixed 10 26 "Nationalfeiertag")
          (holiday-fixed 11 1 "Allerheiligen")
          (holiday-fixed 12 8 "Mariä Empfängnis")
          (holiday-fixed 12 24 "Weihnachten")
          (holiday-fixed 12 25 "Christtag")
          (holiday-fixed 12 26 "Stefanitag")
          ;; variable
          (holiday-easter-etc -2 "Karfreitag")
          (holiday-easter-etc 0 "Ostersonntag")
          (holiday-easter-etc 1 "Ostermontag")
          (holiday-easter-etc 39 "Christi Himmelfahrt")
          (holiday-easter-etc 49 "Pfingstsonntag")
          (holiday-easter-etc 50 "Pfingstmontag")
          (holiday-easter-etc 60 "Fronleichnam"))))
Set calendars
(after! calfw
  :config
  (setq calendar-holidays
        (append
          general-holidays
          austrian-holidays
          holiday-solar-holidays)))
General
(after! calfw
  :config
  (setq calendar-week-start-day 1)
  (setq calendar-time-display-form
        '(24-hours ":" minutes (and time-zone (concat " (" time-zone ")"))))
  (setq calendar-abbrev-length 2))

Calc

Additional Units

(setq math-additional-units '((GB "1024 * MiB" "Giga Byte")
                              (MB "1024 * KiB" "Mega Byte")
                              (KB "1024 * B" "Kilo Byte")
                              (B nil "Byte")))

Comint

Allow evil enter anywhere

Fix for comint mode, throwing an error when pressing enter in the middle of the line.

error in process filter: End of buffer
(map!
 :after comint
 :map comint-mode-map
 :ni "RET" (cmd! (comint-send-input nil t))
 :n "<C-backspace>" #'comint-clear-buffer)

Company

Config

Sort company by occurrence
(setq company-transformers '(company-sort-by-occurrence)
      company-idle-delay 0.5)

Functions

Company complete whole lines for all matching buffers

Complete a whole line with all lines from buffers matching the current major-mode.

(defun +company:buffer-list-with-modes (modes)
  "Get all buffers that match MODES"
  (--filter
   (with-current-buffer it (-contains? (doom-enlist modes) major-mode))
   (buffer-list)))

(defun +company:buffer-list-with-major-mode ()
  "Get all buffers matching the current major-mode
Has built in aliases"
  (let ((javascript-modes (list 'rjsx-mode 'js2-mode)))
    (pcase major-mode
      ('rjsx-mode
       (+company:buffer-list-with-modes javascript-modes))
      ('js2-mode
       (+company:buffer-list-with-modes javascript-modes))
      (_
       (+company:buffer-list-with-modes major-mode)))))

(defun +company/whole-lines-all-buffers (command &optional arg &rest ignored)
  "`company-mode' completion backend that completes whole-lines, akin to vim's
C-x C-l."
  (interactive (list 'interactive))
  (require 'company)
  (pcase command
    (`interactive (company-begin-backend '+company/whole-lines-all-buffers))
    (`prefix      (company-grab-line "^[\t\s]*\\(.+\\)" 1))
    (`candidates
     (all-completions
      arg
      (funcall (-compose
                #'-uniq
                #'-flatten
                (lambda (xs)
                  (--map (with-current-buffer it
                           (split-string
                            (replace-regexp-in-string
                             "^[\t\s]+" ""
                             (buffer-substring-no-properties (point-min) (point-max)))
                            "\\(\r\n\\|[\n\r]\\)" t)) xs)))
               (+company:buffer-list-with-major-mode))))))

Bindings

(map!
 (:prefix "C-x"
   :i "C-l" #'+company/whole-lines-all-buffers
   :i "C-." #'+company/whole-lines))

Dired

Config

(use-package! dired
  :init
  (setq dired-omit-files "^\\.?#\\|^\\.$\\|^\\.\\.$\\|\\.DS_Store$\\|flycheck_config.el"))
Enable Async Mode
(after! async
  (dired-async-mode 1))
Ignore .bs.js
(use-package! dired-x
  :after dired
  :config
  (setq dired-omit-files
        (concat dired-omit-files
                ;; Reason Compiled Files
                "\\|\\.bs.js$")))
Auto Refresh

Automatically revert dired buffers.

(add-hook 'dired-mode-hook 'auto-revert-mode)
Automatically create directories when moving/copying items
(after! dired
  (defadvice dired-mark-read-file-name (after rv:dired-create-dir-when-needed (prompt dir op-symbol arg files &optional default) activate)
    (when (member op-symbol '(copy move))
      (let ((directory-name (if (< 1 (length files))
                                ad-return-value
                              (file-name-directory ad-return-value))))
        (when (and (not (file-directory-p directory-name))
                   (y-or-n-p (format "directory %s doesn't exist, create it?" directory-name)))
          (make-directory directory-name t))))))
Listing Switches / Sorting

Adds +dired-default-listing-switches to reset the value.

(defconst +dired-default-listing-switches
  "-ahl -t --group-directories-first")
(setq dired-listing-switches +dired-default-listing-switches)

Utils

Dired Paste DWIM
(defun +dired|paste-dwim ()
  "Paste data in the current directory."
  (interactive)
  (let ((file (read-string "Filename: "))
        (last-clip-type (->> (shell-command-to-string "greenclip print")
                             (s-split "\n")
                             (-first-item))))
    (unless (string= "" file)
      (cond
       ((s-matches? "^image\\/png" last-clip-type)
        (shell-command-to-string (template "xclip -selection clipboard -t image/png -o > <<file>>")))))
    (dired-revert)))
Kill all dired buffers with Q
(defun +dired|kill-dired-buffers ()
  "Kills all dired buffers
Dired creates a buffer for every directory which it visits
Which is fine since you can easily switch between visited buffers
But at some time I want to purge those buffers"
  (interactive)
  (mapc (lambda (buffer)
          (when (eq 'dired-mode (buffer-local-value 'major-mode buffer))
            (kill-buffer buffer)))
        (buffer-list)))

(map! :after dired
      :map dired-mode-map
      :n "q" (cmd! (kill-buffer))
      :n "Q" #'+dired|kill-dired-buffers)
Wdired Mode Switch

Stay in normal mode when switching to wdired

(defun +dired|change-to-wdired-mode ()
  "Simple forward to wdired-change-to-wdired-mode, but staying in normal mode."
  (interactive)
  (wdired-change-to-wdired-mode)
  (evil-normal-state))

Map \ to change to wdired mode, like text mode in magit buffers.

(map!
 :after dired
 :map dired-mode-map
 :n "\\" #'+dired|change-to-wdired-mode)
Use same window for copying/renaming with prefix

I’ve set dired-dwim-target to t, so it uses the other window as the target destination. I undo this option with the interactive prefix argument, which can be accessed via SPC ucopy R/U.

(defun +dired/dired-target-from-prefix (fn)
  (let ((dired-dwim-target (if (eq (prefix-numeric-value current-prefix-arg) 4) ;; Single C-u
                              nil
                            dired-dwim-target)))
      (call-interactively fn)))

(defun +dired|dired-do-copy (&optional arg)
  (interactive "P")
  (+dired/dired-target-from-prefix #'dired-do-copy))

(defun +dired|dired-do-rename (&optional arg)
  (interactive "P")
  (+dired/dired-target-from-prefix #'dired-do-rename))

(map! :after dired
      :map dired-mode-map
      :n "R" #'+dired|dired-do-rename
      :n "C" #'+dired|dired-do-copy)
Show Marked File Size
(defun dired-get-size ()
 (interactive)
 (let ((files (dired-get-marked-files)))
   (with-temp-buffer
     (apply 'call-process "du" nil t nil "-sch" files)
     (message
      "Size of all marked files: %s"
      (progn (re-search-backward "\\(^[0-9.,]+[A-Za-z]+\\).*total$") (match-string 1))))))
Open file externally
(defun +dired/mouse-open-externally (event)
  "Open marked dired file(s) at point with an external application. Open directories normally"
  (interactive "e")
  (+dired/mouse-open-externally event))

(defun +dired/mouse-open-externally (event &optional find-file-func find-dir-func)
  "In Dired, visit the file or directory name you click on.
The optional arguments FIND-FILE-FUNC and FIND-DIR-FUNC specify
functions to visit the file and directory, respectively.  If
omitted or nil, these arguments default to `find-file' and `dired',
respectively."
  (interactive "e")
  (or find-file-func (setq find-file-func '+dired|open-externally))
  (or find-dir-func (setq find-dir-func 'dired))
  (let (window pos file)
    (save-excursion
      (setq window (posn-window (event-end event))
            pos (posn-point (event-end event)))
      (if (not (windowp window))
          (error "No file chosen"))
      (set-buffer (window-buffer window))
      (goto-char pos)
      (setq file (dired-get-file-for-visit))
      (if (file-directory-p file)
          (or (and (cdr dired-subdir-alist)
                   (dired-goto-subdir file))
              (progn
                (select-window window (funcall find-dir-func file))))
        (select-window window)
        (funcall find-file-func)))))

(defun +dired|open-externally (&optional file)
  "Open marked dired file(s) at point with an external application."
  (interactive)
  (let ((file-list (or file (dired-get-marked-files)))
        (process-connection-type nil))
    (--map (+my/nohup-shell-command "xdg-open" (s-wrap (expand-file-name it) "\"")) file-list)))
Toggle Sorting

When you toggle the sorting via dired-sort-toggle-or-edit the cursor stays at the current file, which is very disorienting. With this function the cursor stays on the current line. save-excursion does not work in this function, it just throws to the top of the buffer. The dired-line jumps at the end are used to jump the filename again.

(defun +dired|toggle-sort (&optional arg)
  "Change sorting but stay on the same line."
  (interactive "P")
  (let ((pos (point)))
    (cond
     ((eq arg 3)
      (setq dired-listing-switches (symbol-value '+dired-default-listing-switches))
      (setq dired-actual-switches (symbol-value '+dired-default-listing-switches))
      (dired-sort-set-mode-line)
      (revert-buffer))
     ((eq arg 2)
      (setq dired-listing-switches
            (read-string "Global ls switches (must contain -l): " dired-listing-switches))
      (setq dired-actual-switches (symbol-value 'dired-listing-switches))
      (dired-sort-set-mode-line)
      (revert-buffer))
     (arg
      (setq dired-actual-switches
            (read-string "Buffer ls switches (must contain -l): " dired-actual-switches)))
     (t (dired-sort-toggle-or-edit)))
    (goto-char pos)
    (dired-next-line 1)
    (dired-previous-line 1)))
Convert Images
(defun +dired|convert-image (&optional arg)
  "Convert image files to other formats."
  (interactive "P")
  (assert (executable-find "convert") nil "Install imagemagick")
  (let* ((dst-fpath)
         (src-fpath)
         (src-ext)
         (last-ext)
         (dst-ext))
    (mapc
     (lambda (fpath)
       (setq src-fpath fpath)
       (setq src-ext (downcase (file-name-extension src-fpath)))
       (when (or (null dst-ext)
                 (not (string-equal dst-ext last-ext)))
         (setq dst-ext (completing-read "to format: "
                                        (seq-remove (lambda (format)
                                                      (string-equal format src-ext))
                                                    '("jpg" "png")))))
       (setq last-ext dst-ext)
       (setq dst-fpath (format "%s.%s" (file-name-sans-extension src-fpath) dst-ext))
       (message "convert %s to %s ..." (file-name-nondirectory dst-fpath) dst-ext)
       (set-process-sentinel
        (start-process "convert"
                         (generate-new-buffer (format "*convert %s*" (file-name-nondirectory src-fpath)))
                         "convert" src-fpath dst-fpath)
        (lambda (process state)
          (if (= (process-exit-status process) 0)
              (message "convert %s" (file-name-nondirectory dst-fpath))
            (message "convert %s" (file-name-nondirectory dst-fpath))
            (message (with-current-buffer (process-buffer process)
                       (buffer-string))))
          (kill-buffer (process-buffer process)))))
     (dired-map-over-marks (dired-get-filename) arg))))
Unparent directory at point

Move all files and directories to the current directory, If everything goes well and the directory is empty, safely delete it. It might not work out though, when any of the entries has the same name, in that case throw a warning.

(defun +dired|ungroup-directory ()
  "Move the marked files one folder upwards, never overwrite the files."
  (interactive)
  (->> (dired-get-marked-files)
       (-map (lambda (x)
               (let ((cmd (if (f-has-hidden-entry x)
                              "mv -n ./* ./.* ../"
                            "mv -n ./* ../")))
                 (shell-command (t! "cd \"<<x>>\"; <<cmd>>"))
                 (when (f-empty? x)
                   (dired-delete-file x))))))
  (revert-buffer))
Dired go to top

When I do gg in dired, I mostly want to go to the first entry. This function first goes to the first entry and then to the top of the buffer.

(defun +dired|goto-top ()
  "Go to first directory, when already there go to first line."
  (interactive)
  (if (<= (line-number-at-pos) 3)
      (goto-char (point-min))
    (goto-line 3)
    (dired-next-dirline 1)
    (dired-prev-dirline 1)))

Extensions

Narrow

Dired Narrow allows narrowing a dired buffer to a subselection. Widen can be restored with g.

(use-package! dired-narrow
  :after dired
  :config
  (bind-key "C-c C-n" #'dired-narrow))
Subtree
(use-package! dired-subtree
  :after dired
  :config
  (evil-define-key 'normal dired-mode-map
    (kbd "<tab>") (cmd! () (dired-subtree-toggle)
                      ;; Fix for dired-all-the-icons not showing up
                      (dired-revert))
    (kbd "<backtab>") (cmd! () (dired-subtree-cycle)
                          (dired-revert))
    (kbd "gh") 'dired-subtree-up))
Filter
(use-package! dired-filter
  :after dired
  :config
  (setq dired-filter-saved-filters
        (quote (("images"
                 (extension "jpg" "png" "gif"))
                ("media"
                 (extension "mp3" "mp4" "MP3" "MP4" "avi" "mpg" "flv" "ogg" "wmv" "mkv" "mov" "wma"))
                ("archives"
                 (extension "zip" "bz2" "tgz" "txz" "gz" "xz" "z" "Z" "jar" "war" "ear" "rar" "sar" "xpi" "apk" "xz" "tar"))
                ("documents"
                 (extension "doc" "docx" "odt" "pdb" "pdf" "ps" "rtf" "djvu" "epub")))))
  (dired-filter-define created-today
      "Show only files that are newer than today."
    (:description "created-today")
    (let ((case-fold-search nil))
      (time-less-p (time-subtract (current-time) (* 60 60 24))
                   (file-attribute-modification-time (file-attributes file-name))))))

Bindings

(map! :after dired
      :map dired-mode-map
      :ng "<C-return>" '+dired|open-externally
      :ng (kbd "<mouse-2>") '+dired|open-externally
      :gn "o" '+dired|toggle-sort
      :n "p" '+dired|paste-dwim
      :n "gg" '+dired|goto-top)

(define-key dired-mode-map (kbd "<down-mouse-1>") 'dired-mouse-find-file)

Eldoc

;; Always truncate ElDoc messages to one line. This prevents the echo
;; area from resizing itself unexpectedly when point is on a variable
;; with a multiline docstring.
(setq eldoc-echo-area-use-multiline-p nil)

;; Show ElDoc messages in the echo area immediately, instead of after
;; 1/2 a second.
(setq eldoc-idle-delay 0)

;; Disable eldoc mode
(global-eldoc-mode -1)

Eshell

Aliases

Custom eshell aliases.

(setq +eshell-aliases
  '(("q"      "exit")
    ("f"      "find-file $1")
    ("bd"     "eshell-up $1")
    ("rg"     "rg --color=always $*")
    ("ag"     "ag --color=always $*")
    ("l"      "ls -lh")
    ("ll"     "ls -lah")
    ("gs"     "git status")
    ("groot"  "cd (projectile-project-root)")
    ("gc"     "git commit")
    ("grha"   "git reset --hard; git clean -f -d")
    ("clear"  "clear-scrollback")))

Syntax highlighted cat

(defun +eshell/cat (file)
  "Like `cat' but output with Emacs syntax highlighting."
  (with-temp-buffer
    (insert-file-contents file)
    (let ((buffer-file-name file))
      (delay-mode-hooks
        (set-auto-mode)
        (if (fboundp 'font-lock-ensure)
            (font-lock-ensure)
          (with-no-warnings
            (font-lock-fontify-buffer)))))
    (buffer-string)))

(add-to-list '+eshell-aliases '("cat" "+eshell/cat $1"))

Evil-Snipe

Repeat snipe after further key press

(after! evil-snipe
  (setq evil-snipe-repeat-keys t))

EWW Web Browser

Set the max page width

(setq shr-width 100)

Jest

(use-package! jest
  :after (js2-mode)
  :hook (js2-mode . jest-minor-mode)
  :config
  (set-popup-rule! "^\\*jest\\*"
    :side 'right
    :size 0.5
    :select nil :quit 'current :ttl nil))

Git Lens

(use-package! git-lens
  :commands (git-lens))

Symex

(use-package! symex
  :commands (symex-mode symex-mode-interface))

Indium

(use-package! indium
  :commands (indium-connect indium-launch)
  :init
  (setq indium-chrome-use-temporary-profile t)
  (setq indium-chrome--default-data-dir (f-join (getenv "XDG_CACHE_HOME") "inidum-chrome-profile"))
  (setq indium-chrome-data-dir (f-join (getenv "XDG_CACHE_HOME") "inidum-chrome-profile"))
  (map! :map (js2-mode-map rjsx-mode-map)
        :localleader
        (:prefix ("i" . "Indium")
          :desc "Console"                   "c" #'indium-switch-to-repl-buffer
          :desc "Launch"                    "l" #'indium-launch
          :desc "Launch"                    "q" #'indium-quit
          :desc "Add breakpoint"            "r" #'indium-reload
          (:prefix ("b" . "breakpoint")
            :desc "Add"                     "b" #'indium-add-breakpoint
            :desc "Conditional"             "c" #'indium-add-conditional-breakpoint
            :desc "Conditional"             "e" #'indium-edit-breakpoint-condition
            :desc "Conditional"             "l" #'indium-list-breakpoints
            :desc "Conditional"             "0" #'indium-deactivate-breakpoints
            :desc "Conditional"             "1" #'indium-activate-breakpoints
            :desc "Delete"                  "d" #'indium-remove-breakpoint
            :desc "Delete all from buffer"  "D" #'indium-remove-all-breakpoints-from-buffer
            :desc "Edit Condition"          "e" #'indium-toggle-breakpoint
            :desc "Toggle"                  "t" #'indium-toggle-breakpoint)))
  (map!
        :map indium-inspector-mode-map
        :n "-" #'indium-inspector-pop)
  (map! :map indium-debugger-mode-map
        :n "gr" #'indium-debugger-resume
        :n "gi" #'indium-debugger-step-into
        :n "go" #'indium-debugger-step-over
        :n "ge" #'indium-debugger-evaluate
        :n "gl" #'indium-debugger-locals)
  :config
  (set-popup-rule! "^\\*JS REPL*" :size 0.3)
  (set-popup-rule! "^\\*JS Debugger Locals*" :size 0.3))

Impatient-Mode

Serves the current buffer live over http.

(use-package! impatient-mode
  :commands impatient-mode)

Narrow To Defun Indirect

When narrowing to region or defun, make it in an indirect other window.

Source:

(use-package! narrow-indirect
  :init
  (global-set-key (kbd "C-x n n") 'ni-narrow-to-region-indirect-other-window)
  (global-set-key (kbd "C-x n d") 'ni-narrow-to-defun-indirect-other-window))

Ivy / Counsel

Config

Always show actions in hydra.

(setq ivy-read-action-function #'ivy-hydra-read-action)

Bindings

Adds general bindings to ivy. MOD + ↩ will insert the current item and continue with a new instance of the same ivy buffer. CTRL + ↩ will abort completion and insert the inserted text.

(map!
 :after ivy
 :map ivy-minibuffer-map
  "<s-return>" 'ivy-call
  "<C-tab>" 'minibuffer-complete
  "<C-return>" 'ivy-immediate-done
  "M-m" 'ivy-mark)

Find file on enter press for ivy-occur-mode

(map! :map ivy-occur-mode-map
      :gni "RET" #'ivy-occur-press-and-switch)

Functions

Counsel Project File Jump
(defun +ivy/counsel-project-file-jump (x)
  "Jump to file in project"
  (interactive)
  (counsel-file-jump nil (f-join (projectile-project-root) x)))
Counsel Grep Buffers
(defun +ivy|search-buffers (buffer-list)
  "Grep workspace buffers."
  (interactive)
  (-->
   (or buffer-list (+workspace-buffer-list))
   (cl-remove-if-not #'buffer-file-name it)
   (-map #'buffer-file-name it)
   (-map #'shell-quote-argument it)
   (s-join " " it)
   (counsel-grep-files it)))

(defun +ivy/search-buffer-list (&optional buffer-list)
  (--> (or buffer-list ivy--old-cands)
   (-log it)
   (-map #'shell-quote-argument it)
   (--map (f-join (doom-project-root) it) it)
   (s-join " " it)
   (counsel-grep-files it)))

(defun counsel-grep-files (buffer-file-names)
  "Grep for a string in the file visited by the current buffer."
  (interactive)
  (counsel-require-program counsel-grep-base-command
   (setq counsel-grep-command
         (format counsel-grep-base-command "%s" buffer-file-names))
   (message "%s" counsel-grep-command)
   (let ((init-point (point))
         res)
     (unwind-protect
         (setq res (ivy-read "grep: " #'counsel-grep-function
                             :dynamic-collection t
                             :require-match t
                             :keymap counsel-grep-map
                             :history 'counsel-grep-history
                             :re-builder #'ivy--regex
                             :action #'counsel-grep-action
                             :caller 'counsel-grep))
       (unless res
         (goto-char init-point))))))

(after! ivy
  (ivy-set-actions
   '+git|ivy-changed-files
   '(("/" (lambda (&optional arg)
            (+ivy/search-buffer-list))
      "Grep Forward"))))
Counsel External Drives
(defun +ivy|counsel-mounted-drives ()
  "Counsel of mounted drives from udiskie."
  (interactive)
  (let ((default-directory (concat "/run/media/" (user-login-name) "/")))
    (counsel-find-file)))

Hacks

Banish mouse cursor

The cursor will steal the focus from ivy-posframe, breaking ivy completely…

This option moves the mouse cursor to 0x0, which is really unacceptable, but works for now.

(setq posframe-mouse-banish t)
Fix case-insensitive wgrep
(after! ivy
  (defadvice! respect-case-fold-search (args)
    :filter-return #'counsel--ag-extra-switches
    (concat args (pcase ivy-case-fold-search
                   (`auto " -S ")
                   (`t " -i ")
                   (`nil " -s ")))))

JSON

Utils

Is Last JSON key at point
(defun +json/is-last-key? ()
  "Is the next line the last json key."
  (save-excursion
    (forward-line)
    (+my/buffer-line-has "}")))
Insert JSON Key

Insert JSON key in a JSON document This functions is dependant on the yasnippet: key

(defun +json/insert-key (&optional above?)
  "Adds a new JSON key pair."
  (let ((last-line? (+my/buffer-line-has ",$")))
    ;; Insert comma
    (if (and (not last-line?) (not above?))
        (replace-regexp "$" "," nil (point-at-bol) (point-at-eol)))
    (end-of-line)
    (if above?
        (evil-insert-newline-above)
      (evil-insert-newline-below))
    (indent-according-to-mode)
    (yas/insert-by-name "key")))

(defun +json|insert-key-above ()
  "Function docstring"
  (interactive)
  (+json/insert-key t))

(defun +json|insert-key-below ()
  "Function docstring"
  (interactive)
  (+json/insert-key nil))
Autofix JSON

Uses json-fix to autofix JSON files.

npm i -g json-fix
(defun +json|autofix-buffer ()
  "Autofix json buffer"
  (interactive)
  (let ((b (if mark-active (min (point) (mark)) (point-min)))
        (e (if mark-active (max (point) (mark)) (point-max))))
    (shell-command-on-region b e
     (template "json-fix --no-sort --spaces <<tab-width>>") (current-buffer) t)))

Bindings

(map!
 :after json-mode
 :map json-mode-map
 :gni "<C-return>" #'+json|insert-key-below
 :gni "<C-S-return>" #'+json|insert-key-above)

LSP

Disable presets

All are these are distracting and not helpful.

  • Removes all popup UIs
  • Remove signature message
  • Remove lsp flycheck
(setq lsp-eldoc-render-all nil
      lsp-eldoc-enable-hover nil
      lsp-eldoc-enable-signature-help nil
      lsp-eldoc-prefer-signature-help nil
      lsp-inhibit-message t

      lsp-eldoc-enable-hover nil
      ;; Disable Signature
      lsp-signature-auto-activate nil
      lsp-signature-render-documentation nil
      lsp-signature-doc-lines 1

      lsp-highlight-symbol-at-point nil
      ;; Disable make error highlighting
      lsp-prefer-flymake nil)

;; (setq-hook! 'js2-mode lsp-eldoc-enable-hover nil)

(setq lsp-on-idle-hook nil)

Fix flycheck for js buffers

LSP manually disables all checkers and chooses it’s own. This way you can’t add checkers after your regular hooks like js2-hook. So I fix the checker manually for each mode after lsp was loaded.

(after! lsp-mode
  (remove-hook 'lsp-mode-hook #'+lsp-init-flycheck-or-flymake-h))

(defun +js/fix-checker ()
  "Fix LSP overwritten checkers."
  (interactive)
  (when (-contains? '(js2-mode rjsx-mode) major-mode)
    (flycheck-select-checker 'javascript-eslint)))

(add-hook 'lsp-mode-hook #'+js/fix-checker)

Mail

Config

Accounts
Bookmarks
(setq mu4e-bookmarks
  `(("flag:unread AND NOT flag:trashed AND NOT maildir:/Spam" "Unread messages"      ?u)
    ("date:today..now AND NOT maildir:/Spam"                  "Today's messages"     ?T)))
Disable Background Color
(setq shr-use-colors nil)
Shortcuts
(setq mu4e-maildir-shortcuts
      '(("/mailbox/work/INBOX"     . ?i)
        ("/mailbox/work/Sent Mail" . ?s)
        ("/mailbox/work/Trash"     . ?t)
        ("/mailbox/work/All Mail"  . ?a)))
Automatically apply Section without asking
(setq mu4e-headers-leave-behavior 'apply)

Magit / Git

Utils

Create worktree workspace

Creates a new git workspace from a branch. Automatically adds .projectfile and opens a new doom workspace.

(defun magit-worktree-branch-project-worktree (branch start-point &optional force)
  "Create a new BRANCH and check it out in a new worktree at PATH in a new workspace."
  (interactive
   `(,@(butlast (magit-branch-read-args "Create and checkout branch"))
     ,current-prefix-arg))
  (let* ((worktree-path (f-join (projectile-project-root) ".worktrees"))
         (path (f-join (projectile-project-root) ".worktrees" branch)))
    (when (not (f-exists-p worktree-path))
      (mkdir worktree-path t))
    (magit-run-git "worktree" "add" (if force "-B" "-b")
                   branch (expand-file-name path) start-point)
    (f-touch (f-join path ".projectile"))
    (+workspace-new branch)
    (+workspace-switch branch)
    (magit-diff-visit-directory path)
    (projectile-add-known-project path)
    path))
Revision show original file

Show the original file when visiting a revision buffer. E.g.: When showing a diff from a commit, you may want to edit that file.

(defun magit-revision-show-original-file ()
  "Show the orginal file from a revision buffer
If possible also go to the pointing line"
  (interactive)
  (when magit-buffer-file-name
    (let ((file-name magit-buffer-file-name)
          (line-number (line-number-at-pos)))
      (if current-prefix-arg
          (delete-other-windows))
      (find-file file-name)
      (goto-line line-number))))
Changed files in branch

Show a list of the changed files in the current branch. For now only works on branches that were directly forked from master.

(defun +git/new-files ()
  "List of added files in the current branch."
  (shell-command-to-list "git ls-files -om --exclude-standard"))

(defun +git/modfied-files (&optional branch)
  "Get a list of modified files from the BRANCH to head."
  (shell-command-to-list
   (template "git --no-pager diff --no-renames --name-only --no-merges <<(magit-rev-parse \"HEAD\")>> <<branch>>;")))

(defun +git/changed-branch-files (branch)
  "Get a list of new and modified files from BRANCH to head."
  (->> (+git/modfied-files branch)
       (-concat (+git/new-files))
       (-uniq)
       (-filter
        (lambda (x)
          (let ((default-directory (projectile-project-root)))
            (f-exists? x))))))

(defun +git|ivy-changed-files (&optional branch)
  (interactive)
  (let ((enable-recursive-minibuffers t))
    (ivy-read (template "Changed files for <<(or branch (magit-get-current-branch))>>:")
              (+git/changed-branch-files (or "origin/master"))
              :require-match t
              :history 'file-name-history
              :action counsel-projectile-find-file-action
              :caller '+git|ivy-changed-files)))
Undo commit
(defun +git|undo ()
  "Soft reset current git repo to HEAD~1."
  (interactive)
  (magit-reset-soft "HEAD~1"))
Push dated remote branch

For work I need remote branches with a date prefix.

(defun +git|push-dated (&optional branch)
  "Pushes the given the current BRANCH with a dated prefix
my-branch-name -> 19-01-my-branch-name
When no BRANCH is given, take the current one."
  (interactive)
  (let* ((branch (or branch (magit-get-current-branch)))
         (date (format-time-string "%y-%m"))
         (remote (template "origin/<<date>>-<<branch>>")))
    (magit-git-push branch remote "--set-upstream")
    remote))
Diff range from current branch to magit-thing-at-point

When I’m on the log view, I want to quickly diff it against the currently checked out branch. The transient shortcut for this is d R define here.

(defun +magit|diff-range-from-current-branch ()
  "Ranged diff from the checked out branch to the commit at point."
  (interactive)
  (magit-diff-range (template "<<(magit-commit-at-point)>>..<<(magit-get-current-branch)>>")))
Diff range from current pull request
(defun +magit|diff-range-from-pullreq ()
  "Ranged diff from the pull request under point."
  (interactive)
  (-some->> (forge-current-topic)
    (forge--pullreq-range)
    (magit-diff-range)))
Review branch
(defun +magit|delete-review-branches ()
  "Delete all review branches that no longer have an upstream."
  (interactive)
  (->> (magit-list-branches)
       (--filter (s-starts-with? "refs/heads/REVIEW" it))
       (--map (magit-name-local-branch it))
       (--reject (magit-get-upstream-branch it))
       (--each (lambda (x) (magit-branch-delete x t)))))

(defun +magit|checkout-review-branch (&optional branch start-point)
  "Create a branch with review prefix for easy cleanup afterwards."
  (interactive)
  (let* ((remotes (magit-list-remote-branch-names))
         (atpoint (magit-branch-at-point))
         (branch (magit-completing-read
                  "Checkout branch" remotes
                  nil nil nil 'magit-revision-history
                  (or (car (member atpoint remotes))
                      (and atpoint
                           (car (member (and (string-match "[^/]+/" atpoint)
                                             (substring atpoint (match-end 0)))
                                        remotes))))))
         (review-branch-name (s-replace "origin/" "REVIEW-" branch)))
    ;; HACK Workaround where the buffer cant be read
    (advice-remove 'magit-checkout #'+magit-revert-repo-buffers-deferred-a)
    (magit-checkout branch)
    (when (magit-anything-modified-p)
      (user-error "Cannot checkout when there are uncommitted changes"))
    (if (-contains? (magit-list-local-branch-names) review-branch-name)
        (magit-branch-checkout review-branch-name)
      (magit-branch-and-checkout (s-replace "origin/" "REVIEW-" branch) branch))))
Cleanup branches

Cleans up all merged and review branches

(defun +magit|cleanup-branches (&optional base-branch)
  "Remove all merged and review branches."
  (interactive)
  (+magit|delete-review-branches)
  (let* ((base-branch (or base-branch "master")))
    (call-interactively #'+magit|delete-review-branches)
    (deferred:$
      (deferred:process-shell (template "git branch --merged | egrep -v \"(^\\*|<<base-branch>>)\" | xargs git branch -d"))
      (deferred:nextc it
        (magit-status-maybe-update-revision-buffer)))))
Branches by user
(defun +magit|branches-by-user (&optional ignore-review?)
  "List all branches by user.
Universal argument to ignore review branches."
  (interactive "P")
  (let ((branches
         (->>
          (concat "git for-each-ref"
                  " --sort=-committerdate"
                  " --format='%(committerdate) %(authorname) %(refname)'"
                  " --sort=-committerdate"
                  ;; refs/remotes/origin/ for remote branches
                  " refs/heads"
                  " | grep -e 'Florian Schroedl'")
          (shell-command-to-string)
          (s-split "\n")
          (-butlast)
          (-map (lambda (x) (->> (s-match ".*Florian Schroedl refs\\/heads\\/\\(.*\\)" x)
                                 (-last-item))))
          ((lambda (xs)
             (if ignore-review?
                 (--reject (s-starts-with? "REVIEW" it) xs)
               xs))))))
    (ivy-read "Checkout: " branches :action #'magit-checkout)))
Load forge when needed
(use-package! forge
  :commands (forge-browse-commit))
Commit Template

Loads commit template from ./git/TEMPLATE if the file exist.

(defun +git/template-file (branch)
  (when-let* ((dir (+git/find-root-git-dir))
              (entries (f-entries dir (lambda (x) (s-starts-with? "TEMPLATE" (f-base x)))))
              (branch-template (template "TEMPLATE-<<branch>>")))
    (-log entries)
    (-log branch-template)
    (or
     (-find (lambda (x) (string= branch-template (f-base x))) entries)
     (-find (lambda (x) (string= "TEMPLATE" (f-base x))) entries))))

(defun +git/load-git-template (&rest discard)
  "When a TEMPLATE file in the git directory exists, insert it as a commit template."
  (when (eq (point) (point-at-eol))
    (-some->> (+git/template-file (magit-get-current-branch))
      (f-read)
      (s-trim)
      (s-append " ")
      (insert)))
  (evil-insert-state))

And the hook

(add-hook! 'git-commit-setup-hook :append #'+git/load-git-template)
Find root directory

Find the root directory of a .git repository This also works for worktrees that are in a nested directory.

(defun +git/find-root-git-dir ()
  "Find the root directory of a repository."
    (-some->> (magit-toplevel)
      (-f-join ".git")
      (-id-when #'f-exists?)
      ((lambda (x)
         (if (f-file? x)
             (->> (magit-list-worktrees)
                  (car)
                  (car)
                  (-f-join ".git"))
           x)))))
Lock git buffer
(advice-add #'magit-toggle-buffer-lock :after (lambda () (+my/bool-state-message 'magit-buffer-locked-p)))
Check for merge conflicts

Try two merge two branches in memory, to determine if there would be merge conflicts.

(defun +git|check-branch-for-merge-conflict (&optional source-branch target-branch)
  "Try to merge SOURCE-BRANCH into TARGET-BRANCH, return list of conflicting files when there are any.
This will try to merge in memory, so no index files will be created.
SOURCE-BRANCH defaults to the current branch.
TARGET-BRANCH defaults to origin/master."
  (interactive)
  (let* ((source-branch (or source-branch (magit-get-current-branch)))
         (target-branch (or target-branch "origin/master"))
         (merge-base (shell-command-to-list (t! "git merge-base <<target-branch>> <<source-branch>>"))))
    (shell-command-to-list (t! "git merge-tree <<(car merge-base)>> <<target-branch>> <<source-branch>>"))))
Copy over changes from diff
(defun +magit/diff-copy-file-changes ()
  "Copies over contents of diff at point to the curent file system."
  (interactive)
  (save-excursion
    (let ((buffer (call-interactively #'magit-diff-visit-file)))
      (with-current-buffer buffer
        (make-directory (f-dirname magit-buffer-file-name) t)
        (f-write (substring-no-properties (buffer-string)) 'utf-8 magit-buffer-file-name)))
    (kill-buffer)))
Text Mode

Restores functionality when editing magit buffers as text. For now only the toggle functionality is needed.

(defvar +magit-evil-edit-mode-map (make-sparse-keymap))

(define-minor-mode +magit-evil-edit-mode ""
  :keymap +magit-evil-edit-mode-map)

(map! :map +magit-evil-edit-mode-map
      :n [tab] #'magit-section-toggle)

Config

(after! magit
  :config
  (setq
   magit-save-repository-buffers 'dontask
   magithub-clone-default-directory "~/Code/Repositories"
   git-commit-summary-max-length 120))

(after! forge
  :config
  (setq forge-database-file (f-join doom-local-dir "forge-database.sqlite")))

Bindings

Diff Navigation

My workflow for navigating diffs Use z1 to fold all diffs to their file headers and press’s { or } to

  1. Refold all sections
  2. Go to the next section
  3. Unfold everything in the current section

Then use ] to navigate the sections

(defun floscr:magit-jumpunfold-section (&optional forward)
  "Fold all section. Go to next section when FORWARD. Show all children"
  (interactive)
  (magit-section-show-level-1-all)
  (call-interactively (if forward #'magit-section-forward-sibling #'magit-section-backward-sibling))
  (call-interactively #'magit-section-show-children))

(map!
 (:map magit-diff-mode-map
  :nv "}" (cmd! (floscr:magit-jumpunfold-section 't))
  :nv "{" (cmd! (floscr:magit-jumpunfold-section))))
Time Machine Navigation
(map!
 :after git-timemachine
 :map git-timemachine-mode-map
 :n "[[" #'git-timemachine-show-previous-revision
 :n "]]" #'git-timemachine-show-previous-revision)
Disable quit for locked buffers

Disable accidentally quitting magit buffers with q when the buffer is locked.

(defun +magit/disable-locked-quit (orig-fn &rest args)
  (unless magit-buffer-locked-p
    (apply orig-fn args)))

(advice-add #'magit-mode-bury-buffer :around #'+magit/disable-locked-quit)

Transient Bindings

You can add flags or commands to the magit interface transient here. To append something, just state the flag that you see in the transient popup as the 2nd argument.

(after! magit
  (transient-append-suffix 'magit-log "-f" '("-0" "No merges" "--no-merges"))
  (transient-append-suffix 'magit-push "p" '("d" "dated" +git|push-dated))

  (transient-append-suffix 'magit-diff "d" '("R" "Diff range from current branch" +magit|diff-range-from-current-branch))
  (transient-append-suffix 'magit-diff "d" '("P" "Pullrequest Range" +magit|diff-range-from-pullreq))
  (transient-append-suffix 'magit-diff "d" '("f" "File" magit-diff-buffer-file))

  (transient-append-suffix 'magit-commit
    "-D" '("-D" "Override the author date" "--date=" transient-read-date))

  (transient-append-suffix 'magit-branch "l" '("R" "Create review branch" +magit|checkout-review-branch))
  (transient-append-suffix 'magit-branch "l" '("U" "My Branches" +magit|branches-by-user)))

Markdown

Always keep markdown centered, without line numbers.

(use-package! markdown-mode
  :init
  (add-to-list 'auto-mode-alist '("\\.mdx\\'" . markdown-mode))
  (setq markdown-fontify-code-blocks-natively t)
  :config
  (add-hook! markdown-mode
    (hl-line-mode -1)
    (visual-line-mode)
    (visual-fill-column-mode)
    (outline-minor-mode)
    (setq visual-fill-column-width 90
          display-line-numbers nil)
    (setq line-spacing 2
          fill-column 80))

  (map! (:map markdown-mode-map
          :n "<"    #'markdown-promote
          :n ">"    #'markdown-demote)))

Open With

Nov (EPUB Reading Mode)

Adds EPUB reading mode wasamasa/nov.el: Major mode for reading EPUBs in Emacs

I want to keep the buffer centered, but let nov take care of breaking the text, since this is much nicer then visual-line-mode.

(defun my-nov-config ()
  (setq line-spacing 5)
  (face-remap-add-relative 'variable-pitch :family "Liberation Serif" :height 1.4)
  (setq visual-fill-column-center-text t)
  (setq visual-fill-column-width (+ nov-text-width 25))
  (visual-fill-column-mode t))

(use-package! nov
  :defer t
  :init
  (add-to-list 'auto-mode-alist '("\\.epub\\'" . nov-mode))
  (setq nov-text-width 75)
  (setq nov-save-place-file (f-join doom-local-dir "nov-places"))
  :config
  (progn
    (add-hook 'nov-mode-hook 'my-nov-config)))

The font can be installed via brew cask.

brew tap homebrew/cask-fonts
brew cask install font-liberation-sans

NPM Mode

Add CI command

(defun npm-mode-npm-ci ()
  "Run the 'npm install' command."
  (interactive)
  (npm-mode--exec-process "npm ci"))

PDF Tools

;; Fix midnight colors for doom-one theme
(setq pdf-view-midnight-colors '("#BBC2CD" . "#282C34"))

rainbow-mode

(use-package! rainbow-mode
  :commands (rainbow-mode))

Magit Smerge Mode

Fix Colors

(after! smerge-mode
  :config
  ;; TODO This is broken after switching the theme but works for now
  ;; This fixes the smerge diff color is really bright an ugly
  (set-face-attribute 'smerge-refined-added nil :foreground nil :background nil))

Hydra

(use-package smerge-mode
  :after hydra
  :config
  (defhydra unpackaged/smerge-hydra
    (:color pink :hint nil :post (smerge-auto-leave))
    "
^Move^       ^Keep^               ^Diff^                 ^Other^
^^-----------^^-------------------^^---------------------^^-------
_n_ext       _b_ase               _<_: upper/base        _C_ombine
_p_rev       _u_pper              _=_: upper/lower       _r_esolve
^^           _l_ower              _>_: base/lower        _k_ill current
^^           _a_ll                _R_efine
^^           _RET_: current       _E_diff
"
    ("n" smerge-next)
    ("p" smerge-prev)
    ("b" smerge-keep-base)
    ("u" smerge-keep-upper)
    ("l" smerge-keep-lower)
    ("a" smerge-keep-all)
    ("RET" smerge-keep-current)
    ("\C-m" smerge-keep-current)
    ("<" smerge-diff-base-upper)
    ("=" smerge-diff-upper-lower)
    (">" smerge-diff-base-lower)
    ("R" smerge-refine)
    ("E" smerge-ediff)
    ("C" smerge-combine-with-next)
    ("r" smerge-resolve)
    ("k" smerge-kill-current)
    ("ZZ" (lambda ()
            (interactive)
            (save-buffer)
            (bury-buffer))
     "Save and bury buffer" :color blue)
    ("q" nil "cancel" :color blue))
  :hook (magit-diff-visit-file . (lambda ()
                                   (when smerge-mode
                                     (flycheck-mode -1)
                                     (unpackaged/smerge-hydra/body)))))

Workspaces

Config

Always add buffers to current workspace

Doom per default adds buffers to the current workspace on find-file. I want buffers added whenever I visit a buffer.

(after! persp-mode
  (defun +workspace*add-special-buffer ()
    (if-let* ((name (buffer-name))
              (add-buffer? (or
                            ;; Always add files to workspaces
                            (buffer-file-name)
                            ;; Add src buffer
                            (s-matches? "\\*Org Src.*" name))))
      (persp-add-buffer (current-buffer) (get-current-persp))))

  (add-hook 'doom-switch-buffer-hook #'+workspace*add-special-buffer))

Functions

Switch to workspace

Enhancement of the default +workspace/switch-to. This allows quick deletion of workspaces from ivy with CTRL + BACKSPACE.

(defvar counsel-workspace-map
  (let ((map (make-sparse-keymap)))
    (define-key map (kbd "C-<backspace>") #'+workspace/switch-to-delete-space)
    map))

(defun +workspace//switch-to-delete-space (workspace)
  (let* ((current-workspace-name (+workspace-current-name))
         (new-workspace-name
            (or (--first (string= current-workspace-name it) (+workspace-list-names)) "main")))
    (+workspace/delete workspace)
    (+workspace-switch new-workspace-name)
    (+workspace/my-switch-to)))

(defun +workspace/switch-to-delete-space ()
  (interactive)
  (ivy-set-action #'+workspace//switch-to-delete-space)
  (ivy-done))

(defun +workspace/my-switch-to ()
  (interactive)
  (ivy-read "Switch to workspace: "
            (+workspace-list-names)
            :keymap counsel-workspace-map
            :action #'+workspace/switch-to))
Close Other Workspaces
(defun +workspace|close-others ()
  "Close all other workspaces."
  (interactive)
  (--> (+workspace-list-names)
       (--reject (string= (+workspace-current-name) it) it)
       (-each it #'+workspace-delete))) ;
Find file for workspace

Most of the time you create workspaces from a project. But when the CWD has changed in that workspace, you would have to relocate to the projects cwd to find a file.

(defun +workspace/workspace-project-root (&optional arg)
  "Gets the root dir for the current workspace"
  (--find (s-match (concat (+workspace-current-name) "/$") it) projectile-known-projects))

(defun +workspace|find-workspace-project-file ()
  "Projectile find file for the project named after the current workspace."
  (interactive)
  (cl-letf (((symbol-function 'projectile-project-root) #'+workspace/workspace-project-root))
    (projectile-find-file)))

(defun +workspace|workspace-project-vc ()
  "Projectile find file for the project named after the current workspace."
  (interactive)
  (let ((default-directory (+workspace/workspace-project-root)))
    (magit-status)))
New named workspace
(defun +workspace/new-named ()
  "Create a new named workspace."
  (interactive)
  (let ((name (read-string "New workspace name: ")))
    (if name (+workspace/new name))))
Cleanup Workspace
(defun +workspace/remove-other-buffers (&optional keep-alive?)
  "Kill or remove all other buffers from current workspace."
  (interactive)
  (--> (+workspace-buffer-list)
       (--reject (eq (current-buffer) it) it)
       (if keep-alive?
           (persp-remove-buffer it)
         (kill-buffer it))))

(defun +workspace|hide-other-buffers ()
  "Hide all inactive buffers from the current workspace."
  (interactive)
  (+workspace/remove-other-buffers t))

(defun +workspace|kill-other-buffers ()
  "Kill all interactive buffers from the current workspace."
  (interactive)
  (+workspace/remove-other-buffers))

(defun +workspace|hide-non-project-buffers ()
  "Hide all file buffers that don't belong to the project workspace."
  (interactive)
  (let ((project-path (or (expand-file-name (+workspace/workspace-project-root))
                          (projectile-project-root))))
    (-some--> (+workspace-buffer-list)
         ;; Dont remove non-remove buffers
         (--filter (buffer-file-name it) it)
         (--reject (s-contains? project-path (buffer-file-name it)) it)
         (--each (persp-remove-buffer it) it))))

Grep Modes

Bindings

(map!
 :map (wgrep-mode-map ivy-occur-grep-mode-map)
 :n [return] #'compile-goto-error

 :localleader
 :desc "Remove line" "d" (cmd! (let ((inhibit-read-only t))
                                 (+my/delete-current-line))))

Yasnippet

Utils

Expand snippet by name
(defun yas/insert-by-name (name)
  (require 'noflet)
  (noflet ((dummy-prompt
            (prompt choices &optional display-fn)
            (declare (ignore prompt))
            (or (cl-find name choices :key display-fn :test #'string=)
                (throw 'notfound nil))))
    (let ((yas-prompt-functions '(dummy-prompt)))
      (catch 'notfound
        (yas-insert-snippet t)))))
Use last src language

Used with src snippet to auto fill the language from the previously used src language.

(defun +yas/org-src-lang ()
  "Try to find the current language of the src/header at point.
Return nil otherwise."
  (save-excursion
    (pcase
        (downcase
         (buffer-substring-no-properties
          (goto-char (line-beginning-position))
          (or (ignore-errors (1- (search-forward " " (line-end-position))))
              (1+ (point)))))
      ("#+property:"
       (when (re-search-forward "header-args:")
         (buffer-substring-no-properties
          (point)
          (or (and (forward-symbol 1) (point))
              (1+ (point))))))
      ("#+begin_src"
       (buffer-substring-no-properties
        (point)
        (or (and (forward-symbol 1) (point))
            (1+ (point)))))
      ("#+header:"
       (search-forward "#+begin_src")
       (+yas/org-src-lang))
      (_ nil))))

(defun +yas/org-last-src-lang ()
  (save-excursion
    (beginning-of-line)
    (when (search-backward "#+begin_src" nil t)
      (+yas/org-src-lang))))

Load custom snippets

(use-package! yasnippet
  :init
  (require 'doom-snippets nil t))

Babel Async

(use-package! ob-async
  :after org-babel)

Process List / Proced

Config

Proced Narrow
(use-package! proced-narrow
  :after proced
  :config
  (map!
   :map proced-mode-map
   :n "/" #'proced-narrow))
Shorten nixos path names in proced

Nixos paths can be very long, which is distracting in proced. This shortens the nixos paths to {nix}.

(defun +proced/remove-nixos-path-name (oldformat &rest xs)
  (let ((xs (--map (->> it
                        (s-replace-regexp "/nix/store/[^/]+" "{nix}")
                        (s-replace-regexp (template "^/home/<<(user-login-name)>>") "~")
                        ((lambda (x) (if (s-contains? "chromium" x) "{chromium}" x))))
                  xs)))
    (apply oldformat xs)))

(advice-add #'proced-format-args :around #'+proced/remove-nixos-path-name)

Bindings

(map! :map process-menu-mode-map
      :n "gr" #'list-processes)
(map! :map proced-mode-map
      :n "gr" #'proced)

NixOs

Edit indirect

Edit nix script regions in an indirect buffer just like org-edit-special.

(defun +nix|edit-indirect ()
  "Edit script in an indirect buffer."
  (interactive)
  (and-let* ((beg (save-excursion
                    (search-backward "''\n" nil t)
                    (forward-char 3)
                    (point)))
             (end (save-excursion
                    (re-search-forward "''" nil t)
                    (previous-line 1)
                    (goto-char (point-at-eol))
                    (point))))
    (+indirect-indent|edit beg end #'sh-mode)))

(map! :map nix-mode-map "C-c '" '+nix|edit-indirect)

(set-popup-rule! "^\\*edit-indirect" :ignore t)

Bindings

General

(map! :g "C-±" #'+popup/raise)

Disable emacs-state-toggle

(map! :nm "C-z" nil)

Super

(map!
 (:map override
   :g "s-n" #'evil-buffer-new
   :g "s-;" #'eval-expression
   :g "s-a" #'mark-whole-buffer
   :g "s-s" #'save-buffer
   :g "s-v" #'yank
   :g "s-x" #'execute-extended-command
   :g "s-y" #'helm-show-kill-ring

   ;; Text scale
   :g "s-="   #'doom/increase-font-size
   :g "s--"   #'doom/decrease-font-size
   :g "s-0"   #'doom/reset-font-size))

Evil

Config

Use global ex by default

I almost always want global search and replace, doom changed this in 1a6f50864. To undo this behavior just add g flag

(setq evil-ex-substitute-global t)

Motions

Replace With Register Motion

Replace the current selection with a register. Press gr with a following motion character like w.

(use-package! evil-replace-with-register
  :config
  (setq evil-replace-with-register-key (kbd "gr"))
  (define-key evil-normal-state-map
    evil-replace-with-register-key 'evil-replace-with-register)
  (define-key evil-visual-state-map
    evil-replace-with-register-key 'evil-replace-with-register))
Additional text objects
  • q for any type of quote
  • B for curly braces
  • r for square brackets
(after! evil
  (require 'evil-textobj-anyblock)
  (evil-define-text-object my-evil-textobj-anyblock-inner-quote
    (count &optional beg end type)
    "Select the closest outer quote."
    (let ((evil-textobj-anyblock-blocks
           '(("'" . "'")
             ("\"" . "\"")
             ("`" . "`")
             ("" . ""))))
      (evil-textobj-anyblock--make-textobj beg end type count nil)))

  (evil-define-text-object my-evil-textobj-anyblock-a-quote
    (count &optional beg end type)
    "Select the closest outer quote."
    (let ((evil-textobj-anyblock-blocks
           '(("'" . "'")
             ("\"" . "\"")
             ("`" . "`")
             ("" . ""))))
      (evil-textobj-anyblock--make-textobj beg end type count t)))

  (define-key evil-inner-text-objects-map "q" 'my-evil-textobj-anyblock-inner-quote)
  (define-key evil-outer-text-objects-map "q" 'my-evil-textobj-anyblock-a-quote)
  (define-key evil-inner-text-objects-map "r" 'evil-inner-bracket)
  (define-key evil-inner-text-objects-map "B" 'evil-inner-curly))
Up to next/previous indent

Text objects for lines after or before the current line, that have the same or deeper indent.

(defun evil-indent-plus--line-down-indent-range (&optional point)
  (require 'evil-indent-plus)
  (let* ((range (evil-indent-plus--same-indent-range point))
         (base (point))
         (begin (point)))
    (list begin (cl-second range) base)))

(evil-define-text-object evil-indent-plus-i-indent-line-down (&optional count beg end type)
  "Text object describing the block with the same (or greater) indentation as the current line,
and the line above, skipping empty lines."
  :type line
  (require 'evil-indent-plus)
  (evil-indent-plus--linify (evil-indent-plus--line-down-indent-range)))

(define-key evil-inner-text-objects-map "+" 'evil-indent-plus-i-indent-line-down)

(defun evil-indent-plus--line-up-indent-range (&optional point)
  (require 'evil-indent-plus)
  (let* ((range (evil-indent-plus--same-indent-range point))
         (base (point))
         (begin (point)))
    (list begin (cl-first range) base)))

(evil-define-text-object evil-indent-plus-i-indent-line-up (&optional count beg end type)
  "Text object describing the block with the same (or greater) indentation as the current line,
and the line above, skipping empty lines."
  :type line
  (require 'evil-indent-plus)
  (evil-indent-plus--linify (evil-indent-plus--line-up-indent-range)))

(define-key evil-inner-text-objects-map "-" 'evil-indent-plus-i-indent-line-up)
Little Word Motion
(defun load-evil-camel-case-motion ()
  (require 'evil-little-word)
  (define-key evil-normal-state-map (kbd "M-w") 'evil-forward-little-word-begin)
  (define-key evil-normal-state-map (kbd "M-b") 'evil-backward-little-word-begin)
  (define-key evil-operator-state-map (kbd "M-w") 'evil-forward-little-word-begin)
  (define-key evil-operator-state-map (kbd "M-b") 'evil-backward-little-word-begin)
  (define-key evil-visual-state-map (kbd "M-w") 'evil-forward-little-word-begin)
  (define-key evil-visual-state-map (kbd "M-b") 'evil-backward-little-word-begin)
  (define-key evil-visual-state-map (kbd "i M-w") 'evil-inner-little-word))

(load-evil-camel-case-motion)
Sort Motion
(evil-define-operator +evil/sort (beg end)
  "Sort lines with motion"
  (interactive "<r>")
  (sort-lines nil beg end))

(map!
 (:after evil
    :m "gS" #'+evil/sort))
Case Conversion

Copied code from strickinato/evil-briefcase since it’s not maintained anymore. Convert case motion via Z.

Example pressing ZciW would convert the inner Word into camelCase

(evil-define-operator +evil/case-upper (beg end type)
  "Convert text to upper case."
  (if (eq type 'block)
      (evil-apply-on-block #'s-upcase beg end nil)
    (let ((str (s-upcase (buffer-substring-no-properties beg end))))
      (delete-region beg end)
      (insert str))))

(evil-define-operator +evil/case-lower (beg end type)
  "Convert text to lowercase."
  (if (eq type 'block)
      (evil-apply-on-block #'s-downcase beg end nil)
    (let ((str (s-downcase (buffer-substring-no-properties beg end))))
      (delete-region beg end)
      (insert str))))

(evil-define-operator +evil/case-camel-upper (beg end type)
  "Convert text to CamelCase with a Capital C"
  (if (eq type 'block)
      (evil-apply-on-block #'s-upper-camel-case beg end nil)
    (let ((str (s-upper-camel-case (buffer-substring-no-properties beg end))))
      (delete-region beg end)
      (insert str))))

(evil-define-operator +evil/case-camel-lower (beg end type)
  "Convert text to camelCase with a small C"
  (if (eq type 'block)
      (evil-apply-on-block #'s-lower-camel-case beg end nil)
    (let ((str (s-lower-camel-case (buffer-substring-no-properties beg end))))
      (delete-region beg end)
      (insert str))))

(evil-define-operator +evil/case-snake-lower (beg end type)
  "Convert text to snake_case, slithering"
  (if (eq type 'block)
      (evil-apply-on-block #'s-snake-case beg end nil)
    (let ((str (s-snake-case (buffer-substring-no-properties beg end))))
      (delete-region beg end)
      (insert str))))

(evil-define-operator +evil/case-snake-upper (beg end type)
  "Convert text to SNAKE_CASE, AKA SCREAMING_SNAKE_CASE"
  (if (eq type 'block)
      (evil-apply-on-block #'s-snake-case beg end nil)
    (let ((str (s-upcase (s-snake-case (buffer-substring-no-properties beg end)))))
      (delete-region beg end)
      (insert str))))

(evil-define-operator +evil/case-kebab-upper (beg end type)
  "Convert text to KEBAB-KASE, mmmm... THICK MEAT"
  (if (eq type 'block)
      (evil-apply-on-block #'s-dashed-words beg end nil)
    (let ((str (s-dashed-words (buffer-substring-no-properties beg end))))
      (delete-region beg end)
      (insert (upcase str)))))

(evil-define-operator +evil/case-kebab-lower (beg end type)
  "Convert text to kebab-kase, mmmm... hyphens"
  (if (eq type 'block)
      (evil-apply-on-block #'s-dashed-words beg end nil)
    (let ((str (s-dashed-words (buffer-substring-no-properties beg end))))
        (delete-region beg end)
        (insert str))))

(map!
 (:after evil
  :m "Zu" #'+evil/case-upper
  :m "Zl" #'+evil/case-lower
  :m "ZC" #'+evil/case-camel-upper
  :m "Zc" #'+evil/case-camel-lower
  :m "ZS" #'+evil/case-snake-upper
  :m "Zs" #'+evil/case-snake-lower
  :m "ZK" #'+evil/case-kebab-upper
  :m "Zk" #'+evil/case-kebab-lower))
Org Src Block

Matches any block with BEGIN_, e.g.: SRC blocks.

(defun +evil-org/src-block ()
  "Matches src blocks using org-element-context."
  (-when-let* ((ctx (org-element-context))
               (type (car ctx))
               (ctx (cond
                     ((equal type 'src-block) ctx)
                     ((equal type 'example-block) ctx)
                     ((equal type 'quote-block) ctx)
                     ;; Inside quote blocks org-element-context matches the paragraphs
                     ;; So we have to take the parent block to get the quote-block
                     ((equal type 'paragraph)
                      (-some->> ctx
                        (nth 1)
                        ((lambda (x) (doom-plist-get x :parent)))
                        (--id-when (equal (car it) 'quote-block)))))))
    ctx))

(evil-define-text-object +evil-org-inner-src-block (count &optional beg end type)
  "Select an org src/quote/example block object."
  (evil-org-select-inner-element (+evil-org/src-block)))

(evil-define-text-object +evil-org-an-src-block (count beg end type)
  "An org object.
Matches urls and table cells."
  (evil-org-select-an-element (+evil-org/src-block)))

(after! evil-org
  (evil-define-key '(visual operator) evil-org-mode-map
    "ib" #'+evil-org-inner-src-block
    "ab" #'+evil-org-an-src-block))

Normal Bindings

Window navigation

Fix window navigation for various modes. I don’t like pressing C-w or SPC w as leader to navigate, so I have to work around it:

(map!
 :en "C-h"   #'evil-window-left
 :en "C-j"   #'evil-window-down
 :en "C-k"   #'evil-window-up
 :en "C-l"   #'evil-window-right)

(map!
 :map (image-mode-map
       magit-diff-mode-map
       magit-revision-mode-map
       magit-status-mode-map
       eshell-mode-map
       evil-org-mode-map)
 :en "C-h"   #'evil-window-left
 :en "C-j"   #'evil-window-down
 :en "C-k"   #'evil-window-up
 :en "C-l"   #'evil-window-right)

(add-hook! 'eshell-first-time-mode-hook
  (map!
   :map eshell-mode-map
   :en "C-h"   #'evil-window-left
   :en "C-j"   #'evil-window-down
   :en "C-k"   #'evil-window-up
   :en "C-l"   #'evil-window-right))

(map!
 :map org-agenda-mode-map
 "C-h"  #'evil-window-left
 "C-j"  #'evil-window-down
 "C-k"  #'evil-window-up
 "C-l"  #'evil-window-right)
History navigation in minibuffer
(define-key minibuffer-local-map "\C-p" 'previous-history-element)
(define-key minibuffer-local-map "\C-n" 'next-history-element)
Jump to last buffer
(map! :n "gb" #'evil-switch-to-windows-last-buffer)
Select last paste
(defun +evil|select-pasted ()
  (interactive)
  (let ((start-marker (evil-get-marker ?\[))
        (end-marker (evil-get-marker ?\])))
    (evil-visual-select start-marker end-marker)))

(map! :n "gp" #'+evil|select-pasted)
Go up one directory from the current buffer
(map! :m "-"  #'dired-jump)
Visual Line Mode Navigation
(map! :map visual-line-mode-map
      :nv "j" #'evil-next-visual-line
      :nv "k" #'evil-previous-visual-line)

Insert Bindings

Insert Mode bindings, mostly unicode insertion and workaround for german umlaut.

Insert from the kill ring in insert mode
(map! :i "A-y" #'helm-show-kill-ring)
Unicode Characters
(map!
 :i "M-`"   (cmd! (insert "°"))
 :i "M-."   (cmd! (insert ""))
 :i "M-^"   (cmd! (insert "°"))
 :i "M-l"   (cmd! (insert "λ"))
 :i "M-w"   (cmd! (insert ""))
 :i "M-i"   (cmd! (insert "")))

Square Bracket Bindings

Global [ & ] combinator bindings

Dumb Jump
(map!
 :n "]F" #'dumb-jump-go
 :n "[F" #'dumb-jump-back)
Flycheck Error Jumping
(map!
 :n "]e" #'flycheck-next-error
 :n "[e" #'flycheck-previous-error)

Text Objects

Custom evil text objects mostly stolen from Spacemacs|define-text-object-regexp.

Utils
Define Text Objects
(defmacro +evil/define-text-object-regexp (key name start-regexp end-regexp)
  "Define a text object.
START-REGEXP and END-REGEXP are the boundaries of the text object."
  (let ((inner-name (make-symbol (concat "evil-inner-" name)))
        (outer-name (make-symbol (concat "evil-outer-" name))))
    `(progn
       (evil-define-text-object ,inner-name (count &optional beg end type)
         (evil-select-paren ,start-regexp ,end-regexp beg end type count nil))
       (evil-define-text-object ,outer-name (count &optional beg end type)
         (evil-select-paren ,start-regexp ,end-regexp 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)))))
Config
(+evil/define-text-object-regexp "~" "tilde" "~" "~")
(+evil/define-text-object-regexp "=" "equal" "=" "=")
(+evil/define-text-object-regexp "|" "bar" "|" "|")
(+evil/define-text-object-regexp "*" "star" "*" "*")
(+evil/define-text-object-regexp "$" "dollar" "$" "$")
(+evil/define-text-object-regexp "%" "percent" "%" "%")
(+evil/define-text-object-regexp "/" "slash" "/" "/")
(+evil/define-text-object-regexp "_" "underscore" "_" "_")
(+evil/define-text-object-regexp "-" "hyphen" "-" "-")
Quotes Text Object

Changes the text matching inside quotes with q motion (e.g. ciq) Change inner bracket with r

(after! evil
  (require 'evil-textobj-anyblock)
  (evil-define-text-object my-evil-textobj-anyblock-inner-quote
    (count &optional beg end type)
    "Select the closest outer quote."
    (let ((evil-textobj-anyblock-blocks
           '(("'" . "'")
             ("\"" . "\"")
             ("`" . "'")
             ("" . ""))))
      (evil-textobj-anyblock--make-textobj beg end type count nil)))

  (evil-define-text-object my-evil-textobj-anyblock-a-quote
    (count &optional beg end type)
    "Select the closest outer quote."
    (let ((evil-textobj-anyblock-blocks
           '(("'" . "'")
             ("\"" . "\"")
             ("`" . "'")
             ("" . ""))))
      (evil-textobj-anyblock--make-textobj beg end type count t)))

  (define-key evil-inner-text-objects-map "q" 'my-evil-textobj-anyblock-inner-quote)
  (define-key evil-outer-text-objects-map "q" 'my-evil-textobj-anyblock-a-quote)
  (define-key evil-inner-text-objects-map "r" 'evil-inner-bracket))

Ex Commands

Extend the default evil ex commands from +commands.el

(after! evil-ex
  :config
  (evil-ex-define-cmd "W" #'evil-write))

MultiEdit

(map! :nv "C-M-d" #'evil-multiedit-match-all)

Evil Jump with tab

(map! :n [tab] (general-predicate-dispatch nil
                 (and (featurep! :editor fold)
                      (save-excursion (end-of-line) (invisible-p (point))))
                 #'+fold/toggle
                 (fboundp 'evil-jump-item)
                 #'evil-jump-item)
      :v [tab] (general-predicate-dispatch nil
                 (and (bound-and-true-p yas-minor-mode)
                      (or (eq evil-visual-selection 'line)
                          (not (memq (char-after) (list ?\( ?\[ ?\{ ?\} ?\] ?\))))))
                 #'yas-insert-snippet
                 (fboundp 'evil-jump-item)
                 #'evil-jump-item))

Evil Paste Fix

Evil pastes at the current cursor, which I don’t expect it to do. Most of the time it requires me to move into insert mode, move one character, and then do my function.

(defun +evil/normal-mode-paste-fix (fn &optional insert-char)
  "Move forward one character and then paste."
  (interactive)
  (if (and (evil-normal-state-p)
           (+my/buffer-line-has "[^\s]"))
      (progn
        (forward-char 1)
        (when (and insert-char (looking-at insert-char))
          (insert insert-char))
        (let ((line (substring-no-properties (thing-at-point 'line))))
          (call-interactively fn)
          (when (eq line (substring-no-properties (thing-at-point 'line)))
            (delete-char 1))))
    (call-interactively fn)))

Leader

(map!
 :leader
 "RET"  #'+bookmarks|main
 "'"  #'+popup/toggle
 "au" #'undo-tree-visualize
 "-"  #'quick-calc)

Buffer

(map!
 :leader
 (:prefix-map ("b" . "buffer")
   :desc "Rename Buffer"  "r" #'rename-buffer))

File

(map!
 :leader
 (:prefix-map ("f" . "file")
  :desc "Open Private Config" "P" (cmd! (find-file (f-join doom-private-dir "config.org")))
  (:prefix-map ("g" . "goto")
   :desc "Drive"          "/" #'+ivy|counsel-mounted-drives
   :desc "Desktop"        "D" (cmd! (find-file "~/Desktop"))
   :desc "Code"           "c" (cmd! (find-file "~/Code"))
   :desc "Last captured"  "c" (cmd! (org-goto-marker-or-bmk org-capture-last-stored-marker))
   :desc "Downloads"      "d" (cmd! (find-file "~/Downloads"))
   :desc "Elfeed"         "e" (cmd! (find-file (car rmh-elfeed-org-files)))
   :desc "Scans"          "s" (cmd! (find-file (f-dirname (f-expand +docs:index-file))))
   :desc "Music"          "m" (cmd! (find-file "~/Media/Music"))
   :desc "Project Root"   "p" (cmd! (find-file (projectile-project-root)))
   :desc "Last refiled"   "r" (cmd! (org-refile-goto-last-stored))
   :desc "Tmp"            "t" (cmd! (find-file "/tmp"))
   :desc "Home"           "h" (cmd! (find-file "~"))
   :desc "Home"           "h" (cmd! (find-file "~")))))

Git

(map!
 :leader
 (:prefix-map ("g" . "git")
   :desc "Amend Commit (No Message)"           "A" (cmd! (magit-commit-amend "--no-edit"))
   :desc "Blame"                               "B" #'magit-blame
   :desc "Changed Files"                       "F" #'+git|ivy-changed-files
   :desc "New Branch"                          "N" #'magit-branch-spinoff
   :desc "Show revision original File"         "O" #'magit-revision-show-original-file
   :desc "Map-editor Changed Files"            "T" (cmd! (+git|ivy-changed-files "map-editor"))
   :desc "Amend Commit"                        "a" #'magit-commit-amend
   :desc "Checkout"                            "b" #'magit-checkout
   :desc "Diff"                                "d" #'magit-diff
   :desc "Push"                                "p" #'magit-push
   :desc "Undo"                                "u" #'+git|undo
   :desc "Worktree Popup"                      "w" #'magit-worktree
   (:prefix ("l" . "list")
    :desc "List gists"                         "g" #'+gist:list
    :desc "List submodules"                    "n" #'magit-list-submodules
    :desc "List issues"                        "p" #'forge-list-issues
    :desc "List pull requests"                 "r" #'forge-list-pullreqs
    :desc "List pull awaiting review requests" "R" #'forge-list-requested-reviews
    :desc "List notifications"                 "s" #'forge-list-notifications)))

Insert

(map!
 :leader
 (:prefix-map ("i" . "insert")
   :desc "New Snippet"    "S" #'+snippets/new
   :desc "Killring"       "y" #'helm-show-kill-ring))

Open

(map!
 :leader
 (:desc "open" :prefix "o"
   :desc "Calc"         :g "c" #'calc
   :desc "Calc"         :g "C" #'=calendar
   :desc "Transmission" :g "D" #'transmission
   :desc "Elfeed"       :g "e" #'+eshell/toggle
   :desc "Eshell"       :g "E" #'+rss|open
   :desc "Irc"          :g "i" #'=irc
   :desc "Mail"         :g "m" #'=mu4e
   :desc "Proced"       :g "p" #'proced
   :desc "Snippet"      :g "s" #'+snippets/edit))

Projects

(map!
 :leader
 (:prefix-map ("p" . "project")
  :desc "Workspace Project Files" "P" #'+workspace|find-workspace-project-file
  :desc "Project VC"              "v" #'+workspace|workspace-project-vc
  :desc "Project Bookmarks"       "RET" #'+bookmarks))

Search

(map!
 :leader
 (:prefix-map ("s" . "search")
   :desc "Project for symbol" "P" #'+default/search-project-for-symbol-at-point))

Toggle

(map!
 :leader
 (:prefix-map ("t" . "toggle")
   :desc "Theme Dark/Light"     "t" #'+doom|toggle-theme))

Window

(map!
 :leader
 :desc  "Split Vertical"        "|"    #'evil-window-vsplit
 :desc  "Split Horizontal"      "-"    #'evil-window-split

 (:prefix-map ("w" . "window")
  :desc  "Delete"               "d"    #'delete-window
  :desc  "Ace Delete Windo"     "D"    #'ace-delete-window
  :desc  "New"                  "n"    #'evil-window-new

  :desc  "Undo"                 "u"    #'winner-undo
  :desc  "Redo"                 "r"    #'winner-redo
  :desc  "Enlargen"             "o"    #'doom/window-enlargen

  :desc  "Toggle Split"         "T"    #'+window|toggle-split-direction

  :desc  "Split Vertical"       "|"    #'evil-window-vsplit
  :desc  "Split Horizontal"     "_"    #'evil-window-split

  :desc  "Move Left"            "H"    #'+evil/window-move-left
  :desc  "Move Down"            "U"    #'+evil/window-move-down
  :desc  "Move Up"              "K"    #'+evil/window-move-up
  :desc  "Move Right"           "L"    #'+evil/window-move-right

  :desc  "Set Height"           "C-_"  #'evil-window-set-height
  :desc  "Set Height"           "C-|"  #'evil-window-set-width

  :desc  "Swap"                 "SPC"  #'ace-swap-window
  :desc  "Toggle Locked"        "#"    #'+my|toggle-window-dedicated
  :desc "Toggle Locked"         "."    #'+popup/raise))

Workspace

(map!
 :leader
 (:prefix-map ("TAB" . "workspace")
  :desc "Switch to"     "." #'+workspace/my-switch-to
  :desc "Close others"  "o" #'+workspace|close-others
  :desc "Create"        "c" #'+workspace/new-named
  :desc "Rename"        "," #'+workspace/rename
  :desc "Project Files" "p" #'+workspace|find-workspace-project-file
  :desc "Project VC"    "v" #'+workspace|workspace-project-vc
  :desc "Clone"         "C" (cmd!
                             (+workspace/new (format "Clone: %s" (+workspace-current-name)) t)
                             (message "Cloned current workspace %s" (+workspace-current-name)))
  :desc "Switch to last workspace" "0"   #'+workspace/other))

Yank

(defun +yank/dired-path ()
  "Returns the current dired entry or the buffer directory."
  (or (dired-file-name-at-point) dired-directory))

(defun +yank/buffer-filename ()
  "Returns the current buffers file name if possible."
  (cond ((doom-dired-buffer-p (current-buffer)) (+yank/dired-path))
        (buffer-file-name buffer-file-name)
        (org-src-source-file-name org-src-source-file-name)
        (t nil)))

(defun +yank/buffer-path ()
  "Returns the current buffers path."
  (or (+yank/buffer-filename) default-directory))

(defun +yank|filename ()
  "Yank the buffer file name."
  (interactive)
  (or
   (-some->> (+yank/buffer-filename)
     ;; When pointing at a directory at dired, I still take the directory
     ((lambda (x) (if (f-file? x)
                      (file-name-directory x)
                    x)))
     (+my/kill-and-message))
   (user-error "Error: Not a file bufer!")))

(defun +yank|base ()
  "Yank the buffer files base name."
  (interactive)
  (or
   (-some->> (+yank/buffer-filename)
     (file-name-base)
     (+my/kill-and-message))
   (user-error "Error: Not a file bufer!")))

(defun +yank|directory ()
  "Yank the buffer files directory."
  (interactive)
  (or
   (-some->> (+yank/buffer-path)
     (file-name-directory)
     (+my/kill-and-message))
   (user-error "Error: Not a file bufer!")))

(defun +yank|path ()
  "Yank the buffer files path."
  (interactive)
  (or
   (-some->> (+yank/buffer-path)
     (+my/kill-and-message))
   (user-error "Error: Not a file bufer!")))

(defun +yank|relative-to-project ()
  "Yank the buffer path relative to the projectile-project-root."
  (interactive)
  (or
   (-some->> (+yank/buffer-path)
     (f-full)
     ((lambda (x)
        (or
         (->> (s-replace (projectile-project-root) "" x)
              (-id-when #'s-present?))
         x)))
     (+my/kill-and-message))
   (user-error "Error: Not a file bufer!")))

(map!
 :leader
 (:prefix-map ("y" . "Yank")
  :desc "filename"            "f" #'+yank|filename
  :desc "base"                "b" #'+yank|base
  :desc "directory"           "d" #'+yank|directory
  :desc "path"                "p" #'+yank|path
  :desc "path"                "y" #'+yank|path
  :desc "relative to project" "r" #'+yank|relative-to-project))

Local-Leader

Eshell

(defun floscr|+eshell|init-keymap ()
  "Setup additional custom eshell keybindings to already existing doom bindings. This must be done in a hook because eshell-mode
redefines its keys every time `eshell-mode' is enabled."
  (map! :map eshell-mode-map
        :in "C-p"         #'eshell-previous-input
        :in "C-n"         #'eshell-next-input

        :localleader "l"  #'eshell/clear))

(add-hook 'eshell-first-time-mode-hook #'floscr|+eshell|init-keymap)

Elisp

(map! :map emacs-lisp-mode-map
      ;;
      ;; Rearrange Sexps

      :gni "M-;"   #'symex-mode-interface
      :n "s-k"   (cmd! (sp-transpose-sexp)
                     (evil-previous-line))
      :n "s-j"   (cmd! (sp-push-hybrid-sexp)
                     (evil-next-line))

      ;; Eval Buffer
      :n "s-r" #'eval-buffer

      ;; Slurp and barf
      :n "g]"  #'sp-slurp-hybrid-sexp
      :n "g["  #'sp-forward-barf-sexp

      :localleader
      :desc "Ly Mode"                 "l"  #'+ly-mode
      :desc "Jump to tangled source"  "j"  #'org-babel-tangle-jump-to-org
      :desc "Symex Mode"              "s" #'symex-mode-interface
      :desc "Raise sexp"              "<" #'raise-sexp
      :desc "Barf Sexp"               ">" #'barf-sexp)

Bash

(map! :map sh-mode-map
      :localleader
      :desc "Eval Region"  "e" #'sh-execute-region
      :desc "Eval Region"  "E" #'executable-interpret)

Git

(map! :map magit-mode-map
      :localleader
      :desc "Toggle Magit Buffer Lock" "#" #'magit-toggle-buffer-lock)

Reasonml

(map! :map reason-mode-map
      :localleader
      :desc "Eval Region"  "r" #'refmt)

Mail

(map! :map (mu4e-view-mode-map mu4e-headers-mode-map)
      :localleader
      :g "x" (cmd!
              (require 'org-mu4e)
              (org-mu4e-store-and-capture)))

Beancount

(map! :map (mu4e-view-mode-map mu4e-headers-mode-map)
      :localleader
      :g "x" (cmd!
              (require 'org-mu4e)
              (org-mu4e-store-and-capture)))

Images

(map! :map 'image-mode-map
      :desc "Zoom in" :gn "+" #'image-increase-size
      :desc "Zoom in" :gn "-" #'image-decrease-size
      :desc "Zoom in" :gn "0" (cmd! (image-transform-set-scale 1)))

Minibuffer

Copy and Paste from the minibuffer

Since the minibuffer has no evil mode, i’ve got these bindings to help out:

  • M-c: Copy the minibuffer line
  • M-v: Paste from clipboard to minibuffer (Same as C-r 0) This also removes trailing newlines
(defun evil-get-register-string (REGISTER)
  "Get evil-register pure text content
Registers can be selected with ?letter
E.g.: ?* -> Clipboard Contents"
  (evil-vector-to-string (evil-get-register REGISTER)))

(defun paste-evil-register-clipboard-pruned ()
  "Paste the current clipboard pruned from newlines"
  (interactive)
  (insert (s-trim (shell-command-to-string "pbpaste")))
  (doom/forward-to-last-non-comment-or-eol))

(defun copy-minibuffer-line ()
  "Copies the minibuffer content to the clipboard"
  (interactive)
  (save-excursion
    (doom/forward-to-last-non-comment-or-eol)
    (set-mark-command nil)
    (doom/backward-to-bol-or-indent)
    (kill-ring-save (mark) (point))))

(defun setup-minibuffer ()
  "Set up keybindings for the minibuffer"
  (local-set-key (kbd "s-v") 'paste-evil-register-clipboard-pruned)
  (local-set-key (kbd "s-c") 'copy-minibuffer-line))

(add-hook 'minibuffer-setup-hook 'setup-minibuffer)

;; (define-key! :keymaps +default-minibuffer-maps
;;   "C-w" 'sp-backward-delete-word)

Hacks

Fix evil-org-mode-map

Fixes (void-function ad-Advice-newline-and-indent) error for now. I honestly don’t know where this comes from.

(map!
 :map evil-org-mode-map
 :after org
 :i [return] #'org-return-and-maybe-indent)

Prevent woman defvaralias error

I’m using woman for manuals, but it throws this error on first launch

Warning (defvaralias): Overwriting value of ‘woman-topic-history’ by aliasing to ‘Man-topic-history’

Solution from here: Doom Discord question

(add-to-list 'warning-suppress-types '(defvaralias))