Skip to content

Latest commit

 

History

History
2849 lines (2625 loc) · 105 KB

emacs.org

File metadata and controls

2849 lines (2625 loc) · 105 KB

Emacs Config

Emacs is a very versatile tool. I use it as:

What’s more, I’m using a distribution of Emacs called Spacemacs, which comes with many sensible defaults and simplifies the installation and setup of packages. Because it is so feature-rich, Emacs can take an annoying time to start up. That’s why I have set up a systemd service that runs an emacs daemon on system startup. When I want to launch emacs to perform a task, I run myemacs (a custom command that wraps emacsclient), which attaches to the daemon, without the need to load the cruft each time.

Daemons

For some of those use cases I mentioned above, I want to run a standalone emacs daemon, specialized for that use case. One reason is that emacs is single-threaded, so a hang-up in one application would render the rest of the daemon unresponsive. Another reason is simply to have a cleaner buffer list, so I can use ibuffer more efficiently.

Here’s a list of standalone daemons I have configured. Each one has a systemd service configured and thus launches on system startup. For each entry the daemon name is shown (as passed to the --socket-name option). Each daemon has its own configuration file (see Configuration loading) to prevent loading of unnecessary code.

irc
Used as an IRC chat client
Configuration file
~/.emacs.d/haris/irc.el
<<irc.service>>
    
ide
Used as a multilingual IDE via the lsp protocol

NOTE: The functionality for this daemon is not yet configured.

emacs
Everything else is done through this daemon. This includes basic text editing and applications that don’t warrant a standalone daemon.
Configuration file
~/.emacs.d/default.el

TODO: It is planned that this config file be used, but it’s not yet implemented.

Configuration loading

The main generated user configuration file is ~/.emacs.d/haris/root.el. This file is loaded by spacemacs and internally, it loads the other files based on the type of the purpose of the current emacs daemon (by reading the server-name variable). This is the config file:

