Skip to content
Permalink
b2d59b4a78
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
 
 
Cannot retrieve contributors at this time

Emacs Configuration

This is my Emacs configuration.

Table of contents

Prerequisites

  • Linux or Window Subsystem for Linux
  • Nix package manager for installing dependencies

Installation

This configuration depends on some external programs as well as Emacs Lisp packages built by Nix.

At present, a suggested installation procedure is to first install my home.nix and then run mr checkout at your home directory. In the future, it may support an alternative for trying out.

License

GPL v3.

Some libraries were originally written by other people, and they follow their respective licenses.

Initialization

(when (version< emacs-version "26.1")
  (error "Use GNU Emacs version 26.1 or later"))

Expand the GC threshold until gcmh-mode is activated. gcmh-mode updates this value later, so you don’t have to reset it. The value is stolen from http://akrl.sdf.org/

(setq gc-cons-threshold #x40000000)

(unless (fboundp 'whitespace-cleanup-mode)
  (defun whitespace-cleanup-mode (&rest args)
    (when (require 'whitespace-cleanup-mode nil t)
      (apply #'whitespace-cleanup-mode args))))

(add-to-list 'exec-path (expand-file-name "~/.nix-profile/bin"))

(defconst akirak/to-be-run-as-exwm (member "--exwm" command-line-args))

(defun akirak/exwm-session-p ()
  akirak/to-be-run-as-exwm)

Configure straight.el

(load-file (expand-file-name "core/straight.el" user-emacs-directory))

Install use-package using straight.el

(straight-use-package 'use-package)

Use straight.el by default in use-package directives

(setq straight-use-package-by-default t)

Benchmarking the startup process

(use-package benchmark-init
  :hook
  (after-init . benchmark-init/deactivate))

Use the latest Git version of Org mode

(require 'cl-lib)
(require 'subr-x)

Remove org-mode shipped with Emacs from load-path

(cl-delete-if (lambda (dpath) (string-match-p "/org/?$" dpath)) load-path)

Install org-mode from the Git repository

(load-file (expand-file-name "core/org-from-git.el" user-emacs-directory))

Recipe overrides

(akirak/straight-use-recipes-from-file
 akirak/straight-default-recipes-file)

(setq straight-x-pinned-packages
      '(("dracula-theme" . "11391ea531d40fb08c64313bbb86e4d29d7fe1c5")))

Load configuration files

(add-to-list 'load-path (expand-file-name "lisp" user-emacs-directory))
(require 'my/const/system)

TODO: Move to lisp/

(add-to-list 'load-path (expand-file-name "extras" user-emacs-directory))

Configuration

Prevent a confirmation dialog when the org file is loaded. Don’t forget to revert this variable at the beginning of the Org file.

(setq-default enable-local-variables :all)
(load-file (expand-file-name "core/setup.el" user-emacs-directory))

Things to set up before using use-package

(akirak/require 'setup-gc)

(setq-default enable-local-variables :safe)

These packages are required in other use-package directives declared in this configuration.

(use-package el-patch
  :custom
  (el-patch-enable-use-package-integration t))

Activate package.el for loading built-in packages from nixpkgs:

(require 'package)
(package-initialize 'noactivate)

Package-specific configuration files, including snippets, are kept in a separate repository, not in this repository.

(use-package no-littering)

Use the executable path from the shell

(use-package exec-path-from-shell
  :disabled t
  :if (memq window-system '(mac ns x))
  :init
  (exec-path-from-shell-initialize))

Use diminish to reduce clutters from the modeline. This adds support for :diminish keyword:

(use-package diminish
  :disabled t
  :init
  (diminish 'auto-revert-mode)
  (diminish 'outline-minor-mode)
  (diminish 'flyspell-mode))

(use-package use-package-company
  ;; Originally written by Foltik, but I use my fork
  :straight (use-package-company :host github :repo "akirak/use-package-company"))

(use-package general)

Default settings

(require 'setup-defaults)

(when (akirak/running-on-crostini-p)
  (require 'my/system/platform/crostini))

Migrating

(org-babel-load-file (expand-file-name "main.org" user-emacs-directory))

Packages

(use-package docker)
(use-package electric
  :straight (:type built-in)
  :hook
  (text-mode . electric-pair-local-mode))

(use-package helm-tail
  :commands (helm-tail))
(use-package org-recent-headings
  :after org
  :config
  (org-recent-headings-mode 1))
(use-package whole-line-or-region)

Autoloads

(use-package my/project
  :straight (:type built-in))

(use-package my/buffer/predicate
  :straight (:type built-in))

Commands and keybindings

Basic keybindings

These keybindings basically emulate UNIX shells (i.e. sh, bash, etc.).

I also like to define “dwim” commands, if applicable, to save the keybinding space and key strokes.

C-a

By default, C-a is bound to beginning-of-line.

This command first jump to the indentation and then visits the beginning of line.

(general-def prog-mode-map
  "C-a"
  (defun akirak/back-to-indentation-or-beginning-of-line ()
    (interactive)
    (if (or (looking-at "^")
            (string-match-p (rx (not (any space)))
                            (buffer-substring-no-properties
                             (line-beginning-position)
                             (point))))
        (back-to-indentation)
      (beginning-of-line))))

In org-mode, I prefer org-beginning-of-line.

(general-def :keymaps 'org-mode-map :package 'org
  "C-a" #'org-beginning-of-line)

C-e

(general-def :keymaps 'org-mode-map :package 'org
  "C-e" #'org-end-of-line)

C-h

(general-def
  "C-h" 'backward-delete-char)

C-w

(general-def
  "C-w"
  (defun akirak/kill-region-or-backward-kill-word (&optional arg)
    "If a region is active, run `kill-region'. Otherwise, run `backward-kill-word'."
    (interactive "p")
    (if (region-active-p)
        (kill-region (region-beginning) (region-end))
      (backward-kill-word arg))))

(general-def minibuffer-local-map
  "C-w" #'backward-kill-word)

(general-def ivy-minibuffer-map :package 'ivy
  "C-w" #'ivy-backward-kill-word)

C-u

(general-def minibuffer-local-map
  "C-u" #'backward-kill-sentence)

(general-def ivy-minibuffer-map :package 'ivy
  "C-u"
  (defun ivy-backward-kill-sentence ()
    (interactive)
    (if ivy--directory
        (progn (ivy--cd "/")
               (ivy--exhibit))
      (if (bolp)
          (kill-region (point-min) (point))
        (backward-kill-sentence)))))

C-r

In minibuffers, C-r should call history.

(general-def ivy-minibuffer-map :package 'ivy
  "C-r" 'counsel-minibuffer-history)

Key translation

Since I have bound C-h to backward-delete-char but still use the help system frequently, I bind M-` to <f1> in key-translation-map.

(general-def key-translation-map
  ;; * Obsolete
  ;; As <menu> (application on Windows keyboards) is hard to reach on some
  ;; keyboards, I will use <C-tab> instead. This key combination is occupied on
  ;; web browsers but vacant on most Emacs major modes, so it is safe to use it
  ;; on non-EXWM buffers.
  ;; "<C-tab>" (kbd "<menu>")

  ;; Chromebook don't have physical function keys. They substitute
  ;; Search + num for function keys, but Search + 1 is hard to press,
  ;; especially when Search and Ctrl are swapped.
  ;; This is quite annoying, so I will use M-` as <f1>.
  "M-`" (kbd "<f1>"))

Emulate virtual function keys of Chrome OS

Emulate function keys of Chrome OS, i.e. use s-NUM as function keys.

(define-globalized-minor-mode akirak/emulate-chromeos-fnkey-mode
  nil
  (lambda ()
    (cond
     (akirak/emulate-chromeos-fnkey-mode
      (dolist (n (number-sequence 1 9))
        (define-key key-translation-map
          (kbd (format "s-%d" n)) (kbd (format "<f%d>" n))))
      (define-key key-translation-map
        (kbd "s-0") (kbd "<f10>"))
      (define-key key-translation-map
        (kbd "s--") (kbd "<f11>"))
      (define-key key-translation-map
        (kbd "s-=") (kbd "<f12>")))
     (t
      (dolist (n (number-sequence 0 9))
        (define-key key-translation-map
          (kbd (format "s-%d" n)) nil))
      (define-key key-translation-map
        (kbd "s--") nil)
      (define-key key-translation-map
        (kbd "s-=") nil)))))

(unless (akirak/running-on-crostini-p)
  (akirak/emulate-chromeos-fnkey-mode 1))

Switching buffers

Switching buffers is the most essential operation in Emacs. Most of these commands are bound on C-x.

Helm commands

(general-def
  "C-x b"
  (defun akirak/switch-to-project-file-buffer (project)
    (interactive (list (if current-prefix-arg
                           'all
                         (-some-> (project-current)
                           (project-roots)
                           (car-safe)))))
    (require 'my/helm/action/git)
    (cond
     ((eq project 'all)
      (helm-buffers-list))
     (t
      (let ((default-directory (or project default-directory)))
        (helm :prompt (format "Project %s: " project)
              :sources
              `(,@(akirak/helm-project-buffer-sources project #'akirak/switch-to-project-file-buffer)
                ,akirak/helm-source-recent-files))))))
  "C-x p"
  (defun akirak/find-file-recursively (root)
    (interactive (list (akirak/project-root default-directory)))
    (require 'my/helm/source/file)
    (when current-prefix-arg
      (akirak/clear-project-file-cache root :sort 'modified))
    (let ((default-directory root))
      (helm :prompt (format "Browse %s: " root)
            :sources
            (list akirak/helm-source-project-files
                  akirak/helm-source-dummy-find-file))))
  "C-x d"
  (defun akirak/switch-to-dired-buffer ()
    "Switch to a directory buffer interactively.

Without a prefix, it displays a list of dired buffers, a list of
directories of live file buffers, and a list of directory
bookmarks.

With a single universal prefix, it displays a list of known Git
repositories.

With two universal prefixes, it displays a list of remote
connection identities of recent files."
    (interactive)
    (pcase current-prefix-arg
      ('(16)
       (require 'my/helm/source/remote)
       (helm :prompt "Remotes: "
             :sources
             '(akirak/helm-source-recent-remotes)))
      ('(4)
       (require 'my/helm/source/dir)
       (helm :prompt "Git repositories: "
             :sources akirak/helm-magit-list-repos-source))
      ('()
       (require 'my/helm/source/dir)
       (helm :prompt "Switch to a dired buffer: "
             :sources
             (list (akirak/helm-dired-buffer-source)
                   akirak/helm-open-buffer-directories-source
                   akirak/helm-directory-bookmark-source)))))
  "C-x j"
  (defun akirak/switch-to-org-buffer ()
    (interactive)
    (require 'helm-org-ql)
    (require 'org-recent-headings)
    (helm :prompt "Switch to Org: "
          :sources
          (list (akirak/helm-indirect-org-buffer-source)
                helm-source-org-recent-headings
                akirak/helm-source-org-starter-known-files
                helm-source-org-ql-views)))
  "C-x x"
  (defun akirak/switch-to-x-buffer (&optional arg)
    (interactive "P")
    (cond
     ((akirak/exwm-session-p)
      (helm :prompt "Switch to EXWM buffer: "
            :sources (akirak/helm-exwm-buffer-source)))
     ((akirak/windows-subsystem-for-linux-p)
      (user-error "Not supported on WSL"))
     ((eq system-type 'linux)
      ;; TODO: Implement it
      (cl-assert (executable-find "wmctrl"))
      (helm :prompt "X window: "
            :source
            (helm-build-sync-source "X windows"
              :candidates (-map (lambda (s) (cons s (car (s-split-words s))))
                                (process-lines "wmctrl" "-l"))
              :action (lambda (wid)
                        (async-start-process "wmctrl" "wmctrl" nil
                                             "-a" wid)))))))
  "C-x '"
  (defun akirak/switch-to-reference-buffer ()
    (interactive)
    (helm :prompt "Switch to a reference buffer: "
          :sources (akirak/helm-reference-buffer-source))))

In the list of project buffers, you can switch to a file list with M-/.

(general-def
  :keymaps 'akirak/helm-project-buffer-map
  :package 'my/helm/source/complex
  "M-/" (lambda ()
          (interactive)
          (helm-run-after-quit
           (lambda ()
             (akirak/find-file-recursively default-directory)))))

I haven’t bound any key to this command yet.

(defun akirak/switch-to-scratch-buffer ()
  (interactive)
  (helm :prompt "Switch to a scratch/REPL buffer: "
        :sources
        (akirak/helm-scratch-buffer-source)))

Browsing contents in specific buffers without leaving the context

(general-def
  ;; This command lets you browse lines in error buffers.
  "C-x t" #'helm-tail)

Navigation in buffer

Page navigation

I will use C-x [ and C-x ] for “page” navigation. These keys are bound to backward-page and forward-page by default, but they should be rebound depending on the major mode, since the notion of page/chunk varies.

(general-def
  ;; Default
  "C-x [" #'backward-page
  "C-x ]" #'forward-page)

(general-def :keymaps 'org-mode-map :package 'org
  ;; [remap backward-page]
  [remap forward-page]
  (defun akirak/org-narrow-to-next-sibling-subtree ()
    (interactive)
    (if (buffer-narrowed-p)
        (let ((old-level (save-excursion
                           (goto-char (point-min))
                           (org-outline-level)))
              (end (point-max)))
          (goto-char (point-max))
          (widen)
          (if (re-search-forward org-heading-regexp nil t)
              (let ((new-level (org-outline-level)))
                (org-narrow-to-subtree)
                (org-back-to-heading)
                (org-show-subtree)
                (cond
                 ((= new-level old-level)
                  (message "Narrowing to the next sibling"))
                 ((> new-level old-level)
                  (message "Narrowing to a child"))
                 ((< new-level old-level)
                  (message "Narrowing to an upper level"))))
            (message "No more heading")))
      (message "Buffer is not narrowed"))))

Editing

Undo and redo

You still can use the built-in undo command with C-x u

(use-package undo-fu
  :general
  ("C-/" #'undo-fu-only-undo
   "C-?" #'undo-fu-only-redo))

Editing source code comments in org-mode using outorg

Bind ~C-c ‘~ to outorg, which is the same keybinding as org-edit-special.

(use-package outorg
  :commands (outorg-edit-as-org)
  :config/el-patch
  (el-patch-defun outorg-convert-oldschool-elisp-buffer-to-outshine ()
    "Transform oldschool elisp buffer to outshine.
In `emacs-lisp-mode', transform an oldschool buffer (only
semicolons as outline-regexp) into an outshine buffer (with
outcommented org-mode headers)."
    (save-excursion
      (goto-char (point-min))
      (when (outline-on-heading-p)
        (outorg-convert-oldschool-elisp-headline-to-outshine))
      (while (not (eobp))
        (outline-next-heading)
        (outorg-convert-oldschool-elisp-headline-to-outshine)))
    (el-patch-remove (funcall 'outshine-hook-function))))
(general-def :keymaps 'emacs-lisp-mode-map
  "C-c '" #'outorg-edit-as-org)
(general-def :keymaps 'outorg-edit-minor-mode-map :package 'outorg
  "C-c '" #'outorg-copy-edits-and-exit)

Formatting code

(akirak/bind-generic
  "lf"
  (defun akirak/run-formatter ()
    (interactive)
    (require 'my/formatter)
    (pcase (akirak/get-project-formatter)
      (`(reformatter ,name)
       (if (region-active-p)
           (funcall (intern (concat name "-region")))
         (funcall (intern (concat name "-buffer"))))
       (let ((error-buf (get-buffer "*nixfmt errors*")))
         (if (and error-buf
                  (> (buffer-size error-buf) 0))
             (display-buffer error-buf)
           (when-let (w (and error-buf
                             (get-buffer-window error-buf)))
             (quit-window nil w)))))
      (_ (user-error "%s formatter" formatter)))))

(akirak/bind-mode :keymaps 'magit-status-mode-map :package 'magit-status
  "lf"
  (defun akirak/run-formatter-on-project ()
    (interactive)
    (require 'my/formatter)
    (let* ((project default-directory)
           (files (akirak/project-files project))
           (alist (->> (-group-by #'f-ext files)
                       (-sort (lambda (a b)
                                (> (length (cdr a))
                                   (length (cdr b)))))
                       (-filter #'car)))
           (ext (completing-read "File extension: "
                                 (-map #'car alist)
                                 nil t)))
      (dolist (file (cdr (assoc ext alist)))
        (let (new-buffer)
          (with-current-buffer (or (find-buffer-visiting file)
                                   (setq new-buffer
                                         (find-file-noselect file)))
            (save-restriction
              (widen)
              (pcase (akirak/get-project-formatter project :mode major-mode)
                (`(reformatter ,name)
                 (funcall (intern (concat name "-buffer"))))
                (_ (user-error "%s formatter" formatter)))
              (save-buffer))
            (let ((error-buf (get-buffer "*nixfmt errors*")))
              (if (and error-buf
                       (> (buffer-size error-buf) 0))
                  (progn
                    (switch-to-buffer (current-buffer))
                    (display-buffer error-buf)
                    (user-error "Error while applying the formatter"))
                (when-let (w (and error-buf
                                  (get-buffer-window error-buf)))
                  (quit-window nil w)))))
          (when new-buffer
            (kill-buffer new-buffer)))))
    (if (derived-mode-p 'magit-status-mode)
        (progn
          (message "Finished formatting. Refreshing the magit buffer...")
          (magit-refresh))
      (message "Finished formatting"))))

Running external commands

(general-def
  "C-x c"
  (defun akirak/compile-command (&optional arg)
    (interactive "P")
    (require 'my/compile)
    (cl-labels
        ((spago-root
          ()
          (locate-dominating-file default-directory "spago.dhall"))
         (spago-build
          (root)
          (let ((command (completing-read "PureScript spago command: "
                                          akirak/spago-compile-command-list)))
            (akirak/compile command :directory root)))
         (make-root
          ()
          (locate-dominating-file default-directory "Makefile"))
         (npm-root
          ()
          (locate-dominating-file default-directory "package.json"))
         (npm-run-something
          (root)
          (progn
            (require 'my/compile/npm)
            (let ((script-alist (akirak/npm-package-json-commands (f-join root "package.json")))
                  (default-directory root)
                  (action (lambda (command)
                            (akirak/compile (concat "npm " command)
                                            :nix-shell-args (unless (executable-find "npm")
                                                              '("-p" "nodejs"))))))
              (helm :prompt (format "npm command [%s]: " root)
                    :sources
                    (list (helm-build-sync-source "Script"
                            :candidates
                            (-map (lambda (cell)
                                    (cons (format "%s: %s" (car cell) (cdr cell))
                                          (cdr cell)))
                                  script-alist)
                            :coerce (-partial #'s-append "run ")
                            :action action)
                          (helm-build-sync-source "Basic commands"
                            :candidates
                            '("install")
                            :action action)))))))
      (let (root)
        (cond
         ((and (derived-mode-p 'purescript-mode)
               (setq root (spago-root)))
          (spago-build root))
         ((equal arg '(4))
          (helm :prompt "Compile history: "
                :sources akirak/helm-compile-history-source)
          (akirak/helm-shell-command))
         ((equal arg 0)
          (let ((root (akirak/project-root default-directory)))
            (if (and root (f-exists (f-join root ".github"))
                     (executable-find "act"))
                (let ((default-directory root))
                  (compile "act"))
              (user-error "N/A"))))
         ((make-root)
          (counsel-compile))
         ((setq root (npm-root))
          (npm-run-something root))))))
  "C-x C"
  (defun akirak/helm-shell-command ()
    (interactive)
    (require 'my/helm/source/org)
    (require 'my/helm/action/org-marker)
    (let ((root (or (akirak/project-root default-directory)
                    default-directory)))
      (setq akirak/programming-recipe-mode-name "sh"
            akirak/helm-org-ql-buffers-files (org-multi-wiki-entry-files 'organiser :as-buffers t))
      (helm :prompt (format "Execute command (project root: %s): " root)
            :sources
            (helm-make-source "Command" 'akirak/helm-source-org-ql-src-block
              :action akirak/helm-org-marker-sh-block-action-list)))))

Maintenance and development of the config

These commands are used to maintain this Emacs configuration.

(general-def
  "C-x M-m"
  (defun akirak/helm-my-library ()
    "Browse the library for this configuration."
    (interactive)
    (require 'my/helm/source/file)
    (let ((default-directory (f-join user-emacs-directory "lisp")))
      (helm :prompt (format "Files in %s: " default-directory)
            :sources (list (helm-make-source "Files in project"
                               'akirak/helm-source-project-file)
                           (helm-build-dummy-source "New file in lisp directory"
                             :action #'find-file))))))

Administration

Docker

(akirak/bind-admin
  "k" '(nil :wk "docker")
  "ki" #'docker-images
  "kk" #'docker-containers
  "kn" #'docker-networks
  "kv" #'docker-volumes)

Nix

(akirak/bind-admin
  "n" '(nil :wk "nix")
  "nC" #'nix-env-install-cachix-use
  "nn" #'nix-env-install-npm
  "nu" #'nix-env-install-uninstall)

Remote connections (TRAMP)

(akirak/bind-admin
  "r" '(nil :wk "remote")
  "rk" #'helm-delete-tramp-connection)