(load-file (cond ((and (boundp 'server-name) (equal server-name "irc"))
                  "~/.emacs.d/haris/irc.el")
                 ("~/.emacs.d/haris/default.el")))

General

Setup

Run this script in order to perform a first-time setup of Emacs: ~/.haris-setup/setup-emacs.sh.
set -e

Initialize spacemacs repo

mkdir -p ~/.emacs.d

pushd ~/.emacs.d >/dev/null
if [ ! -d .git ]; then
    git init
    git remote add origin 'https://github.com/veracioux/spacemacs'
    git remote set-url --push origin '[email protected]:veracioux/spacemacs'
    git remote add upstream 'https://github.com/syl20bnr/spacemacs'
    git checkout --recurse-submodules haris/main
fi
popd >/dev/null

Interactively install spacemacs

ln -s ~/.haris/.spacemacs ~/.spacemacs || true
echo "Emacs will be launched now. Follow the prompts to setup spacemacs"
echo "When done, you can close the emacs window"
read -n1 -p 'Press any key to continue: '
# NOTE: In same cases I observed that the GUI won't run on initial spacemacs
# startup, so I used --no-window-system
COLORTERM=truecolor LSP_USE_PLISTS=true emacs --debug-init --no-window-system

Theme

I use a slightly customized version of the beautiful Dracula theme. Make sure you perfomed the setup in order to have the theme available.

Configure custom theme directory

(setq custom-theme-directory "~/.emacs.d/private/themes")

Theme: Dracula

(load-theme 'dracula t)
(add-hook 'after-make-frame-functions
          (defun haris/load-theme-delayed (frame)
            ;; Without this the theme only loads after a second frame is created
            (run-with-timer 0 nil
                            (lambda ()
                              (load-theme 'dracula t)))
            (remove-hook 'after-make-frame-functions #'haris/load-theme-delayed)))

Package bootstrapping

I normally use use-package for installing packages (with melpa as the default source). I use straight to clone some packages that are not available on melpa.

(defvar bootstrap-version)
(let ((bootstrap-file
       (expand-file-name
        "straight/repos/straight.el/bootstrap.el"
        (or (bound-and-true-p straight-base-dir)
            user-emacs-directory)))
      (bootstrap-version 7))
  (unless (file-exists-p bootstrap-file)
    (with-current-buffer
        (url-retrieve-synchronously
         "https://raw.githubusercontent.com/radian-software/straight.el/develop/install.el"
         'silent 'inhibit-cookies)
      (goto-char (point-max))
      (eval-print-last-sexp)))
  (load bootstrap-file nil 'nomessage))

(setq straight-vc-git-default-clone-depth 1)

(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t)
(package-initialize)
(require 'use-package)
;; (setq use-package-defaults (assq-delete-all ':straight use-package-defaults))
(setq use-package-always-ensure t)

Global packages

(use-package focus-autosave-mode :defer t)
(use-package multi-vterm :defer t)
(use-package command-log-mode)
(straight-use-package
 '(explain-pause-mode
   :type git
   :host github
   :repo "lastquestion/explain-pause-mode"))
(use-package fontawesome :defer t)

Language modes

(use-package json-mode :defer t)
(use-package counsel-jq :defer t)
(use-package fish-mode :defer t)
(use-package vimrc-mode :defer t)
(use-package sxhkdrc-mode :defer t)
(use-package i3wm-config-mode :defer t)
(use-package git-modes :defer t)
(use-package systemd :defer t)
(use-package ssh-config-mode :defer t)
(use-package crontab-mode :defer t)

Global settings

;; Performance improvements
(setq native-comp-async-report-warnings-errors nil)
;; 500 MB
(setq gc-cons-threshold (* 500 1024 1024))

(setq paradox-github-token t)

(setq image-auto-resize 'fit-width)
(setq image-auto-resize-on-window-resize t)
(setq evil-want-keybinding nil)
(setq uniquify-buffer-name-style 'forward)
(setq frame-title-format "%b"
      dotspacemacs-frame-title-format nil)
(eval-after-load "recentf"
  (defun haris//after-load/recentf ()
    (add-hook 'dired-after-readin-hook
              (defun haris//add-file-to-recentf ()
                (recentf-add-file default-directory)))
    (add-to-list 'recentf-exclude "^/tmp/haris-pipe-")
    (cl-delete-if (lambda (el) (string= el "~/.emacs.d/elpa/29.1/develop/"))
                  recentf-exclude)))
(setq winum-scope 'frame-local)

(add-to-list 'image-types 'svg)
;; Enable clipboard in the terminal
(use-package xclip)
;; Using xclip instead makes emacsclient hang
(setq xclip-method 'xsel)
(setq xclip-program "xsel")
(xclip-mode)
(setq warning-minimum-level :emergency)

;; Handle URLs as files
(url-handler-mode)

(add-to-list 'exec-path (expand-file-name "~/.local/bin"))
(if (not (boundp 'haris/prepended-path))
    (progn
      (setq haris/prepended-path "yes")
      (setenv "PATH" (concat (expand-file-name "~/.local/bin") ":" (getenv "PATH")))))

;; TODO whatever this does
(add-hook 'text-mode-hook #'auto-fill-mode)
(add-hook 'prog-mode-hook #'auto-fill-mode)

;; Some packages prefer helm over ivy and don't provide a customization option.
(with-eval-after-load 'helm
  (fmakunbound 'helm))

Editor settings

(setq fill-column 80)
(setq-default tab-width 4)
(spacemacs/toggle-visual-line-navigation-globally-on)
(set-language-environment "UTF-8")
(modify-syntax-entry ?_ "w")
(setq indent-guide-delay 0.1)

External tools

(setq browse-url-generic-program (executable-find "firefox"))
(setq dotspacemacs-search-tools '("rg" "grep"))

;; Ways to spawn shells from within Emacs
(setq shell-default-shell 'shell)
(setq terminal-here-linux-terminal-command '("alacritty")
      terminal-here-mac-terminal-command   '("alacritty"))

(setq shell-file-name "bash")

Scrolling

(setq scroll-bar-mode-explicit nil)
(setq mouse-wheel-scroll-amount '(6))

;; Fix scrolling performance
(use-package fast-scroll)
(fast-scroll-config)
(fast-scroll-mode 1)

Custom miscellaneous backup file patterns

I change the following in order to prevent Emacs from complaining about filenames being too long. The main culprit is Java, with its horrendously deep directory structures.

(make-directory "~/.emacs.d/.cache/auto-save/dist" t)
(make-directory "~/.emacs.d/.cache/auto-save/site" t)

(dolist (item auto-save-file-name-transforms)
  (let ((uniquify (nthcdr 2 item)))
    (if uniquify (setcar uniquify 'md5))))

(defalias 'haris//undo-tree-make-history-save-file-name/default
  (symbol-function
   (if (fboundp 'haris//undo-tree-make-history-save-file-name/default)
       'haris//undo-tree-make-history-save-file-name/default
     'undo-tree-make-history-save-file-name))
  "The original implementation of the undo-tree-make-history-save-file-name
function, so I can call it in my custom override.")

(defun undo-tree-make-history-save-file-name (file)
  "Create the undo history file name for FILE. This overrides the default
implementation, by making the basename of the file a hashed version of the
original path. The benefit of this is that it prevents a 'filename too long
error'."
  (let ((original (haris//undo-tree-make-history-save-file-name/default file)))
    (concat (file-name-directory original)
            (md5 (file-name-nondirectory file))
            ".~undo-history~")))

Call these to test the patterns:

(make-auto-save-file-name)
(undo-tree-make-history-save-file-name (buffer-file-name))

I change this in order to prevent non-emacs tools from picking up the files, and misbehaving due to their existence.

(make-directory "~/.emacs.d/.cache/lock-file/dist" t)
(make-directory "~/.emacs.d/.cache/lock-file/site" t)

;; Set lock-file-name-transforms to auto-save-file-name-transforms,
;; but replace "auto-save" with "lock-file" in each replacement pattern.
(setq lock-file-name-transforms
      (remove nil
       (mapcar
       (lambda (item)
         (let ((item-copy (copy-tree item)))
           (if (string-match "auto-save" "lock-file")
            (setcar (cdr item-copy)
                   (string-replace "auto-save" "lock-file" (cadr item-copy))))
           item-copy))
       auto-save-file-name-transforms)))

(dolist (item lock-file-name-transforms)
  (let ((uniquify (nthcdr 2 item)))
    (if uniquify (setcar uniquify 'sha512))))

You can test the transforms by calling this:

(make-lock-file-name (buffer-file-name))

Auto-revert mode

(global-auto-revert-mode t)
(setq auto-revert-verbose t
      auto-revert-use-notify t
      auto-revert-avoid-polling t
      ;; When reverting buffers whose files have been changed by emacs, it
      ;; seems that the interval from this variable is used, even though
      ;; polling was disabled above.
      ;; An example where this can be observed is when an org-mode code block
      ;; is tangled and the target file is open in another buffer.
      auto-revert-interval 0.2)

Buffer cleanup

(setq haris/custom-temp-buffer-name "*haris-temp*")

(setq clean-buffer-list-delay-general 1
      clean-buffer-list-delay-special 1800
      clean-buffer-list-kill-regexps
      `(
        "^haris-pipe-"
        "^\\*Help\\*"
        "^\\*helpful "
        "^\\*ivy-occur"
        "^\\*lsp-help\\*"
        "^\\*lsp session\\*"
        "^magit: "
        "^magit-\\(log\\|diff\\|stash\\|revision\\)"
        "^\\*Man "
        "^\\*straight-process\\*"
        "\\*which-key\\*"
        "^\\*xref\\*"
        ;; Docker command output buffers
        "^\\* docker .*\\*"
        ;; Docker compose command output buffers
        "^\\* .* docker-compose .*\\*"
        (concat "^" (regexp-quote haris/custom-temp-buffer-name))))

This enables periodic cleanup:

(when (boundp 'haris/timer/clean-buffer-list)
  (cancel-timer haris/timer/clean-buffer-list)
  (makunbound 'haris/timer/clean-buffer-list))
(setq haris/timer/clean-buffer-list (run-with-timer 1800 1800 #'clean-buffer-list))

Global bindings

(global-set-key (kbd "C-+") 'text-scale-adjust)
(global-set-key (kbd "C--") 'text-scale-adjust)
(global-set-key (kbd "C-0") (defun haris//text-scale-reset ()
                              (interactive)
                              (text-scale-set 0)))

Functions

(defun haris/stage ()
  "Go to a user-specific temporary staging directory, useful as a playground."
  (interactive)
  (let ((dir (format "/tmp/stage-%s"
                     (user-login-name))))
    (mkdir dir t)
    (dired dir)))

(defun haris/force-kill-window (&optional window)
  "Kill a window, and the frame as well if it's the last one."
  (interactive)
  (let ((frame (window-frame window)))
    (if (eq (length (window-list frame)) 1)
        (delete-frame frame)
      (quit-window window))))

(defun haris/yas-minor-mode-on ()
  "Like yas-minor-mode-on, but do not honor yas-dont-activate-functions"
  (yas-minor-mode 1))

(defun haris/fifo (text)
  "Send TEXT to the fifo I use for testing"
  (interactive "sInput: ")
  (with-temp-buffer
    (insert text "\n")
    (shell-command-on-region
     (point-min)
     (point-max)
     "fifo")))

Avy

Try to make evil-easymotion work bidirectionally

(define-key evil-normal-state-map (kbd "M-w") 'avy-goto-word-0)
(define-key evil-normal-state-map (kbd "M-f") 'avy-goto-char)

Dired

(setq dired-listing-switches "-al \"--time-style=+%Y-%m-%d %H:%M\"")
(setq dired-kill-when-opening-new-dired-buffer t)

Additional packages

(use-package dired-rsync :defer t)
(use-package dired-rsync-transient :defer t)
(straight-use-package '(dired-hacks :type git :host github :repo "Fuco1/dired-hacks"))

Interactive commands

These are commands that I primarily intend to use interactively and directly, without binding them to any keys.

Commands from local shell scripts

All commands defined in ./scripts.org are taken and loaded as equivalent Elisp interactive commands. Each command is mapped to a function named haris/script/<script-name-from-scripts.org>. When this interactive command is run, it opens a vterm buffer named based on the command name, and runs the command there (without any arguments).

(funcall
 (defun haris/load-commands-from-local-shell-scripts ()
   "Load all local shell script commands as interactive Elisp commands."
   (interactive)
   (with-temp-buffer
     (org-mode)
     (setq-local org-use-tag-inheritance nil)
     (insert-file-contents "~/.haris/scripts.org")

     ;; Extract all applicable script commands
     (setq-local
      _commands
      (org-map-entries
       (lambda () (let ((title (nth 4 (org-heading-components))))
                    (string-replace "=" "" title)))
       "script" nil))

     ;; Create an interactive function definition for each command
     (mapcar
      (lambda (command)
        (eval
         `(defun ,(intern (format "haris/script/%s" command))
              ;; Arglist
              (prefix-arg)
            ;; Docstring
            ,(format
              "Interactive command corresponding to the custom local shell script '%s'"
              command)
            (interactive "P")
            (let ((default-directory "~")
                  (command ,command)
                  (_vterm nil)
                  (run-command nil))
              ;; Run multi-vterm
              (setq _vterm (multi-vterm))
              (with-current-buffer _vterm
                ;; Rename the buffer based on the command name
                (rename-buffer (format "*haris/script/%s*" command) t)
                (setq
                 run-command
                 (eval `(lambda (&optional argstring)
                          (interactive ,(format "sCLI arguments: %s " ,command))
                          "Run the command inside the open vterm buffer"
                          (comint-send-string
                           (get-buffer-process ,_vterm)
                           (format "%s %s\n" ,command (or argstring ""))))))
                ;; Run the command
                (if prefix-arg
                    ;; With prefix arg - prompt for CLI arguments before running
                    (call-interactively run-command)
                  ;; No prefix arg - run without CLI arguments
                  (run-with-timer 0.6 nil (eval `(lambda () (funcall ,run-command))))))))))
      _commands))))

(defun haris/remove-hook ()
  "Find the surrounding add-hook form and revert its effects."
  (interactive)
  (let* ((sexp (save-excursion
                 (search-backward
                  (if (eq (symbol-at-point) 'add-hook)
                      "("
                    "(add-hook"))
                 (sexp-at-point))))
    (setcar sexp 'remove-hook)
    (message "%s" sexp)
    (eval
     ;; Remove DEPTH argument, if any
     (cl-remove-if #'numberp sexp))))

Bindings

(defun haris/insert-tab ()
  (interactive)
  (insert-tab))

(defun haris/describe-symbol-at-point ()
  (interactive)
  (let ((was-in-minibuffer (minibufferp))
        (original-buffer (current-buffer)))
    (helpful-symbol (helpful--symbol-at-point))
    (when was-in-minibuffer (switch-to-buffer original-buffer))))

;; M-TAB in insert mode inserts a tab emulated by spaces
(define-key evil-insert-state-map (kbd "M-TAB") #'insert-tab)
;; "SPC +" will pop up eshell
(spacemacs/set-leader-keys "+" 'spacemacs/shell-pop-eshell)

;; Don't use it, plus it interferes with bindings such as forward-button
(eval-after-load "helpful"
  (lambda ()
    (define-key evil-normal-state-map (kbd "TAB") nil)))

;; Help bindings
(spacemacs/set-leader-keys "hdo" 'helpful-symbol)

(evil-define-key 'normal org-mode-map        (kbd "C-q")
  'haris/describe-symbol-at-point)
(evil-define-key 'normal emacs-lisp-mode-map (kbd "C-q")
  'haris/describe-symbol-at-point)
(evil-define-key 'normal ielm-map            (kbd "C-q")
  'haris/describe-symbol-at-point)
(evil-define-key 'normal read--expression-map (kbd "C-q")
  'haris/describe-symbol-at-point)

(evil-define-key 'normal helpful-mode-map (kbd "TAB") #'forward-button)
(spacemacs/declare-prefix "o" "custom")

Launching other programs at current context

Note: there is also spacemacs’ builtin SPC " that opens a terminal in-place.

(global-set-key (kbd "M-e")
                (defun haris/open-buffer-in-new-frame ()
                  (interactive)
                  (let ((buf (current-buffer)))
                    (select-frame (make-frame '((window-system . x))))
                    (switch-to-buffer buf))))
(global-set-key
 (kbd "M-v")
 (lambda () (interactive)
   (start-process "" nil "gvim" (buffer-file-name (window-buffer)))))

Spacemacs-like bindings

(defun haris/open-emacs.org ()
  (interactive)
  (find-file "~/.haris/emacs.org"))

(defun haris/load-user-config ()
  (interactive)
  (load-file "~/.emacs.d/haris/root.el"))

(defun haris/open-dotfiles-git ()
  (interactive)
  (magit-status "~/.haris"))

(define-key evil-normal-state-map (kbd "SPC f e h") #'haris/open-emacs.org)
(define-key evil-normal-state-map (kbd "SPC f e H") #'haris/open-dotfiles-git)
(define-key evil-normal-state-map (kbd "SPC f e r") #'haris/load-user-config)

(defalias 'spacemacs/default-pop-shell 'spacemacs/shell-pop-multivterm)

Consistent vim-like bindings

There are some inconsistencies in the vim key bindings (vim is guilty of this as well). For example D deletes until end of line, but V visually selects the whole line. This section remaps V to v$ and does the same for other similar cases. Some custom keybindings are defined here as well.

(define-key evil-normal-state-map (kbd "Q")     'delete-window)
(define-key evil-motion-state-map (kbd "Q")     'delete-window)

(define-key evil-visual-state-map (kbd "v")     'evil-visual-line)
(define-key evil-normal-state-map (kbd "V")     (kbd "v$"))
(setq evil-want-Y-yank-to-eol t)

(define-key evil-normal-state-map (kbd "C-a")   'evil-numbers/inc-at-pt)
(define-key evil-visual-state-map (kbd "C-a")   'evil-numbers/inc-at-pt)
(define-key evil-normal-state-map (kbd "C-x")   'evil-numbers/dec-at-pt)
(define-key evil-visual-state-map (kbd "C-x")   'evil-numbers/dec-at-pt)

(defun haris/nohighlight () (interactive)       (evil-ex-call-command "" "noh" ""))
(define-key evil-normal-state-map (kbd "M-/")   'haris/nohighlight)
(define-key evil-motion-state-map (kbd "M-/")   'haris/nohighlight)

(setq dotspacemacs-distinguish-gui-tab t)

Ielm

(setq ielm-dynamic-return nil)

;; Use RET to execute command even in normal mode
(evil-define-key 'normal ielm-map (kbd "RET") #'ielm-send-input)

;; Make RET in insert mode insert newline at point, unless the
;; point is at the end of the line, in which case send input.
(defun haris/ielm-insert-mode-return ()
  "Insert newline at point"
  (interactive)
  (if (= (point)
     (save-excursion
       (end-of-visual-line)
       (point)))
      (ielm-send-input)
    (ielm-return)))

(evil-define-key 'insert ielm-map (kbd "RET") #'haris/ielm-insert-mode-return)

Custom global map

;; Buffer map
(setq haris/buffer-prefix-map (make-sparse-keymap))
(spacemacs/set-leader-keys "ob" haris/buffer-prefix-map)
(define-key haris/buffer-prefix-map (kbd "r") #'rename-buffer)
(define-key haris/buffer-prefix-map (kbd "R") #'revert-buffer)
(define-key haris/buffer-prefix-map (kbd "c") #'clone-buffer)
(define-key haris/buffer-prefix-map (kbd "i") #'ibuffer)

;; Command log mode
(setq haris/command-log-prefix-map (make-sparse-keymap))
(spacemacs/set-leader-keys "oc" haris/command-log-prefix-map)
(define-key haris/command-log-prefix-map (kbd "l") #'haris/command-log)

;; Misc
(with-eval-after-load 'go-translate
  (spacemacs/set-leader-keys "ot" #'gt-do-translate))
(with-eval-after-load 'npm-mode
  (spacemacs/set-leader-keys "on" npm-mode-command-keymap))

;; Git
(setq haris/git-prefix-map (make-sparse-keymap))
(with-eval-after-load 'magit
  (spacemacs/set-leader-keys "og" haris/git-prefix-map))
(define-key haris/git-prefix-map (kbd "c") 'magit-find-git-config-file)

;; Friendly descriptions
(which-key-add-key-based-replacements
  "SPC o b" "Buffer manipulation"
  "SPC o c" "Command log"
  "SPC o c l" "Local command log"
  "SPC o t" "Translate"
  "SPC o n" "NPM"
  "SPC o g" "Git")
<<custom-global-map>>

Miscellaneous

;; Use M-y or M-n to answer a minibuffer prompt
(defun haris/insert-into-minibuffer-and-exit (text)
  (interactive)
  (with-current-buffer (window-buffer (active-minibuffer-window))
    (insert text)
    (exit-minibuffer)))

(global-set-key (kbd "M-y")
                (lambda ()
                  (interactive)
                  (haris/insert-into-minibuffer-and-exit "y")))
(global-set-key (kbd "M-n")
                (lambda ()
                  (interactive)
                  (haris/insert-into-minibuffer-and-exit "n")))

(define-key comint-mode-map (kbd "M-h") (lambda ()
                                          "Search through current history"
                                          (interactive)
                                          (counsel-shell-history)))

Evil

(use-package evil-quickscope)
(global-evil-quickscope-mode)

(setq evil-lookup-func (lambda () (call-interactively #'man)))
(setq evil-want-C-i-jump t)

(add-hook 'evil-insert-state-exit-hook #'company-cancel)

evil-collection

(setq evil-collection-setup-minibuffer t)
;; Please keep this sorted
(evil-collection-init 'bluetooth)
(evil-collection-init 'bookmark)
(evil-collection-init 'calendar)
(evil-collection-init 'comint)
(evil-collection-init 'daemons)
(evil-collection-init 'docker)
(evil-collection-init 'doc-view)
(evil-collection-init 'edbi)
(evil-collection-init 'edebug)
(evil-collection-init 'explain-pause-mode)
(evil-collection-init 'git-timemachine)
(evil-collection-init 'ibuffer)
(evil-collection-init 'info)
(evil-collection-init 'ivy)
(evil-collection-init 'man)
(evil-collection-init 'minibuffer)
(evil-collection-init 'proced)
(evil-collection-init 'tablist)
(evil-collection-init 'tabulated-list)
(evil-collection-init 'yaml-mode)
(evil-collection-init 'compilation)
(evil-collection-init 'evil-mc)

evil-surround

(with-eval-after-load 'evil-surround
  (add-to-list 'evil-surround-pairs-alist '(?$ . ("\"$(" . ")\""))))

Functions

(defun haris/evil-define-key-both (keymap key def &rest bindings)
  "evil-define-key in both normal and insert state"
  ;; Forward to evil-define-key, handling &rest args properly
  (apply 'evil-define-key* (append `(normal ,keymap ,key ,def) bindings))
  (apply 'evil-define-key* (append `(insert ,keymap ,key ,def) bindings)))

Projectile

(setq projectile-require-project-root nil)
(setq projectile-auto-discover nil)
(setq projectile-track-known-projects-automatically t)
(setq projectile-git-ignored-command "git ls-files -zcoi -X=.gitignore")

(setq projectile-ignored-projects '("~/"))

(setq projectile-indexing-method 'hybrid
      projectile-enable-caching t)

Default known projects

(with-eval-after-load 'magit
  (let*
      ((projects (-filter
                  'file-directory-p
                  '("~/.haris/"
                    "~/.emacs.d/"
                    "~/.emacs.d/private/snippets/"
                    "~/.emacs.d/private/themes/dracula/"
                    "~/wiki/")))
       (worktrees
        (flatten-list
         (mapcar
          (lambda (proj)
            (append
             ;; Add all worktrees of the project's repo as projects
             (let ((default-directory proj))
               (mapcar 'car (magit-list-worktrees))
               ;; In case it is not a git repo, worktrees would be nil, so add the
               ;; project itself
               (list proj))))
          projects))))
    ;; Add each project (+ its worktrees) to projectile-known-projects
    (dolist (worktree worktrees)
      (add-to-list 'projectile-known-projects worktree nil 'file-equal-p)
      ;; Also create a .projectile file in case the directory exists and it is not
      ;; a git repo
      (when (and (file-exists-p worktree) (not (magit-git-repo-p worktree)))
        (f-touch (f-join worktree ".projectile"))))))

Keybindings

;; Provides better namespacing than the default spacemacs/projectile-shell-pop
(define-key spacemacs-cmds (kbd "p '") 'multi-vterm-project)

Programming languages

(add-hook 'prog-mode-hook 'spacemacs/toggle-fill-column-indicator)

YAML

(eval-after-load "yaml"
  (defun haris//after-load/yaml ()
    (use-package yaml-pro)))

;; Use yaml-ts-mode instead of yaml-mode (it's faster), unless the mode is docker-compose-mode.
(add-hook 'yaml-mode-hook
          (defun haris/replace-yaml-with-yaml-ts ()
            (when (not (eq major-mode 'docker-compose-mode))
              (yaml-ts-mode))))

(let ((hooks '(yaml-mode-hook yaml-ts-mode-hook))
      (hook))
  (dolist (hook hooks)
    (add-hook hook
              (lambda ()
                (require 'openapi-yaml-mode)
                (require 'openapi-preview)))
    (add-hook hook 'spacemacs/toggle-indent-guide)
    (add-hook hook #'eldoc-mode 90)
    (add-hook hook #'yaml-pro-ts-mode 91)
    (add-hook hook 'spacemacs/toggle-fill-column-indicator)
    (add-hook hook
              (lambda ()
                (setq-local counsel-jq-command "yq")) 92)
    (add-hook hook #'lsp 94)))

(add-hook 'yaml-pro-ts-mode-hook
          (lambda ()
            (setq-local lsp-enable-imenu nil)))

OpenAPI

(straight-use-package '(openapi-yaml-mode :type git :host github :repo "magoyette/openapi-yaml-mode"))
(straight-use-package '(openapi-preview :type git :host github :repo "merrickluo/openapi-preview"))

(eval-after-load "yaml"
  (defun haris//after-load/openapi ()
    (require 'openapi-yaml-mode)
    (require 'openapi-preview)))

(add-to-list 'auto-mode-alist '("(openapi|swagger)\\.ya?ml$" . openapi-yaml-mode))

Keybindings

(eval-after-load "yaml"
  (lambda ()
    (evil-define-key 'normal yaml-mode-map (kbd "SPC j =")    'lsp-format-buffer)

    (evil-define-key 'normal yaml-pro-ts-mode-map (kbd "M-n") 'yaml-pro-ts-next-subtree)
    (evil-define-key 'normal yaml-pro-ts-mode-map (kbd "g j") 'yaml-pro-ts-next-subtree)
    (define-key yaml-pro-ts-mode-map (kbd "C-c C-n") nil)

    (evil-define-key 'normal yaml-pro-ts-mode-map (kbd "M-p") 'yaml-pro-ts-prev-subtree)
    (evil-define-key 'normal yaml-pro-ts-mode-map (kbd "g k") 'yaml-pro-ts-prev-subtree)
    (define-key yaml-pro-ts-mode-map (kbd "C-c C-p") nil)

    (evil-define-key 'normal yaml-pro-ts-mode-map (kbd "g h") 'yaml-pro-ts-up-level)
    (define-key yaml-pro-ts-mode-map (kbd "C-c C-u") nil)

    (evil-define-key 'normal yaml-pro-ts-mode-map (kbd ", '") 'yaml-pro-edit-ts-scalar)
    (define-key yaml-pro-ts-mode-map (kbd "C-c '") nil)

    (evil-define-key 'normal yaml-pro-ts-mode-map (kbd ", <") 'yaml-pro-ts-unindent-subtree)
    (define-key yaml-pro-ts-mode-map (kbd "C-c <") nil)

    (evil-define-key 'normal yaml-pro-ts-mode-map (kbd ", >") 'yaml-pro-ts-indent-subtree)
    (define-key yaml-pro-ts-mode-map (kbd "C-c >") nil)

    (evil-define-key 'normal yaml-pro-ts-mode-map (kbd ", v") 'yaml-pro-ts-mark-subtree)
    (define-key yaml-pro-ts-mode-map (kbd "C-c @") nil)

    (evil-define-key 'normal yaml-pro-ts-mode-map (kbd "M-j") 'yaml-pro-ts-move-subtree-down)
    (define-key yaml-pro-ts-mode-map (kbd "s-<down>") nil)

    (evil-define-key 'normal yaml-pro-ts-mode-map (kbd "M-k") 'yaml-pro-ts-move-subtree-up)
    (define-key yaml-pro-ts-mode-map (kbd "s-<up>") nil)

    (evil-define-key 'normal yaml-pro-ts-mode-map (kbd ", d") 'yaml-pro-kill-subtree)
    (define-key yaml-pro-ts-mode-map (kbd "C-c C-x C-w") nil)

    (evil-define-key 'normal yaml-pro-ts-mode-map (kbd ", p") 'yaml-pro-ts-paste-subtree)
    (define-key yaml-pro-ts-mode-map (kbd "C-c C-x C-y") nil)))

Dependencies

yaml-language-server yamllint

JSON

(defun haris/json/set-indent-level () (setq-local js-indent-level 2))

(add-hook 'json-mode-hook 'haris/json/set-indent-level)
(add-hook 'json-mode-hook 'spacemacs/toggle-indent-guide)
(add-hook 'json-mode-hook 'json-ts-mode 90)

(defalias 'jq 'counsel-jq)

(setq counsel-jq-json-buffer-mode 'json-ts-mode)

LSP

Some performance improvement settings depend on the power of your hardware. You can find them here. You can place them in your private configuration.

(with-eval-after-load 'lsp
  (setq lsp-idle-delay 0.1)
  (setq lsp-keep-workspace-alive nil)

  ;; Diagnostic mode doesn't work well with flycheck
  (setq lsp-diagnostics-disabled-modes '(python-mode sh-mode))

  (setq lsp-enable-on-type-formatting nil)

  ;; This prevents lsp from overriding my custom 'company-backends'
  (setq lsp-completion-provider :none)

  (setq lsp-restart 'ignore)

  (setq lsp-auto-execute-action nil)
  (setq lsp-inlay-hint-enable t)

  ;; Prevents session persistence, which can cause performance issues
  (setq lsp-session-file nil)

  (add-to-list 'lsp-file-watch-ignored-directories "[/\\\\]venv$"))

Keybindings

(with-eval-after-load 'lsp
  ;; TODO: not working
  (haris/evil-define-key-both lsp-mode-map (kbd "M-RET") 'lsp-execute-code-action)
  (spacemacs/set-leader-keys-for-minor-mode
    'lsp-mode
    "FR" (defun haris/lsp-clear-workspace-folders ()
           (interactive)
           (mapcar (lambda (x) (lsp-workspace-folders-remove x))
                   (lsp-session-folders (lsp-session))))))

DAP

(setq dap-auto-show-output nil)

C/C++

(setq c-default-style "bsd"
      c-basic-offset 4)

(add-hook 'c-mode-hook    (lambda () (setq tab-width 4)))
(add-hook 'c++-mode-hook  (lambda () (setq tab-width 4)))

CMake

(defun haris/cmake-info () (interactive)
       (info-display-manual "cmake")
       (Info-top-node))
(defun haris/cmake-help () (interactive)
       (split-window-right-and-focus)
       (let ((symbol (cmake-symbol-at-point)))
         (haris/cmake-info)
         (Info-menu symbol)))

(evil-define-key 'normal cmake-mode-map (kbd ",hc") 'haris/cmake-info)
(evil-define-key 'normal cmake-mode-map (kbd ",hh") 'haris/cmake-help)

Dependencies

cmake-language-server

Python

Spacemacs: elpy layer downloaded from here

(add-hook 'python-mode-hook (lambda () (setq tab-width 4)))

(setq python-shell-interpreter "ipython")

(setq lsp-pylsp-plugins-pylint-enabled t
      lsp-pylsp-plugins-flake8-enabled nil
      lsp-pyls-plugins-flake8-enabled  nil
      lsp-diagnostics--flycheck-enabled t)

;; elpy
(setq elpy-modules nil)

Bindings

(with-eval-after-load 'lsp
  (evil-define-key 'normal lsp-mode-map (kbd ",GG") 'lsp-ui-doc-glance))

Dependencies

python-lsp-server flake8 python-typing_extensions python-lsp-black python-pylint
pyls-isort pyls-mypy pyls-memestra autoflake importmagic epc ptvsd

JavaScript & TypeScript

(use-package mocha-snippets :ensure t)
(use-package npm-mode :ensure t)

(setq javascript-indent-level 4
      js-indent-level 4
      js2-basic-offset 4)

(with-eval-after-load 'js (require 'dap-node))
(with-eval-after-load 'ts (require 'dap-node))

(with-eval-after-load "npm-mode" (npm-global-mode t))

Java

(use-package lsp-java)

(setq-default haris/java-lombok-jar-path nil)

(defun haris/modify-java-vmargs-in-lsp-java ()
  (when (and lsp-mode (eq major-mode 'java-mode))
    (add-to-list 'lsp-java-vmargs "-Xmx3G" t)
    (add-to-list 'lsp-java-vmargs "-Xms512m" t)
    (add-to-list 'lsp-java-vmargs  "-XX:+UseStringDeduplication" t)
    ;; Add Lombok to the lsp server
    (when haris/java-lombok-jar-path
      (add-to-list 'lsp-java-vmargs
                   (format "-javaagent:%s" haris/java-lombok-jar-path) t))))

(add-hook 'lsp-before-initialize-hook #'haris/modify-java-vmargs-in-lsp-java)

Web

(setq web-mode-markup-indent-offset 2
      web-mode-css-indent-offset 2
      web-mode-code-indent-offset 2
      css-indent-offset 2)

Vue

vue-language-server

Angular

@angular/language-service@next typescript @angular/language-server

Shells

(use-package company-shell)

(defun haris/enable--completions-in-shell-modes ()
  (setq-local company-backends haris/company-backends-sh-mode))

(add-hook 'sh-base-mode-hook #'haris/enable--completions-in-shell-modes)
(add-hook 'fish-mode-hook #'haris/enable--completions-in-shell-modes)

Fish

(straight-use-package
 '(company-fish
   :type git
   :host github
   :repo "CeleritasCelery/company-fish"))

(add-to-list 'spacemacs--indent-variable-alist '(fish-mode . fish-indent-offset))

RST

(defun haris/rst-heading () (interactive)
       (evil-execute-macro 1 "\"yyyp^v$"))

(define-key evil-normal-state-map (kbd ", H") 'haris/rst-heading)

Dockerfile

(setq dockerfile-use-buildkit t
      dockerfile-build-progress "plain")
dockerfile-language-server

Docker Compose

(add-to-list 'auto-mode-alist '("compose.*\.ya?ml$" . docker-compose-mode))

(lsp-register-client
 (make-lsp-client :new-connection (lsp-stdio-connection
                                   '("docker-compose-langserver" "--stdio"))
                  :activation-fn (lsp-activate-on "yaml")
                  :priority 0
                  :server-id 'compose-ls
                  :major-modes '(docker-compose-mode)))

Dependencies

docker-compose-langserver

SQL

(setq sql-capitalize-keywords t)

Protobuf

(use-package protobuf-mode :ensure t)
(use-package protobuf-ts-mode :ensure t)

Apps

Man

(setq Man-notify-method 'bully)

Bindings

(defun haris/man-search () (interactive)
       (swiper "^[[:space:]]+"))

(add-hook 'Man-mode-hook (lambda ()
                           (define-key Man-mode-map (kbd "SPC s ^") 'haris/man-search)))

Docker

Fix for empty image list (not sure if the fix works):

(use-package transient :defer t)
(use-package docker-compose-mode :defer t)

Go Translate

(use-package go-translate)
(setq gt-taker-prompt 'buffer)
(setq gt-langs '(en es de bs tr cs))
(setq gt-buffer-render-follow-p t)
(setq gt-polyglot-p t)
(setq gt-taker-pick nil)

(setq gt-default-translator
      (gt-translator
       :engines (gt-google-engine)
       :render (gt-buffer-render)))

Keybindings

;; Perform translation
(evil-define-key 'normal gt-buffer-prompt-map (kbd ",c") (kbd "C-c C-c"))
;; Abort translation
(evil-define-key 'normal gt-buffer-prompt-map (kbd ",k") (kbd "C-c C-k"))

;; Switch translator engine
(evil-define-key 'normal gt-buffer-prompt-map (kbd ",e") (kbd "C-c C-e"))
;; Next language
(haris/evil-define-key-both gt-buffer-prompt-map (kbd "M-n") (kbd "C-c C-n"))
;; Previous language
(haris/evil-define-key-both gt-buffer-prompt-map (kbd "M-p") (kbd "C-c C-p"))

;; go-translate provides only a function for next, but not for prev
(defun haris/gt-buffer-render--cycle-prev (&optional ignore-rules)
  (interactive "P")
  (with-slots (target taker keep) gt-buffer-render-translator
    (if (and (slot-boundp taker 'langs) (gt-functionp (oref taker langs)))
        (user-error "Current taker not support cycle prev")
      (let* ((curr gt-last-target)
             (gt-skip-lang-rules-p ignore-rules)
             (gt-ignore-target-history-p t)
             (prev (gt-target taker gt-buffer-render-translator 'prev)))
        (unless (equal prev curr)
          (setf target prev keep t)
          (gt-start gt-buffer-render-translator))))))

(add-hook
 'gt-buffer-render-output-hook
 (defun haris//gt-buffer-render-output-init-bindings () (interactive)
        ;; Without this, the following keybindings don't take effect
        (evil-normal-state)
        (evil-define-key 'normal gt-buffer-render-local-map
          (kbd "M-n") 'gt-buffer-render--cycle-next)
        (evil-define-key 'normal gt-buffer-render-local-map
          (kbd "M-p") 'haris/gt-buffer-render--cycle-prev)
        (evil-define-key 'normal gt-buffer-render-local-map
          (kbd "q") 'haris/force-kill-window)
        (evil-define-key 'normal gt-buffer-render-local-map
          (kbd "?") (defun haris/gt-buffer-render-help ()
                      (interactive)
                      (describe-keymap gt-buffer-render-local-map)))))

Bluetooth

(use-package bluetooth :defer t)

Email

I use mu4e as my email client.
;; This is set to 't' to avoid mail syncing issues when using mbsync
(setq mu4e-change-filenames-when-moving t)

;; Refresh mail using isync every M minutes
(setq mu4e-update-interval (let ((M 4)) (* M 60)))
(setq mu4e-get-mail-command "mbsync -a")
(setq mu4e-enable-async-operations t)

;; Configure contexts
(setq mu4e-contexts
      `(
        ,(make-mu4e-context
          :name "[email protected]"
          :match-func (lambda (msg) (when msg (mu4e-message-contact-field-matches msg :to "[email protected]")))
          :enter-func (lambda () (message "Entering context: [email protected]"))
          :vars '((user-mail-address . "[email protected]")
                  (user-full-name . "Haris Gusic")
                  (mu4e-drafts-folder .     "/gmail/hgusic.pub/[Gmail]/Drafts")
                  (mu4e-sent-folder   .     "/gmail/hgusic.pub/[Gmail]/Sent Mail")
                  (mu4e-refile-folder .     "/gmail/hgusic.pub/[Gmail]/All Mail")
                  (mu4e-trash-folder  .     "/gmail/hgusic.pub/[Gmail]/Trash")
                  (
                   mu4e-maildir-shortcuts
                   . (("/gmail/hgusic.pub/Inbox"             . ?i)
                      ("/gmail/hgusic.pub/[Gmail]/Sent Mail" . ?s)
                      ("/gmail/hgusic.pub/[Gmail]/Trash"     . ?t)
                      ("/gmail/hgusic.pub/[Gmail]/Drafts"    . ?d)
                      ("/gmail/hgusic.pub/[Gmail]/All Mail" . ?a)))))

        ,(make-mu4e-context
          :name "[email protected]"
          :match-func (lambda (msg) (when msg (mu4e-message-contact-field-matches msg :to "[email protected]")))
          :enter-func (lambda () (message "Entering context: [email protected]"))
          :vars '((user-mail-address . "[email protected]")
                  (user-full-name . "Haris Gusic")
                  (mu4e-drafts-folder .     "/gmail/harisgusic.dev/[Gmail]/Drafts")
                  (mu4e-sent-folder   .     "/gmail/harisgusic.dev/[Gmail]/Sent Mail")
                  (mu4e-refile-folder .     "/gmail/harisgusic.dev/[Gmail]/All Mail")
                  (mu4e-trash-folder  .     "/gmail/harisgusic.dev/[Gmail]/Trash")
                  (
                   mu4e-maildir-shortcuts
                   . (("/gmail/harisgusic.dev/Inbox"             . ?i)
                      ("/gmail/harisgusic.dev/[Gmail]/Sent Mail" . ?s)
                      ("/gmail/harisgusic.dev/[Gmail]/Trash"     . ?t)
                      ("/gmail/harisgusic.dev/[Gmail]/Drafts"    . ?d)
                      ("/gmail/harisgusic.dev/[Gmail]/All Mail" . ?a)))))
        ))

(setq mu4e-context-policy         'ask
      mu4e-compose-context-policy 'ask)

(setq mu4e-org-support t)
;; Enable org mode when composing messages
(setq mu4e-org-compose-support t)

Sending messages

;; Show completion for From and To headers
(setq mail-user-agent 'mu4e-user-agent)
(setq message-mail-alias-type 'ecomplete)

(add-hook 'message-setup-hook 'flyspell-mode)

Notifications

(use-package mu4e-alert :defer t)
(setq mu4e-enable-notifications t)
(mu4e-alert-set-default-style 'libnotify)

Slack

slack-register-team automatically connects to slack. If I add it to the slack-mode-hook hook, it never connects. Investigate

;; (add-hook
;;  'slack-mode-hook
;;  (lambda ()
;;    ;; Add slack teams here
;;    (slack-register-team
;;     :name "efektivnialtruismus"
;;     :token (auth-source-pick-first-password
;;             :host "efektivnialtruismus.slack.com"
;;             :user "[email protected]")
;;     :cookie (auth-source-pick-first-password
;;              :host "efektivnialtruismus.slack.com"
;;              :user "[email protected]^cookie")
;;     :subscribed-channels '((main-announcements
;;                             main-community-events
;;                             main-opportunities
;;                             main-random
;;                             project-eahouse)))))

ERC

(use-package erc)
(setq erc-server "irc.libera.chat"
      erc-nick "veracioux"
      erc-user-full-name "Haris Gušić"
      erc-track-shorten-start 8
      erc-autojoin-channels-alist '(("irc.libera.chat" "#archlinux" "#Jobs" "#fossjobs"))
      erc-kill-buffer-on-part t
      erc-auto-query 'bury)

(add-hook 'erc-join-hook (lambda () (evil-normal-state)))

;; For some reason erc-modules is undefined
(add-to-list 'erc-modules 'notifications)
(delete 'readonly erc-modules)
(erc-services-mode 1)
(erc-update-modules)

(erc-notify-mode t)
(erc-notifications-mode t)

Keybindings

(defun haris/erc-quit-channel () (interactive)
       (erc-part-from-channel ""))
(defun haris/euirc () (interactive)
       (erc :server "irc.euirc.net" :port 6667 :nick "veracioux"))
(defun haris/erc-list-channels () (interactive)
       (erc-with-server-buffer
        (erc-kill-input)
        (insert "/list")
        (erc-send-current-line)))

(define-key               erc-mode-map    (kbd "C-l") 'comint-clear-buffer)
(evil-define-key  'normal erc-mode-map    (kbd ",b")  'erc-switch-to-buffer)
(evil-define-key  'normal erc-mode-map    (kbd ",j")  'erc-join-channel)
(evil-define-key  'normal erc-mode-map    (kbd ",q")  'haris/erc-quit-channel)
(evil-define-key  'normal erc-mode-map    (kbd ",l")  'haris/erc-list-channels)

(evil-define-key  'motion erc-list-menu-mode-map  (kbd "RET")   nil)
(evil-define-key  'normal erc-list-menu-mode-map  (kbd "RET")   nil)
;; TODO shadowed by evil binding, don't know how to fix
;; (evil-define-key  'normal erc-list-menu-mode-map  (kbd ",j")   'erc-list-join)

Theme tweak

(setq erc-track-faces-priority-list
      '(erc-error-face
        erc-notice-face
        (erc-nick-default-face erc-current-nick-face)
        erc-current-nick-face erc-keyword-face
        (erc-nick-default-face erc-pal-face)
        erc-pal-face erc-nick-msg-face erc-direct-msg-face
        (erc-button erc-default-face)
        (erc-nick-default-face erc-dangerous-host-face)
        erc-dangerous-host-face erc-nick-default-face
        (erc-nick-default-face erc-default-face)
        erc-default-face erc-action-face
        (erc-nick-default-face erc-fool-face)
        erc-fool-face erc-input-face erc-prompt-face))

Systemd service

[Unit]
Description=Emacs daemon for IRC chat
Documentation=info:emacs man:emacs(1) https://gnu.org/software/emacs/

[Service]
Type=forking
Environment=COLORTERM=truecolor
ExecStart=/usr/bin/emacs --daemon="irc"
Restart=always
TimeoutStartSec=600
TimeoutStopSec=30
StartLimitBurst=0

[Install]
WantedBy=default.target

EDBI

Database viewer in Emacs.

(use-package edbi
  :defer t
  :config (progn
            (define-key edbi:dbview-keymap (kbd "SPC") nil)
            (define-key edbi:dbview-keymap (kbd "RET")
              'edbi:dbview-show-tabledef-command)))

Dependencies

RPC::EPC::Service DBI
# For postgres support
DBD::Pg

Octave

(defun octave-write-and-source () (interactive)
       (write-file (buffer-file-name))
       (octave-source-file (buffer-file-name)))

(evil-define-key 'normal octave-mode-map
  (kbd ",ss") 'octave-write-and-source)
(evil-define-key 'normal inferior-octave-mode-map
  (kbd ",hh") 'octave-help)

Reddit

md4rd

(add-hook 'md4rd-mode-hook 'md4rd-indent-all-the-lines)
(setq md4rd-subs-active '(linuxquestions+linux+opensource plc))

reddigg

This is a very elegant reader for reddit that uses org-mode.

(defun reddit-view-linux () (interactive)
       (reddigg-view-sub "linux+linuxquestions+opensource"))
(defun reddit-view-elec () (interactive)
       (reddigg-view-sub "plc+ElectricalEngineering+embedded"))

Maxima

I used this mode like 2-3 times, but I’m keeping it in case I have to use it again.

(add-to-list 'load-path "/usr/share/emacs/site-lisp/maxima/")
(autoload 'maxima-mode "maxima" "Maxima mode" t)
(autoload 'imaxima "imaxima" "Frontend for maxima with Image support" t)
(autoload 'maxima "maxima" "Maxima interaction" t)
(autoload 'imath-mode "imath" "Imath mode for math formula input" t)
(setq imaxima-use-maxima-mode-flag t)
(add-to-list 'auto-mode-alist '("\\.ma[cx]\\'" . maxima-mode))

Minibuffer

(define-minor-mode haris/minibuffer-elisp-mode
  "Custom mode that allows me to use emacs-lisp-mode features in minibuffers
that evaluate elisp code.")

(add-hook 'minibuffer-setup-hook
          (defun haris//minibuffer-maybe-enable-elisp-mode ()
            (when (member this-command
                          '(eval-expression
                            edebug-eval-expression))
              (haris/minibuffer-elisp-mode 1))))

(add-hook
 'minibuffer-mode-hook
 (defun haris//enable-company-in-non-completing-minibuffers ()
   "Enable company-mode in buffers which don't already have some kind of
inherent completion mechanism."
   (when (and (not minibuffer-completion-predicate)
              (not minibuffer-completion-table))
     (company-mode))))

Elisp evaluation

(add-hook 'haris/minibuffer-elisp-mode-hook
          (defun haris//initialize-minibuffer-elisp-mode ()
            "Initialize custom haris/minibuffer-elisp-mode"
            (haris/yas-minor-mode-on)
            (yas-activate-extra-mode 'emacs-lisp-mode)
                                        ; (ref:company-backends-emacs-lisp-mode/usage)
            (setq-local company-backends company-backends-emacs-lisp-mode)
            (company-mode)))

(advice-add
 #'eval-expression
 :around
 (defun haris/eval-expression/around (func &rest args)
   "Overrides eval-expression to use active region is its initial input"
   (interactive
    (cons
     (let ((minibuffer-setup-hook minibuffer-setup-hook))
       ;; If a region was selected, open the read--expression minibuffer in normal mode
       (if (use-region-p)
           (add-hook
            'minibuffer-setup-hook
            (defun haris//eval-expression-normal-state ()
              ;; Only do it for the eval-expression minibuffer, otherwise
              ;; stacked minibuffers will misbehave
              (if (eq this-command 'eval-expression)
                  (evil-force-normal-state)))
            99))

       (read--expression
        "Eval: "
        (when (use-region-p)
          (buffer-substring (region-beginning) (region-end)))))

     (eval-expression-get-print-arguments current-prefix-arg)))

   (apply func args)))

Bindings

Note: I override the <escape> binding for company-active-map for this mode specifically. I did that here, because I don’t know a better way to do it.

(define-key minibuffer-mode-map (kbd "M-P") 'counsel-minibuffer-history)

Ivy

(use-package all-the-icons-ivy-rich
  :ensure t
  :config
  (all-the-icons-ivy-rich-mode 1))

(use-package ivy-rich
  :ensure t
  :config
  (setq ivy-rich-display-transformers-list
        (append
         ivy-rich-display-transformers-list
         `(helpful-symbol
           (:columns
            ((,(defun haris/helpful-symbol-transformer (symbol)
                 (or (counsel-describe-variable-transformer symbol)
                     (counsel-describe-function-transformer symbol)))
              (:width 0.3))
             (all-the-icons-ivy-rich-symbol-class
              (:width 8 :face all-the-icons-ivy-rich-type-face))
             (all-the-icons-ivy-rich-symbol-docstring
              (:face all-the-icons-ivy-rich-doc-face)))))
         `(helpful-callable
           ,(nth
             (+ 1 (cl-position 'counsel-describe-function
                               ivy-rich-display-transformers-list))
             ivy-rich-display-transformers-list))
         `(helpful-variable
           ,(nth
             (+ 1 (cl-position 'counsel-describe-variable
                               ivy-rich-display-transformers-list))
             ivy-rich-display-transformers-list))))
  (ivy-rich-mode 1))


(setq ivy-initial-inputs-alist
      (append
       '((helpful-function . "^")
         (helpful-callable . "^")
         (helpful-variable . "^")
         (helpful-symbol . "^")
         (devdocs-browser-open . "^")
         ivy-initial-inputs-alist)))

(add-hook 'lsp-mode-hook
          (lambda ()
            (setq xref-show-xrefs-function 'ivy-xref-show-xrefs
                  xref-show-definitions-function 'ivy-xref-show-defs)))

(defun haris/ivy-insert-current ()
  "Exactly the same as ivy-insert-current, but made interactive."
  (interactive)
  (ivy-insert-current))


(define-key ivy-minibuffer-map (kbd "<backtab>") 'haris/ivy-insert-current)
(evil-define-key 'normal ivy-minibuffer-map (kbd "C-n") 'ivy-next-line)
(evil-define-key 'normal ivy-minibuffer-map (kbd "C-p") 'ivy-previous-line)
(evil-define-key 'normal ivy-minibuffer-map (kbd "gg")  'ivy-beginning-of-buffer)
(evil-define-key 'normal ivy-minibuffer-map (kbd "G")   'ivy-end-of-buffer)
(evil-define-key 'normal ivy-minibuffer-map (kbd "C-j") 'ivy-next-line)

(define-key ivy-minibuffer-map (kbd "C-<return>") 'ivy-immediate-done)
(define-key ivy-minibuffer-map (kbd "M-P") 'ivy-reverse-i-search)

Imenu

(setq imenu-max-item-length nil)

Org mode

Packages

(eval-after-load "org"
  (lambda ()
    (use-package org-transclusion :defer t)
    (use-package org-preview-html :defer t)
    (use-package org-drill        :defer t)
    (use-package ob-restclient)
    (use-package ol-man :ensure nil)
    (use-package org-tempo :ensure nil)
    (use-package org-modern
      :config (setq org-modern-star nil))
    (use-package company-org-block)))

Basic config

(defun haris/org-mode-visual-fill ()
  (setq visual-fill-column-width        90
        visual-fill-column-center-text  t)
  (visual-fill-column-mode 1))

(defun haris/org-babel-goto-tangle-file ()
  "Go to the file that the code block at point tangles to. If there is an
interactive prefix argument, open the final destination (production) file."
  (let ((file (if current-prefix-arg
                  (haris/extract-tangle-final-dest)
                (haris/extract-tangle-dest))))
    (when file (find-file file))))

(add-hook 'org-mode-hook 'org-appear-mode)
(add-hook 'org-mode-hook 'haris/org-mode-visual-fill)
(add-hook 'org-mode-hook 'org-indent-mode)
(add-hook 'org-mode-hook 'org-transclusion-add-all)
(add-hook 'org-open-at-point-functions 'haris/org-babel-goto-tangle-file)

;; Prettiness
(setq org-indent-mode                     t
      org-M-RET-may-split-line            nil
      org-ellipsis                        ""
      org-superstar-headline-bullets-list '("" "" "" "")
      org-hide-emphasis-markers           t
      org-pretty-entities                 t
      org-appear-autoentities             t
      org-appear-autolinks                nil)

;; Misc variables
(setq org-download-screenshot-method      "flameshot gui --path screenshots/%s"
      org-projectile-file                 "TODO.org"
      org-projectile-per-project-filepath "TODO.org")

(add-to-list 'org-file-apps '("\\.x?html?\\'" . "firefox %s"))
(add-to-list 'org-export-backends 'md)

;; Enable org-modern-mode
(with-eval-after-load 'org (global-org-modern-mode))

Agenda

(setq org-agenda-files (append
                        (remove "~/wiki/index.org"
                                (file-expand-wildcards "~/wiki/*"))
                        (file-expand-wildcards "~/proj/*/*.org")
                        (file-expand-wildcards "~/proj/drytoe/*/*.org")))

Syntax extensions

Doesn’t get loaded correctly.

(use-package org-special-block-extras
  :ensure t
  :hook (org-mode . org-special-block-extras-mode))

org-alert

Takes too long to load.

(use-package org-alert :defer t)

Block templates

(setq org-structure-template-alist
      (cl-remove-duplicates
       (append (default-value 'org-structure-template-alist)
               '(("el"        . "src elisp")
                 ("sh"        . "src shell")
                 ("py"        . "src python")
                 ("bash"      . "src bash")
                 ("fish"      . "src fish")
                 ("fishfun"   . "src fish :tangle (haris/tangle-deps \".config/fish/functions/TODO\")")
                 ("fishcomp"  . "src fish :tangle (haris/tangle-deps \".config/fish/completions/TODO\")")
                 ("dep"       . "src shell :tangle (haris/tangle-deps \"TODO\")")
                 ("sht"       . "src shell :tangle (haris/tangle-home \"TODO\")")
                 ("elt"       . "src elisp :tangle (haris/tangle-home \"TODO\")")
                 ("sh"        . "src elisp :tangle (haris/tangle-home \"TODO\")")
                 ("st"        . "src :tangle (haris/tangle-home \"TODO\")")
                 ("rest"      . "src restclient")))
       :test (lambda (a b) (string= (car a) (car b)))))

Babel

I tangle my configs from various org files into their respective destination files. But, sometimes I perform a tangle without wanting to overwrite my live configuration. One reason for this is that I have a (WIP) github workflow that I use to generate the configs from my org files. That is why code blocks in my literal configs use temporary “staging” destinations. So, whenever I run (org-babel-tangle), the files are output into /tmp/tangle-<username> or /tmp/dependencies-<username> (varies by code block). Then, if I want to apply those files to my live config under ~/, I can call (haris/tangle-dest).

;; (use-package ob-async :defer t)

;; There are a few custom functions I define for tangling that are in a separate
;; file, so that file can be used as a minimalistic source for boostrapping.
(load-file "~/.haris/bootstrap/tangle.el")

(add-to-list 'org-babel-load-languages '(restclient . t))
;; (add-to-list 'org-babel-load-languages '(async      . t))
(add-to-list 'org-babel-load-languages '(verb       . t))
(add-to-list 'org-babel-load-languages '(sql        . t))
(org-babel-do-load-languages 'org-babel-load-languages org-babel-load-languages)

;; This variable is by default unbound, and so causes an error whenever a code
;; block with ':session' is evaluated
(setq org-babel-prompt-command "")

(defun haris/tangle-dest (&optional prefix-arg)
  "Tangle block(s) to their final destinations. If a code block has the
temporary staging destination as their :tangle argument, it will be tangled to
the production destination under ~/ as well."
  (interactive "P")
  (let ((tangle-home (haris/tangle-home)))
    (delete-directory tangle-home t)
    (org-transclusion-add-all)
    (org-babel-tangle prefix-arg)
    (shell-command (concat "rsync -ru --keep-dirlinks " tangle-home " ~/"))))

(evil-define-key 'normal org-mode-map (kbd ",bT") 'haris/tangle-dest)

org-src buffer

(add-hook 'org-src-mode-hook
          (defun haris//org-src-inherit-allow-copilot ()
            (setq-local haris/allow-copilot
                        (org-src-do-at-code-block haris/allow-copilot))
            (when haris/allow-copilot
              ;; NOTE: Doesn't work without the timer for some reason
              (run-with-timer 0.1 nil 'copilot-mode))))

Make org-src buffer support lsp-mode

(add-hook
 'org-src-mode-hook
 (defun haris/org-src-mode-prepare-for-lsp-mode ()
   "Make the current org-src buffer support lsp-mode.
1. Create a temporary file and associate it the current org-src-mode buffer to
it by setting 'buffer-file-name'.
2. Set the projectile-project-root to be the same as the source org buffer
3. Cleanup of the temporary file is also scheduled for when the buffer is killed."
   (let* ((tmpdir (make-temp-file "" t))
          ;; Naming the file " " is a hack that prevents an ugly
          ;; "<name>Edit, then exit with ..." in the org-src buffer's header
          (filename (format "%s/ " tmpdir)))
     ;; Create a temporary association of the file with the proper major-mode
     (setq-local buffer-file-name filename)
     (write-region nil nil buffer-file-name)
     ;; Set project root
     (setq-local projectile-project-root
           (org-src-do-at-code-block (projectile-project-root)))
     (setq lsp-auto-guess-root t)
     ;; Clean up
     (add-hook
      'kill-buffer-hook
      (lambda ()
        ;; Delete temporary file
        (delete-file buffer-file-name))
      99
      t)))
 90)

(advice-add
 'org-edit-src-save
 :before
 (lambda (&rest rest)
   (when buffer-file-name
     (write-region nil nil buffer-file-name))))

Utility functions

(defun haris/extract-tangle-dest ()
  "Extract the tangle destination from the code block under point."
  (let* ((args (nth 2 (org-babel-get-src-block-info)))
         (tangle-arg (alist-get :tangle args)))
    (if (and tangle-arg (not (string= "no" tangle-arg)))
        tangle-arg)))

(defun haris/extract-tangle-final-dest ()
  "Extract the tangle destination of the current code block. If the destination
is defined in terms of (haris/tangle-home), then the final destination under
~/ is returned."
  (let* ((dest (haris/extract-tangle-dest)))
    (if dest
        (let* ((home-dir-re (concat "^" (regexp-quote (haris/tangle-home))))
               (deps-dir-re (concat "^" (regexp-quote (haris/tangle-deps ""))))
               (_file (replace-regexp-in-string home-dir-re "~/" dest)))
          (replace-regexp-in-string deps-dir-re "~/" _file))
      nil)))

LaTeX preview

(setq org-preview-latex-default-process        'dvisvgm)
(setq org-latex-create-formula-image-program   'dvisvgm)
(setq org-preview-latex-image-directory        "/tmp/org-mode/ltximg/")
(setq org-image-actual-width 400)

Verb

(add-hook 'verb-response-body-mode-hook 'verb-toggle-show-headers)

(setq verb-json-use-mode 'json-ts-mode)

(spacemacs/set-leader-keys-for-minor-mode
  'verb-response-body-mode
  "rs" #'verb-show-request)

Restclient

(defun haris/org-babel-restclient-split-window-fix ()
  "Fixes a bug where executing a restclient code block splits the window."
  (interactive)
  (if (string=
       (car (org-babel-get-src-block-info))
       "restclient")
      (delete-window)))

(add-hook 'org-babel-after-execute-hook 'haris/org-babel-restclient-split-window-fix)

Bindings

;; Make org-cycle work only in evil normal state, so it doesn't interfere with
;; completion etc.
(define-key org-mode-map (kbd "TAB") nil t)
(evil-define-key 'normal org-mode-map (kbd "TAB")         'org-cycle)

(evil-define-key 'normal org-mode-map (kbd ", S")         'org-attach-screenshot)
(evil-define-key 'normal org-mode-map (kbd ", TAB")       'org-next-link)
(evil-define-key 'normal org-mode-map (kbd ", <backtab>") 'org-previous-link)
(evil-define-key 'normal org-mode-map (kbd ", i c")       'org-columns)
(evil-define-key 'normal org-mode-map (kbd ", b E")       'haris/execute-named-code-block)
(evil-define-key 'normal org-mode-map (kbd ", R")         'org-mode-restart)

(evil-define-key 'normal org-mode-map (kbd "SPC h o")     'org-info-find-node)

;; The , prefix is implied
(define-key spacemacs-org-src-mode-map (kbd "w")
            'org-edit-src-save)
(define-key spacemacs-org-src-mode-map (kbd "bt")
            'haris/org-src-tangle)
(define-key spacemacs-org-src-mode-map (kbd "bT")
            'haris/org-src-tangle-dest)

(spacemacs/set-leader-keys-for-minor-mode 'org-mode "l" #'lsp-org)
(spacemacs/set-leader-keys-for-minor-mode 'org-src-mode "l" #'lsp)

Helper functions

(defun haris/execute-named-code-block ()
  "Execute a named code block from the current buffer, interactively prompting
   the user."
  (interactive)
  (save-excursion
    (call-interactively 'org-babel-goto-named-src-block)
    (org-babel-execute-src-block-maybe)))

(defun haris/org-src-tangle ()
  "Tangle the target code block while inside the org-src buffer"
  (interactive)
  (org-edit-src-save)
  (org-src-do-at-code-block
   ;; Prefix arg '(16) makes it tangle only the code blocks related to the same
   ;; file as the code block that is being edited in the current org-src buffer.
   (org-babel-tangle '(16))))

(defun haris/org-src-tangle-dest ()
  "Tangle the target code block while inside the org-src buffer"
  (interactive)
  (org-edit-src-save)
  (org-src-do-at-code-block
   ;; Prefix arg '(16) makes it tangle only the code blocks related to the same
   ;; file as the code block that is being edited in the current org-src buffer.
   (haris/tangle-dest '(16))))

Git

(use-package git-gutter :defer t)
;; Loading it eagerly helps with proper initialization of hooks.
;; Magit is essential to my workflow so having it eagerly loaded is no tradeoff.
(use-package magit)

(setq magit-display-buffer-function 'magit-display-buffer-same-window-except-diff-v1
      magit-diff-refine-hunk 'all
      magit-save-repository-buffers nil)

(setq magit-repository-directories
      '(("~/.haris" . 0)
        ("~/proj" . 1)
        ("~/proj/drytoe" . 1)
        ("~" . 0)))

(setq magit-generate-buffer-name-function
      (defun haris/magit-generate-buffer-name (mode &optional value)
        "Simple wrapper around `magit-generate-buffer-name-default-function'
      that always includes the full path to the repository."
        (let ((magit-buffer-name-format
               (string-replace "%t" (magit-toplevel)
                               magit-buffer-name-format)))
          (magit-generate-buffer-name-default-function mode value))))

;; Performance improvements
(remove-hook 'server-switch-hook 'magit-commit-diff)
(remove-hook 'with-editor-filter-visit-hook 'magit-commit-diff)
(remove-hook 'magit-status-sections-hook 'magit-insert-unpushed-to-upstream-or-recent)

Commit

(define-derived-mode haris/git-commit-major-mode
  text-mode
  "Git commit major mode (Custom)"
  "Custom major mode for editing git commit messages.")

(defun haris/git-commit/cd-to-worktree ()
  "Make sure that the default-directory in git-commit-mode is the worktree."
  (setq-local default-directory (magit-toplevel)))

(add-hook 'git-commit-mode-hook 'haris/git-commit/cd-to-worktree)
(setq git-commit-major-mode 'haris/git-commit-major-mode)

Custom transient commands

(defun haris/magit-fetch-to-local (remote branch args)
  "Fetch a remote branch to a local branch of the same name"
  (interactive
   (let ((remote (magit-read-remote-or-url "Fetch from remote or url")))
     (list remote
           (magit-read-remote-branch "Fetch branch" remote)
           (magit-fetch-arguments))))
  (magit-git-fetch remote (cons (concat branch ":" branch) args)))

(add-hook 'magit-status-mode-hook
          (lambda ()
             (transient-append-suffix
               'magit-fetch "o"
               '(1 "O" "another, to local" haris/magit-fetch-to-local))))

Bindings

(evil-define-key 'normal magit-section-mode-map (kbd "g h") #'magit-section-up)

GitHub

(setq auth-sources '(password-store "~/.authinfo.dev.gpg" "~/.netrc.gpg"))

AI

ChatGPT

(with-eval-after-load 'chatgpt
  (setq chatgpt-model "gpt-4-turbo-preview"))
(with-eval-after-load 'codegpt
  (setq codegpt-model "gpt-3.5-turbo-instruct"))
(setq chatgpt-window-prompt "")
(setq chatgpt-display-method
      (lambda (buffer-or-name)
        (pop-to-buffer buffer-or-name
                       `((display-buffer-in-direction)
                         (dedicated . t)))
        (spacemacs/toggle-maximize-buffer)
        (chatgpt-type-response)))
(add-hook 'chatgpt-input-mode-hook
          (defun haris//chatgpt-input-setup ()
            (call-interactively 'evil-insert-state)))

Keybindings

;; ChatGPT mode
(define-key chatgpt-mode-map (kbd "q") 'quit-window)
(define-key chatgpt-mode-map (kbd "RET")
            (defun haris//chatgpt-type-response ()
              (interactive)
              (let ((haris/chatgpt-session-buffer (current-buffer)))
                (chatgpt-type-response))))
;; CodeGPT mode
(haris/evil-define-key-both codegpt-mode-map (kbd "q") 'quit-window)

;; ChatGPT input mode
(evil-define-key 'normal chatgpt-input-mode-map (kbd "q") 'kill-this-buffer)
(evil-define-key 'visual chatgpt-input-mode-map (kbd "q") 'kill-this-buffer)

Copilot

(straight-use-package '(copilot
                        :type git
                        :host github
                        :repo "copilot-emacs/copilot.el"))
(setq copilot-indent-offset-warning-disable t)

(defvar haris/allow-copilot nil
  "Whether the copilot-mode function should be allowed to run. This is a
    security measure to prevent sharing of sensitive information in undesired
    contexts.")
(add-hook 'after-change-major-mode-hook
          (lambda ()
            (when haris/allow-copilot
              (copilot-mode))) 90)

(defun haris/allow-copilot (allow)
  "Allow copilot if ALLOW, disallow otherwise"
  (interactive (list (not haris/allow-copilot)))
  (setq-local haris/allow-copilot allow)
  (when (called-interactively-p 'any)
    (message "%s copilot in the current buffer" (if allow
                                                    "Allowed"
                                                  "Disallowed")))
  (unless allow
    (copilot-mode -1)))

Privacy enhancements

(with-eval-after-load 'copilot
  (advice-add
   'copilot-mode
   :around
   (defun haris//advice/copilot-mode (orig-func &rest args)
     "Checks if 'haris/allow-copilot' is true before running ORIG-FUNC."
     (let* ((arg (car args))
            (enabling (if (eq arg 'toggle) (not copilot-mode)
                        (or
                         (not (numberp arg))
                         (not (< arg 0))))))

       ;; If interactive and copilot is disallowed, prompt the user to allow it
       (if (and (not haris/allow-copilot)
                (called-interactively-p 'any))
           (when
               (yes-or-no-p
                "Copilot is disallowed. Do you want to allow it (buffer-locally)?")
             (setq-local haris/allow-copilot t)))

       ;; If copilot is allowed and enable was requested, enable it
       ;; If disable is requested, disable it unconditionally
       (if (or (not enabling)
               haris/allow-copilot)
           (progn
             (apply orig-func args)
             ;; Notify the user if interactive
             (when (called-interactively-p 'any)
               (message "%s Copilot in current buffer"
                        (if enabling
                            "Enabled"
                          "Disabled"))))
         (unless (called-interactively-p 'any)
           (message
            "Warning: Copilot is disallowed (haris/allow-copilot is nil in buffer %s)"
            (buffer-name))))))))

Test

(with-temp-buffer
  (let ((haris/allow-copilot t))
    ;; Should enable copilot-mode successfully
    (copilot-mode)
    (cl-assert copilot-mode)

    ;; Should disable copilot-mode successfully
    (setq haris/allow-copilot t)
    (copilot-mode -1)
    (cl-assert (not copilot-mode))

    ;; With copilot disallowed, attempt to enable should fail without error
    (setq haris/allow-copilot nil)
    (copilot-mode)
    (cl-assert (not copilot-mode))))

Keybindings

(with-eval-after-load 'copilot
  (define-key copilot-completion-map (kbd "<tab>") 'copilot-accept-completion)
  (define-key copilot-completion-map (kbd "TAB") 'copilot-accept-completion)
  (define-key copilot-completion-map (kbd "M-<backspace>") 'copilot-clear-overlay)
  (define-key copilot-completion-map (kbd "M-l") 'copilot-accept-completion-by-word)
  (define-key copilot-completion-map (kbd "M-j") 'copilot-accept-completion-by-line)
  (define-key copilot-completion-map (kbd "M-n") 'copilot-next-completion)
  (define-key copilot-completion-map (kbd "M-p") 'copilot-previous-completion)
  (define-key copilot-completion-map (kbd "C-<tab>") 'copilot-panel-complete)
  (spacemacs/set-leader-keys "oC" 'copilot-mode))

REPLs in Emacs

Common settings

(add-hook 'comint-mode-hook #'smartparens-mode)
(add-hook 'vterm-mode-hook #'smartparens-mode)
(add-hook 'eshell-mode-hook #'smartparens-mode)

Comint

(defun comint-clear-buffer-goto () (interactive)
       (comint-clear-buffer) (evil-goto-line))
(define-key comint-mode-map (kbd "C-l") 'comint-clear-buffer-goto)

(evil-define-key 'insert comint-mode-map (kbd "C-p") 'comint-previous-input)
(evil-define-key 'insert comint-mode-map (kbd "C-n") 'comint-next-input)

(evil-define-key 'insert comint-mode-map (kbd "C-k") 'comint-previous-prompt)
(evil-define-key 'insert comint-mode-map (kbd "C-j") 'comint-next-prompt)

Vterm

(setq vterm-max-scrollback 10000)

(setq vterm-exit-functions 'delete-frame)

(defun haris/vterm-set-environment-windowid (&rest _)
  (setq
   vterm-environment
   (list (format
          "WINDOWID=%s"
          (cdr (assoc 'window-id
                      (cadr (cadr
                             (current-frame-configuration)))))))))

;; Ideally, I would add haris/vterm-set-environment-windowid to a
;; vterm-before-shell-hook, if such a hook existed. But it doesn't, so...
(advice-add #'vterm                          :before #'haris/vterm-set-environment-windowid)
(advice-add #'multi-vterm                    :before #'haris/vterm-set-environment-windowid)
(advice-add #'spacemacs/shell-pop-vterm      :before #'haris/vterm-set-environment-windowid)
(advice-add #'spacemacs/shell-pop-multivterm :before #'haris/vterm-set-environment-windowid)

(defalias 'haris/vterm-toggle-freeze 'vterm-copy-mode
  "Freeze/unfreeze the output of the current vterm buffer")

Line wrapping workaround

Vterm doesn’t automatically re-wrap truncated lines when the terminal width increases. This seems to be a bug.

I wrap my shell in screen as a workaround. NOTE: If I set vterm-shell directly, it is overriden by the Spacemacs shell layer.

(setq shell-default-term-shell "screen fish")

Vterm daemon

I sometimes use Emacs as my terminal emulator. For that I need additional stability, so I run a separate daemon for that, which is unaffected by blockage and crashes of my default Emacs instance.

[Unit]
Description=Emacs daemon for Vterm

[Service]
Type=forking
Environment=COLORTERM=truecolor
ExecStart=/usr/bin/emacs --daemon="vterm"
Restart=always
TimeoutStartSec=600
TimeoutStopSec=30
StartLimitBurst=0

[Install]
WantedBy=default.target

Bindings

(defun haris/vterm-clear-keep-scrollback ()
  (interactive)
  (let ((vterm-clear-scrollback-when-clearing nil))
    (vterm-clear)))

(with-eval-after-load 'vterm
  (haris/evil-define-key-both vterm-mode-map (kbd "C-l") #'haris/vterm-clear-keep-scrollback)

  (evil-define-key 'normal vterm-mode-map      (kbd "0")     'evil-collection-vterm-first-non-blank)
  (evil-define-key 'normal vterm-mode-map      (kbd "A")     'evil-append-line)
  (evil-define-key 'normal vterm-mode-map      (kbd "M-TAB") 'other-window)
  (evil-define-key 'normal vterm-mode-map      (kbd ",f")    'haris/vterm-toggle-freeze)
  (evil-define-key 'normal vterm-copy-mode-map (kbd ",f")    'haris/vterm-toggle-freeze))

Dependencies

libvterm

EAF

Dependencies

git nodejs npm python-pyqt5 python-pyqt5-sip python-pyqtwebengine wmctrl python-pymupdf
python-epc

Completion

Note: Some variables are configured in Spacemacs layers.

Company

(use-package company)
(use-package company-statistics)

(global-company-mode)
(setq tab-always-indent t)
(setq company-minimum-prefix-length 1)
(setq company-tooltip-align-annotations t)
(setq completion-ignore-case t
      read-file-name-completion-ignore-case t
      read-buffer-completion-ignore-case t)
(setq company-tempo-expand t)
;; This should ideally be 0, but a non-zero value might help with performance
(setq company-idle-delay 0.05)
(setq company-posframe-quickhelp-delay nil)
(setq company-show-quick-access 'left)

(add-hook 'company-mode-hook #'company-statistics-mode)
(setq company-statistics-file (concat spacemacs-cache-directory
                                      "company-statistics-cache.el"))

;; Default backends for any new mode
(setq-default company-backends company-backends-text-mode)

;; Show popup even when the current text is the only candidate
(with-eval-after-load 'company
  (setq company-frontends (delq 'company-pseudo-tooltip-unless-just-one-frontend company-frontends))
  (add-to-list 'company-frontends 'company-pseudo-tooltip-frontend))

company-dabbrev

(setq company-dabbrev-char-regexp "\\sw\\(?:-\\sw\\)*")

Functions

(defun haris/company-disable-idle-popup ()
  "Disable company idle tooltip"
  (setq-local company-idle-delay nil))

(defun haris/company-enable-idle-popup ()
  "Enable company idle tooltip"
  (kill-local-variable 'company-idle-delay))

(defun haris/company-candidate-is-snippet-p (candidate)
  (equal (get-text-property 0 'company-backend candidate)
          'company-yasnippet))

(defun haris/company-sort-snippets-first (candidates)
  "Sort candidates using backend company-yasnippet before other ones."
  (sort candidates
        (lambda (c1 c2)
          (and
           (haris/company-candidate-is-snippet-p c1)
           (not
            (haris/company-candidate-is-snippet-p c2))))))

(defun haris/company-sort-prefix-first (candidates)
  "Sort CANDIDATES so that those starting with the typed prefix come first."
  (let ((prefix (company-grab-symbol)))
    (if prefix
        (let ((prefix-regex (concat "^" (regexp-quote prefix))))
          (sort candidates
                (lambda (c1 c2)
                  (and (string-match prefix-regex c1)
                       (not (string-match prefix-regex c2))))))
      candidates)))

(defun haris/company-toggle-tooltip ()
  (interactive)
  (cond
   ;; Tried company-tooltip-visible-p instead, but it doesn't work
   ((company--active-p) (company-cancel) t)
   ((company-manual-begin))))

(defun haris/company-sort-by-statistics-preserve-snippet-order (candidates)
  "Wraps company-sort-by-statistics but preserves the relative order of snippet completions."
  (let* ((old-score-calc company-statistics-score-calc)
         (company-statistics-score-calc
          (lambda (candidate)
            (if (haris/company-candidate-is-snippet-p candidate)
                99999999
              (funcall old-score-calc candidate)))))
    (company-sort-by-statistics candidates)))

(defun haris/init-company-backends-for-mode (mode company-backends)
  "Add a hook for MODE which initializes company backends for it.
The backends are taken from the variable haris/company-backends-MODE."
  (let ((mode-hook-symbol (intern (format "%s-hook" mode)))
        (init-func-symbol (intern
                           (format "haris/init-company-backends-for-%s" mode)))
        (backends-var-symbol (intern (format "haris/company-backends-%s" mode))))
    (eval `(setq ,backends-var-symbol company-backends))
    (defalias init-func-symbol
      `(lambda ()
         (setq-local company-backends ,backends-var-symbol))
      (format "Initialize company-backends for %s" mode))
    (add-hook mode-hook-symbol init-func-symbol 99)))

Backend configurations

Most of these have been createdu using the default ones set up by Spacemacs auto-completion as the starting value.

(setq haris/company-backends-default
      '((company-files :with company-yasnippet)
        (company-capf :with company-yasnippet)
        (company-semantic company-dabbrev-code company-keywords company-ispell :with company-yasnippet)
        (company-dabbrev :with company-yasnippet)))

(haris/init-company-backends-for-mode
 'sh-mode
 '(
   (company-files :with company-yasnippet)
   (;; This will only take effect in fish mode
    company-fish-shell
    company-shell-env
    company-shell
    company-capf
    :with company-yasnippet)
   (company-semantic
    company-dabbrev-code
    company-keywords
    company-ispell
    :with company-yasnippet)
   (company-dabbrev :with company-yasnippet)))

(remove-hook 'org-mode-hook #'spacemacs//init-company-backends-org-mode)
(haris/init-company-backends-for-mode
 'org-mode
 '((company-files :with company-yasnippet)
   (company-capf :with company-yasnippet)
   (company-org-block
    company-semantic
    company-dabbrev-code
    company-keywords
    company-ispell
    :with company-yasnippet)
   (company-dabbrev :with company-yasnippet)))

(haris/init-company-backends-for-mode
 'nginx-mode
 '((company-files :with company-yasnippet)
   (company-nginx
    company-semantic
    company-dabbrev-code
    company-keywords
    company-ispell
    :with company-yasnippet)
   (company-dabbrev :with company-yasnippet)))

(haris/init-company-backends-for-mode 'fundamental-mode haris/company-backends-default)
(haris/init-company-backends-for-mode 'text-mode haris/company-backends-default)
(haris/init-company-backends-for-mode 'yaml-mode haris/company-backends-default)
(haris/init-company-backends-for-mode 'yaml-ts-mode haris/company-backends-default)
(haris/init-company-backends-for-mode 'json-mode haris/company-backends-default)
(haris/init-company-backends-for-mode 'json-ts-mode haris/company-backends-default)
(haris/init-company-backends-for-mode 'java-mode haris/company-backends-default)
(haris/init-company-backends-for-mode 'java-ts-mode haris/company-backends-default)
(haris/init-company-backends-for-mode 'conf-mode haris/company-backends-default)
(haris/init-company-backends-for-mode 'python-mode haris/company-backends-default)
(haris/init-company-backends-for-mode 'inferior-python-mode haris/company-backends-default)
(haris/init-company-backends-for-mode 'sql-mode haris/company-backends-default)
(haris/init-company-backends-for-mode 'systemd-mode haris/company-backends-default)
(haris/init-company-backends-for-mode 'haris/git-commit-major-mode haris/company-backends-default)
(haris/init-company-backends-for-mode 'typescript-mode haris/company-backends-default)
(haris/init-company-backends-for-mode 'emacs-lisp-mode haris/company-backends-default)
(haris/init-company-backends-for-mode 'lisp-data-mode haris/company-backends-default)
(haris/init-company-backends-for-mode 'dockerfile-mode haris/company-backends-default)
(haris/init-company-backends-for-mode 'docker-compose-mode haris/company-backends-default)
(haris/init-company-backends-for-mode 'markdown-mode haris/company-backends-default)
(haris/init-company-backends-for-mode 'minibuffer-mode haris/company-backends-default)
(haris/init-company-backends-for-mode 'web-mode haris/company-backends-default)
(haris/init-company-backends-for-mode 'sgml-mode haris/company-backends-default)
(haris/init-company-backends-for-mode 'nxml-mode haris/company-backends-default)
(haris/init-company-backends-for-mode 'chatgpt-input-mode haris/company-backends-default)
(haris/init-company-backends-for-mode 'sql-interactive-mode haris/company-backends-default)
(haris/init-company-backends-for-mode 'nodejs-repl-mode haris/company-backends-default)

(haris/init-company-backends-for-mode
 'sql-interactive-mode
 '((company-files :with company-yasnippet)
   ;; TODO: company-edbi seems to be useless
   (company-edbi
    company-semantic
    company-dabbrev-code
    company-keywords
    company-ispell
    :with company-yasnippet)
   (company-dabbrev :with company-yasnippet)))

This variable is initialized too late, and we require it here. I wasn’t able to identify where exactly it is initialized by spacemacs, so I set it explicitly here.

(setq company-backends-emacs-lisp-mode haris/company-backends-default)

Behavior overrides

(setq company-transformers
      '(haris/company-sort-by-statistics-preserve-snippet-order
        company-sort-by-backend-importance
        haris/company-sort-snippets-first
        haris/company-sort-prefix-first))

(add-hook
 'company-statistics-mode-hook
 (lambda ()
   "Remove company-sort-by-statistics element that was added automatically by company-statistics"
   (setq company-transformers (delq 'company-sort-by-statistics company-transformers))))

(setq completion-styles '(basic partial-completion emacs22 initials substring flex))

Bindings

(with-eval-after-load 'company-posframe
  (define-key company-posframe-active-map (kbd "<f1>") nil)
  (define-key company-posframe-active-map (kbd "M-h")
              #'company-posframe-quickhelp-toggle))

;; Additional trigger for company
(define-key evil-insert-state-map (kbd "C-SPC") #'haris/company-toggle-tooltip)

The following overrides the escape key for company-active-map in eval-expression buffers. I know of no other way to make this binding take precedence over the other maps, so I define it here.

                                        ; (ref:company-active-map-escape-override)
(define-key
 company-active-map
 (kbd "<escape>")
 (defun haris/company-active-mode-escape-override ()
   "Override the <escape> key to close the company menu instead of exiting
to normal mode in minibuffers. This prevents accidental closing of the
minibuffer."
   (interactive)
   (if (eq major-mode 'minibuffer-mode)
       (haris/company-toggle-tooltip)
     (evil-force-normal-state))))

Workarounds

Posframe in minibuffer is making minibuffer lose focus

Disabled posframe in Spacemacs Layers.

Yasnippet

(use-package yasnippet)
(yas-reload-all)

(setq yas-alias-to-yas/prefix-p nil)

(add-hook 'git-commit-mode-hook
          (defun haris/git-commit-init-yasnippet ()
            (haris/yas-minor-mode-on)
            (yas-activate-extra-mode 'git-commit-mode))
          90)
(add-hook 'org-src-mode-hook
          (defun haris/org-src-init-yasnippet ()
            (haris/yas-minor-mode-on)
            (yas-activate-extra-mode 'org-src-mode))
          90)

(add-hook 'verb-mode-hook (lambda () (yas-activate-extra-mode 'verb-mode)))

(advice-add 'yas-tryout-snippet :after #'evil-insert-state)

;; Remove default bindings for next/prev field
(define-key yas-keymap (kbd "<tab>") nil)
(define-key yas-keymap (kbd "TAB") nil)
(define-key yas-keymap (kbd "<backtab>") nil)
(define-key yas-keymap (kbd "S-<tab>") nil)

(define-key yas-keymap (kbd "M-n") #'yas-next-field)
(define-key yas-keymap (kbd "M-p") #'yas-prev-field)

The TAB key

The TAB key is much abused. Keeping track of it in each minor mode separately is a nightmare. Therefore I unbind TAB in some keymaps so that it falls back to the TAB binding in the global keymap. This way, I can define the exact order of precedence of TAB-bound commands for all modes.

(defun haris/handle-TAB-in-insert-mode (&optional alert)
  "Handle the TAB key exactly the way I want to."
  (interactive)
  (setq alert command-log-mode)

  (let ((last-fun nil))
    (defun _ (fun &rest rest)
      "Call the function normally, and record it to the last-fun variable."
      (setq last-fun (append (list fun) rest))
      (apply fun rest))

    (cond
     ;; If in an org table, try org-cycle
     ((when (and (fboundp 'org-at-table-p) (org-at-table-p)) (or (_ 'org-cycle) t)))
     ;; If company-mode is on, complete
     ((when (and company-mode (company-tooltip-visible-p)) (_ 'company-complete-selection)))
     ;; If org-mode is on, try org-cycle
     ((when (eq major-mode 'org-mode)
        (cond ((_ 'org-cycle))
              ;; If there's nothing to cycle, go to next link
              ((_ 'org-next-link)))))
     ;; Default: indent
     ((when (eq major-mode 'minibuffer-mode) (_ 'minibuffer-complete)))
     ((_ 'indent-for-tab-command)))

    (when alert
      (alert (format "%s" last-fun)
             :title "Command bound to pressed key:"))))

(define-key evil-insert-state-map (kbd "TAB") #'haris/handle-TAB-in-insert-mode)
;; Add ivy-partial-or-done to ivy-minibuffer-map in insert mode so as to not be
;; overridden by the above.
(evil-define-key 'insert ivy-minibuffer-map (kbd "TAB") #'ivy-partial-or-done)

Info mode

Remove Info mode annoying keybindings.

(evil-define-key 'normal Info-mode-map (kbd "[")    'Info-prev)
(evil-define-key 'normal Info-mode-map (kbd "]")    'Info-next)
(evil-define-key 'normal Info-mode-map (kbd "C-p")  'Info-backward-node)
(evil-define-key 'normal Info-mode-map (kbd "C-n")  'Info-forward-node)

Miscellaneous

Ispell

(setq ispell-program-name "aspell")

Accent

(use-package accent)

(setq accent-diacritics
      '((a (á à â ä æ ã å ā))
        (c (č ć ç))
        (d (ď đ))
        (e (é ě è ê ë ē ė ę))
        (i (í î ï ī į ì))
        (l (ł))
        (n (ň ñ ń))
        (o (ô ö ò ó œ ø ō õ))
        (r (ř))
        (s (š ß ś))
        (t (ť))
        (u (ů ú û ü ù ū))
        (y (ý ÿ))
        (z (ž ź ż))
        ;; Capital
        (A (Á À Â Ä Æ Ã Å Ā))
        (C (Č Ć Ç))
        (D (Ď Đ))
        (E (É Ě È Ê Ë Ē Ė Ę))
        (I (Í Î Ï Ī Į Ì))
        (L (Ł))
        (N (Ň Ñ Ń))
        (O (Ô Ö Ò Ó Œ Ø Ō Õ))
        (R (Ř))
        (S (Š Ś))
        (T (Ť))
        (U (Ů Ú Û Ü Ù Ū))
        (Y (Ý Ÿ))
        (Z (Ž Ź Ż))
        ;; Non-alphabetic
        (< («))
        (> (»))))

(define-key evil-insert-state-map (kbd "M-S-<return>") 'accent-menu)
(define-key evil-normal-state-map (kbd "M-S-<return>")
            (defun haris/accent-menu-normal-mode ()
              (interactive)
              (let ((accent-position 'after))
                (accent-menu))))

Ibuffer

(define-key ibuffer-mode-map (kbd "j") 'evil-next-line)
(define-key ibuffer-mode-map (kbd "k") 'evil-previous-line)

Currency converter

(use-package currency-convert
  :defer t
  :init (lambda () (setq
                    currency-convert-exchangeratesapi-key
                    (string-trim (shell-command-to-string "pass show @apilayer/api-key")))))

Command log mode

(setq command-log-mode-auto-show nil)

;; TODO: sometimes you have to call this twice for the command-log buffer to appear
(defun haris/command-log ()
  (interactive)
  (let ((command-log-mode-auto-show t))
    (call-interactively #'command-log-mode)))

Nerd commenter

(add-hook 'octave-mode-hook
          (lambda ()
            (setq comment-start "% "
                  comment-end "")))
(define-key evil-normal-state-map (kbd "SPC c c") 'evilnc-copy-and-comment-lines)

Alert

(setq alert-default-style 'libnotify)

Daemons

(use-package daemons)
(evil-define-key 'normal daemons-mode-map (kbd "u") #'daemons-systemd-toggle-user)

Nginx

(use-package company-nginx)

Helpful

(add-hook 'helpful-mode-hook
          (lambda ()
            (setq evil-lookup-func 'helpful-at-point)))

atomic-chrome

(use-package atomic-chrome)
(setq atomic-chrome-buffer-open-style 'frame)

(atomic-chrome-start-server)

gitconfig-mode

(add-hook 'gitconfig-mode-hook
          (lambda () (setq-local tab-width 2)))

bookmark

(evil-collection-init 'bookmark)

nmcli-wifi

(straight-use-package '(nmcli-wifi :type git :host github :repo "luckysori/nmcli-wifi"))
(evil-define-key 'normal nmcli-wifi-mode-map (kbd "c") 'nmcli-wifi-connect)
(evil-define-key 'normal nmcli-wifi-mode-map (kbd "r") 'nmcli-wifi-refresh)

transient

(with-eval-after-load 'transient
  (setq transient-values-file "~/.emacs.d/transient/values.el"))

Documentation

(use-package tldr :defer t)
(use-package devdocs-browser)

(with-eval-after-load 'devdocs-browser
  (setq devdocs-browser-major-mode-docs-alist
        (append
         devdocs-browser-major-mode-docs-alist
         '((typescript-mode "TypeScript")
           (javascript-mode "JavaScript")
           (java-mode "OpenJDK")
           (org-mode "elisp")))))

Keybindings

(spacemacs/set-leader-keys "hbd" 'devdocs-browser-open)

Spacemacs

This prevents Spacemacs from asking me to install missing layers. This forces me to put layers directly in dotspacemacs-configuration-layers, and keeps me from forgetting to put the layers in the versioned config.

(setq dotspacemacs-ask-for-lazy-installation nil)

Custom config organization

Spacemacs provides the spacemacs/ediff-dotfile-and-template function (bound to SPC f e D) for updating the dotfile at ~/.spacemacs when Spacemacs is updated. But the dotfile also contains content dynamically inserted by Emacs, which I don’t want to version. That’s why I keep the crux of my configuration in a separate file: ~/.haris/.spacemacs. This file is loaded from ~/.spacemacs, and should contain the following (along with any content dynamically inserted by Emacs):

(load-file "~/.haris/.spacemacs")
;; Do not write anything past this comment. This is where Emacs will
;; auto-generate custom variable definitions.

In order to have the spacemacs/ediff-dotfile-and-template function use my custom file, I advise it with a custom function:

(advice-add
 'spacemacs/ediff-dotfile-and-template
 :around
 (defun haris//advice/ediff-dotfile-and-template (oldfun &rest ignored)
   "Use a different file as the diff target instead of ~/.spacemacs."
   (let ((dotspacemacs-filepath (expand-file-name "~/.haris/.spacemacs")))
     (call-interactively oldfun))))

The remaining code blocks in this section are tangled to ~/.spacemacs-init.el. This file is in turn loaded from ~/.spacemacs.

Layers

(setq-default
 dotspacemacs-configuration-layers ; (ref:dotspacemacs-configuration-layers)
 '(syntax-checking
   multiple-cursors
   octave
   markdown
   html
   spacemacs-language
   spacemacs-navigation
   spacemacs-project
   (spacemacs-editing
    :variables
    vim-style-enable-undo-region t)
   spacemacs-editing-visual
   spacemacs-org
   search-engine
   spell-checking
   major-modes
   helpful
   ivy
   imenu-list
   (lsp
    :variables
    lsp-headerline-breadcrumb-enable t
    lsp-ui-sideline-show-symbol t
    lsp-lens-enable t)
   (c-c++ :variables c-c++-backend 'lsp-clangd c-c++-enable-clang-support t)
   (cmake :variables cmake-backend 'lsp cmake-enable-cmake-ide-support t)
   (python :variables python-formatter 'black python-backend 'lsp)
   dap
   vagrant
   ;; elpy
   ;; pythonp
   ipython-notebook
   emacs-lisp
   shell
   (shell-scripts :variables shell-scripts-backend 'lsp)
   javascript
   typescript
   java
   kotlin
   go
   prettier
   vue
   (yaml :variables yaml-enable-lsp t)
   csv
   rust
   (docker :variables docker-dockerfile-backend 'lsp)
   vagrant
   translate
   git
   lua
   (org :variables
        org-enable-appear-support t
        org-enable-transclusion-support t
        org-enable-verb-support t)
   restclient
   slack
   ;; mu4e
   pass
   sql
   nginx
   systemd
   tmux
   ;; eaf
   ;; emms
   debug
   (auto-completion ; (ref:auto-completion)
    :variables
    auto-completion-enable-snippets-in-popup t
    auto-completion-use-company-posframe nil ; (ref:disable-posframe)
    auto-completion-return-key-behavior nil
    auto-completion-tab-key-behavior 'complete
    auto-completion-enable-help-tooltip 'manual
    auto-completion-enable-sort-by-usage nil ;; I configure this manually
    )
   openai))

Additional packages

Packages installed with use-package should be added here as well. Otherwise Spacemacs would delete them every time on startup.

(setq-default
 dotspacemacs-additional-packages
 '(
   org-fragtog
   org-drill
   org-ref
   org-attach-screenshot
   org-special-blocks
   ob-ipython
   yasnippet-snippets
   vterm
   rainbow-mode
   evil-easymotion
   reddigg
   md4rd
   pydoc
   pylint
   python-info
   nodejs-repl
   command-log-mode
   org-preview-html
   vimrc-mode
   systemd
   evil-quickscope
   edbi
   counsel-jq
   sxhkdrc-mode
   bluetooth
   git-gutter
   json-mode
   fish-mode
   currency-convert
   i3wm-config-mode
   docker-compose-mode
   react
   focus-autosave-mode
   xclip
   accent
   sqlite3
   company-shell
   company-statistics))

Upgrading

When upgrading Spacemacs, run spacemacs/ediff-dotfile-and-template to merge any upstream changes to the dotfile.

Appendix

Keys

(condition-case err
    (setq openai-key
          (replace-regexp-in-string
           "\n$" ""
           (with-temp-buffer
             (insert-file-contents "~/.local/share/haris/openai-api-key.txt")
             (buffer-string))))
  (error (message "WARNING: %s" err)))

Config check

I use this variable to check if the config loaded correctly.

(setq haris/config-loaded-fine (current-time-string))

Emacs startup Notification

(when (daemonp)
  (when (not (boundp 'haris//daemon-ready-notified))
    (alert (format "Daemon '%s' ready" server-name)
           :title "Emacs Daemon")
    (setq haris//daemon-ready-notified t)))

Private config

The private config is loaded from ~/.emacs.d/haris/private.el.

(let ((file "~/.emacs.d/haris/private.el"))
  (when (file-exists-p file) (load-file file)))

Local variables