Skip to content
master
Switch branches/tags
Code

Files

Permalink
Failed to load latest commit information.
Type
Name
Latest commit message
Commit time
 
 
 
 
 
 
bin
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

README.org

Introduction

Hi there! That’s my dotfiles.

This repository contains configuration for three hosts:

  • omicron — my main laptop which runs NixOS
  • pie — my home server RPi running NixOS (see pie.org)

Most of config files are generated by org-babel from org files in this repository (yes, including this very same README.org). That’s literate programming applied to dotfiles.

This file contains Emacs configuration, NixOS and home-manager configuration for omicron.

pie.org is a separate NixOS config for pie host.

To generate actual nix files, you can open this file in Emacs, and execute M-x org-babel-tangle. Or from command line with the following command.

emacs README.org --batch -f org-babel-tangle

Note that you need to patch org-babel to correctly generate configs (<<patch-ob-tangle>>)

I keep generated files in sync with org files (so this repo is a valid Nix Flake), but they are not worth looking at—you’ll have much better time reading this doc instead.

Pieces not (yet) covered in org files are:

  • scripts at bin/

Table of Contents

Top-level

Flake

This repository is nix flakes–compatible.

The following goes to flake.nix file.

#
# This file is auto-generated from "README.org"
#
{
  description = "rasendubi's packages and NixOS/home-manager configurations";

  inputs = {
    nixpkgs = {
      type = "github";
      owner = "NixOS";
      repo = "nixpkgs";
      ref = "nixpkgs-unstable";
    };

    <<flake-inputs>>
  };

  outputs = { self, ... }@inputs:
    let
      # Flakes are evaluated hermetically, thus are unable to access
      # host environment (including looking up current system).
      #
      # That's why flakes must explicitly export sets for each system
      # supported.
      systems = ["x86_64-linux" "aarch64-linux"];

      # genAttrs applies f to all elements of a list of strings, and
      # returns an attrset { name -> result }
      #
      # Useful for generating sets for all systems or hosts.
      genAttrs = list: f: inputs.nixpkgs.lib.genAttrs list f;

      # Generate pkgs set for each system. This takes into account my
      # nixpkgs config (allowUnfree) and my overlays.
      pkgsBySystem =
        let mkPkgs = system: import inputs.nixpkgs {
              inherit system;
              overlays = self.overlays.${system};
              config = { allowUnfree = true; input-fonts.acceptLicense = true; };
            };
        in genAttrs systems mkPkgs;

      # genHosts takes an attrset { name -> options } and calls mkHost
      # with options+name. The result is accumulated into an attrset
      # { name -> result }.
      #
      # Used in NixOS and Home Manager configurations.
      genHosts = hosts: mkHost:
        genAttrs (builtins.attrNames hosts) (name: mkHost ({ inherit name; } // hosts.${name}));

      # merges a list of attrsets into a single attrset
      mergeSections = inputs.nixpkgs.lib.foldr inputs.nixpkgs.lib.mergeAttrs {};

    in mergeSections [
      <<flake-outputs-nixos>>
      <<flake-outputs-home-manager>>
      <<flake-outputs-packages>>
      <<flake-outputs-overlays>>
    ];
}

Nix flakes are still an experimental feature, so you need the following in NixOS configuration to enable it.

{
  nix = {
    package = pkgs.nixFlakes;
    extraOptions = ''
      experimental-features = nix-command flakes ca-references
    '';
  };
}

For non-NixOS system, install nixFlakes and put the following into ~/.config/nix/nix.conf.

experimental-features = nix-command flakes ca-references

Stable packages

For packages that are broken in nixpkgs-unstable, expose the latest stable channel as pkgs.stable.

Add input:

nixpkgs-stable = {
  type = "github";
  owner = "NixOS";
  repo = "nixpkgs";
  ref = "nixos-20.09";
};

Add overlay:

(final: prev: {
  stable = import inputs.nixpkgs-stable {
    inherit system;
    overlays = self.overlays.${system};
    config = { allowUnfree = true; };
  };
})

NixOS

Expose NixOS configurations.

(let
  nixosHosts = {
    omicron = { system = "x86_64-linux";  config = ./nixos-config.nix; };

    # pie uses a separate config as it is very different
    # from other hosts.
    pie =     { system = "aarch64-linux"; config = ./pie.nix; };
  };

  mkNixosConfiguration = { name, system, config }:
    let pkgs = pkgsBySystem.${system};
    in inputs.nixpkgs.lib.nixosSystem {
      inherit system;
      modules = [
        { nixpkgs = { inherit pkgs; }; }
        (import config)
      ];
      specialArgs = { inherit name inputs; };
    };

in {
  nixosConfigurations = genHosts nixosHosts mkNixosConfiguration;
})

Home manager

Add home-manager to flake inputs.

home-manager = {
  type = "github";
  owner = "rycee";
  repo = "home-manager";
  ref = "master";
  inputs.nixpkgs.follows = "nixpkgs";
};

Expose home-manager configurations.

(let
  homeManagerHosts = {
  };

  mkHomeManagerConfiguration = { system, name, config, username, homeDirectory }:
    let pkgs = pkgsBySystem.${system};
    in inputs.home-manager.lib.homeManagerConfiguration {
      inherit system pkgs username homeDirectory;
      configuration = { ... }: {
        nixpkgs.config.allowUnfree = true;
        nixpkgs.config.firefox.enableTridactylNative = true;
        nixpkgs.overlays = self.overlays.${system};
        imports = [
          self.lib.home-manager-common

          (import config)
        ];
      };
    };

in {
  # Re-export common home-manager configuration to be reused between
  # NixOS module and standalone home-manager config.
  lib.home-manager-common = { lib, pkgs, config, ... }: {
    imports = [
      <<home-manager-section>>
    ];
    home.stateVersion = "20.09";
  };
  homeManagerConfigurations = genHosts homeManagerHosts mkHomeManagerConfiguration;
})

Integrate home-manager module into NixOS.

{
  imports = [inputs.home-manager.nixosModules.home-manager];
  home-manager = {
    useUserPackages = true;
    useGlobalPkgs = true;
    users.rasen = inputs.self.lib.home-manager-common;
  };
}

Packages

Generate packages set for each supported system.

(let
  mkPackages = system:
    let
      pkgs = pkgsBySystem.${system};
    in
      mergeSections [
        <<flake-packages>>
      ];

in {
  packages = genAttrs systems mkPackages;
})

Overlays

Generate overlays for all supported systems.

(let
  mkOverlays = system: [
    # mix-in all local packages, so they are available as pkgs.${packages-name}
    (final: prev: self.packages.${system})

    <<flake-overlays>>
  ];
in {
  overlays = genAttrs systems mkOverlays;
})

<<flake-overlays>> are defined elsewhere.

NixOS

General

I’m a NixOS user. What’s cool about it is that I can describe all my system configuration in one file (almost). I can execute a single command and have a system with the same software, system settings, etc.

An outline of configuration looks like this:

#
# This file is auto-generated from "README.org"
#
{ name, config, pkgs, lib, inputs, ... }:
let
  machine-config = lib.getAttr name {
    omicron = [
      <<machine-omicron>>
    ];
  };

in
{
  imports = [
    {
      nixpkgs.config.allowUnfree = true;

      # The NixOS release to be compatible with for stateful data such as databases.
      system.stateVersion = "19.09";
    }

    <<nixos-section>>
  ] ++ machine-config;
}

This <<nixos-section>> is replaced by other parts of this doc.

Re-expose nixpkgs

{
  # for compatibility with nix-shell, nix-build, etc.
  environment.etc.nixpkgs.source = inputs.nixpkgs;
  nix.nixPath = ["nixpkgs=/etc/nixpkgs"];

  # register self and nixpkgs as flakes for quick access
  nix.registry = {
    self.flake = inputs.self;

    nixpkgs.flake = inputs.nixpkgs;
  };
}

Same but for Home Manager–managed host.

{
  home.file."nixpkgs".source = inputs.nixpkgs;
  systemd.user.sessionVariables.NIX_PATH = lib.mkForce "nixpkgs=$HOME/nixpkgs\${NIX_PATH:+:}$NIX_PATH";

  xdg.configFile."nix/registry.json".text = builtins.toJSON {
    version = 2;
    flakes = [
      {
        from = { id = "self"; type = "indirect"; };
        to = ({
          type = "path";
          path = inputs.self.outPath;
        } // lib.filterAttrs
          (n: v: n == "lastModified" || n == "rev" || n == "revCount" || n == "narHash")
          inputs.self);
      }
      {
        from = { id = "nixpkgs"; type = "indirect"; };
        to = ({
          type = "path";
          path = inputs.nixpkgs.outPath;
        } // lib.filterAttrs
          (n: v: n == "lastModified" || n == "rev" || n == "revCount" || n == "narHash")
          inputs.nixpkgs);
      }
    ];
  };
}

Sandbox

Build all packages in sandbox:

{
  nix.useSandbox = true;
}

Users

I’m the only user of the system:

{
  users.extraUsers.rasen = {
    isNormalUser = true;
    uid = 1000;
    extraGroups = [ "users" "wheel" ];
    initialPassword = "HelloWorld";
  };
  nix.trustedUsers = ["rasen"];
}

initialPassword is used only first time when user is created. It must be changed as soon as possible with passwd.

Machines

omicron

This is my small Dell XPS 13.

{
  imports = [
    (import "${inputs.nixos-hardware}/dell/xps/13-9360")
    inputs.nixpkgs.nixosModules.notDetected
  ];

  boot.initrd.availableKernelModules = [ "xhci_pci" "nvme" "usb_storage" "sd_mod" "rtsx_pci_sdmmc" ];
  boot.kernelModules = [ "kvm-intel" "wl" ];
  boot.extraModulePackages = [ config.boot.kernelPackages.rtl88x2bu config.boot.kernelPackages.broadcom_sta ];

  hardware.opengl = {
    enable = true;
    extraPackages = [
      pkgs.vaapiIntel
      pkgs.vaapiVdpau
      pkgs.libvdpau-va-gl
    ];
  };

  nix.maxJobs = lib.mkDefault 4;

  # powerManagement.cpuFreqGovernor = "powersave";

  boot.loader.systemd-boot.enable = true;
  boot.loader.efi.canTouchEfiVariables = true;
}

inputs.nixos-hardware comes from the following flake input.

nixos-hardware = {
  type = "github";
  owner = "NixOS";
  repo = "nixos-hardware";
  flake = false;
};

LVM on LUKS setup for disk encryption.

{
  boot.initrd.luks.devices = {
    root = {
      device = "/dev/disk/by-uuid/8b591c68-48cb-49f0-b4b5-2cdf14d583dc";
      preLVM = true;
    };
  };
  fileSystems."/boot" = {
    device = "/dev/disk/by-uuid/BA72-5382";
    fsType = "vfat";
  };
  fileSystems."/" = {
    device = "/dev/disk/by-uuid/434a4977-ea2c-44c0-b363-e7cf6e947f00";
    fsType = "ext4";
    options = [ "noatime" "nodiratime" "discard" ];
  };
  fileSystems."/home" = {
    device = "/dev/disk/by-uuid/8bfa73e5-c2f1-424e-9f5c-efb97090caf9";
    fsType = "ext4";
    options = [ "noatime" "nodiratime" "discard" ];
  };
  swapDevices = [
    { device = "/dev/disk/by-uuid/26a19f99-4f3a-4bd5-b2ed-359bed344b1e"; }
  ];
}

Clickpad:

{
  services.xserver.libinput = {
    enable = true;
    touchpad.accelSpeed = "0.7";
  };
}

Fix screen tearing (ArchWiki):

{
  services.xserver.config = ''
    Section "Device"
      Identifier "Intel Graphics"
      Driver "intel"

      Option "TearFree" "true"
      Option "TripleBuffer" "true"
    EndSection
  '';
}

Emacs

Install Emacs

I use emacs-27 from emacs-overlay.

emacs-overlay = {
  type = "github";
  owner = "nix-community";
  repo = "emacs-overlay";
};

Use overlay (<<flake-overlays>>).

inputs.emacs-overlay.overlay

Expose Emacs with my packages as a top-level package (<<flake-packages>>).

(let
  emacs-base = pkgs.emacsUnstable.override {
    withX = true;
    # select lucid toolkit
    toolkit = "lucid";
    withGTK2 = false; withGTK3 = false;
  };
  # emacs = pkgs.emacsUnstable;
  # emacs = pkgs.emacs.override {
  #   # Build emacs with proper imagemagick support.
  #   # See https://github.com/NixOS/nixpkgs/issues/70631#issuecomment-570085306
  #   imagemagick = pkgs.imagemagickBig;
  # };
  emacs-packages = (epkgs:
    (with epkgs.melpaPackages; [

      aggressive-indent
      atomic-chrome
      avy
      bash-completion
      beacon
      blacken
      cider
      clojure-mode
      cmake-mode
      color-identifiers-mode
      company
      company-box
      counsel
      counsel-projectile
      diff-hl
      diminish
      direnv
      dockerfile-mode
      doom-modeline
      dtrt-indent
      edit-indirect
      eglot
      el-patch
      elpy
      emojify
      epresent
      evil
      evil-collection
      evil-numbers
      evil-org
      evil-surround
      evil-swap-keys
      fish-completion
      fish-mode
      flycheck
      flycheck-inline
      flycheck-jest
      flycheck-rust
      forth-mode
      general
      go-mode
      google-translate
      graphviz-dot-mode
      groovy-mode
      haskell-mode
      imenu-list
      ivy
      ivy-bibtex
      ivy-pass
      jinja2-mode
      js2-mode
      json-mode
      ledger-mode
      lispyville
      lsp-haskell
      lsp-mode
      lsp-ui
      lua-mode
      magit
      markdown-mode
      modus-themes
      nix-mode
      nix-sandbox
      notmuch
      org-cliplink
      org-download
      org-drill
      org-ref
      org-roam
      org-roam-bibtex
      org-super-agenda
      paren-face
      pass
      php-mode
      pip-requirements
      plantuml-mode
      prettier-js
      projectile
      protobuf-mode
      psc-ide
      purescript-mode
      py-autopep8
      racer
      racket-mode
      restclient
      rjsx-mode
      rust-mode
      slime
      smex
      spaceline
      terraform-mode
      tide
      toc-org
      typescript-mode
      undo-fu
      use-package
      visual-fill-column
      vterm
      vue-mode
      w3m
      web-mode
      wgrep
      which-key
      whitespace-cleanup-mode
      writegood-mode
      yaml-mode
      yasnippet

    ]) ++
    [
      epkgs.elpaPackages.org
      epkgs.nongnuPackages.org-contrib
      epkgs.elpaPackages.adaptive-wrap
      epkgs.exwm
      # not available in melpa
      epkgs.elpaPackages.valign

      (epkgs.trivialBuild rec {
        pname = "org-ref-cite";
        version = "20210810";
        src = pkgs.fetchFromGitHub {
          owner = "jkitchin";
          repo = "org-ref-cite";
          rev = "102a853a678c57de43081bcf824917e2f7a8a4af";
          sha256 = "sha256-caHUyePjMk186d38xic105Jd3Q28reNO8tBrLvr8+0s=";
        };

        packageRequires = [
          epkgs.elpaPackages.org
          epkgs.melpaPackages.ivy
          epkgs.melpaPackages.hydra
          epkgs.melpaPackages.bibtex-completion
          epkgs.melpaPackages.avy
          epkgs.melpaPackages.ivy-bibtex
        ];

        meta = {
          description = "An org-cite processor that is like org-ref.";
          license = pkgs.lib.licenses.gpl2Plus;
        };
      })

      (epkgs.trivialBuild rec {
        pname = "org-roam-ui";
        version = "20210830";
        src = pkgs.fetchFromGitHub {
          owner = "org-roam";
          repo = "org-roam-ui";
          rev = "9ad111d2102c24593f6ac012206bb4b2c9c6c4e1";
          sha256 = "sha256-x6notv/U+y9Es8m58R/Qh7GEAtRqXqXvr7gy5OiDDUM=";
        };
        packageRequires = [
          epkgs.melpaPackages.f
          epkgs.melpaPackages.org-roam
          epkgs.melpaPackages.websocket
          epkgs.melpaPackages.simple-httpd
        ];

        postInstall = ''
          cp -r ./out/ $LISPDIR/
        '';

        meta = {
          description = "A graphical frontend for exploring your org-roam Zettelkasten";
          license = pkgs.lib.licenses.gpl3;
        };
      })

      (epkgs.trivialBuild rec {
        pname = "org-fc";
        version = "20201121";
        src = pkgs.fetchFromGitHub {
          owner = "rasendubi";
          repo = "org-fc";
          rev = "35ec13fd0412cd17cbf0adba7533ddf0998d1a90";
          sha256 = "sha256-2h1dIR7WHYFsLZ/0D4HgkoNDxKQy+v3OaiiCwToynvU=";
          # owner = "l3kn";
          # repo = "org-fc";
          # rev = "f1a872b53b173b3c319e982084f333987ba81261";
          # sha256 = "sha256-s2Buyv4YVrgyxWDkbz9xA8LoBNr+BPttUUGTV5m8cpM=";
        };
        packageRequires = [
          epkgs.elpaPackages.org
          epkgs.melpaPackages.hydra
        ];
        propagatedUserEnvPkgs = [ pkgs.findutils pkgs.gawk ];

        postInstall = ''
          cp -r ./awk/ $LISPDIR/
        '';

        meta = {
          description = "Spaced Repetition System for Emacs org-mode";
          license = pkgs.lib.licenses.gpl3;
        };
      })

      # required for org-roam/emacsql-sqlite3
      pkgs.sqlite

      pkgs.notmuch
      pkgs.w3m
      pkgs.imagemagick
      pkgs.shellcheck

      (pkgs.python3.withPackages (pypkgs: [
        pypkgs.autopep8
        pypkgs.black
        pypkgs.flake8
        pypkgs.mypy
        pypkgs.pylint
        pypkgs.virtualenv
      ]))

      (pkgs.aspellWithDicts (dicts: with dicts; [en en-computers en-science ru uk]))

      # latex for displaying fragments in org-mode
      (pkgs.texlive.combine {
        inherit (pkgs.texlive) scheme-small dvipng dvisvgm mhchem tikz-cd ;
      })
      pkgs.ghostscript
    ]
  );

  overrides = self: super: {
    # select org from elpa
    org = super.elpaPackages.org;
  };

  emacs-final = ((pkgs.emacsPackagesGen emacs-base).overrideScope' overrides).emacsWithPackages emacs-packages;

 in {
   my-emacs = emacs-final // {
     base = emacs-base;
     overrides = overrides;
     packages = emacs-packages;
   };
 })

Install Emacs with Home manager (<<home-manager-section>>)

{
  programs.emacs = {
    enable = true;
    package = pkgs.my-emacs.base;
    extraPackages = pkgs.my-emacs.packages;
    overrides = pkgs.my-emacs.overrides;
  };
  # services.emacs.enable = true;

  # fonts used by emacs
  home.packages = [
    pkgs.input-mono
    pkgs.libertine
  ];
}

Bootstrap Emacs config

Besides tangling into Flake/NixOS configuration files, this file is Emacs configuration.

Emacs does not source this file automatically, so I need to instruct it to do so. Check org-babel documentation for more info. The following snippet is an adaptation of that idea and goes to my .emacs.d/init.el.

;;
;; This file is auto-generated from "README.org"
;;

(defvar rasen/dotfiles-directory
  (file-name-as-directory
   (expand-file-name ".." (file-name-directory (file-truename user-init-file))))
  "The path to the dotfiles directory.")

(require 'org)
(require 'ob-tangle)

<<ob-tangle-patch>>

(org-babel-load-file (expand-file-name "README.org" rasen/dotfiles-directory))

Another important file that needs to be tangled is ./.emacs.d/early-init.el. For now, just add a header to it.

;;;
;;; This file is auto-generated from "README.org"
;;;

Patch ob-tangle

This patch is critical to getting this config working. Without it, org-babel will tangle this file incorrectly

This patches ob-tangle to allow defining sections with the same name multiple times. All sections with the same name are concatenated. (This was the default behavior some time ago, so this restores it.) (<<ob-tangle-patch>>)

(require 'el-patch)
;; org-babel fixes to tangle ALL matching sections
(defun rasen/map-regex (regex fn)
  "Map the REGEX over the BUFFER executing FN.

FN is called with the match-data of the regex.

Returns the results of the FN as a list."
  (save-excursion
    (goto-char (point-min))
    (let (res)
      (save-match-data
        (while (re-search-forward regex nil t)
          (let ((f (match-data)))
            (setq res
                  (append res
                          (list
                           (save-match-data
                             (funcall fn f))))))))
      res)))

(el-patch-feature ob-core)
(el-patch-defun org-babel-expand-noweb-references (&optional info parent-buffer)
  "Expand Noweb references in the body of the current source code block.

For example the following reference would be replaced with the
body of the source-code block named `example-block'.

<<example-block>>

Note that any text preceding the <<foo>> construct on a line will
be interposed between the lines of the replacement text.  So for
example if <<foo>> is placed behind a comment, then the entire
replacement text will also be commented.

This function must be called from inside of the buffer containing
the source-code block which holds BODY.

In addition the following syntax can be used to insert the
results of evaluating the source-code block named `example-block'.

<<example-block()>>

Any optional arguments can be passed to example-block by placing
the arguments inside the parenthesis following the convention
defined by `org-babel-lob'.  For example

<<example-block(a=9)>>

would set the value of argument \"a\" equal to \"9\".  Note that
these arguments are not evaluated in the current source-code
block but are passed literally to the \"example-block\"."
  (let* ((parent-buffer (or parent-buffer (current-buffer)))
         (info (or info (org-babel-get-src-block-info 'light)))
         (lang (nth 0 info))
         (body (nth 1 info))
         (comment (string= "noweb" (cdr (assq :comments (nth 2 info)))))
         (noweb-re (format "\\(.*?\\)\\(%s\\)"
                           (with-current-buffer parent-buffer
                             (org-babel-noweb-wrap))))
         (cache nil)
         (c-wrap
          (lambda (s)
            ;; Comment string S, according to LANG mode.  Return new
            ;; string.
            (unless org-babel-tangle-uncomment-comments
              (with-temp-buffer
                (funcall (org-src-get-lang-mode lang))
                (comment-region (point)
                                (progn (insert s) (point)))
                (org-trim (buffer-string))))))
         (expand-body
          (lambda (i)
            ;; Expand body of code represented by block info I.
            (let ((b (if (org-babel-noweb-p (nth 2 i) :eval)
                         (org-babel-expand-noweb-references i)
                       (nth 1 i))))
              (if (not comment) b
                (let ((cs (org-babel-tangle-comment-links i)))
                  (concat (funcall c-wrap (car cs)) "\n"
                          b "\n"
                          (funcall c-wrap (cadr cs))))))))
         (expand-references
          (lambda (ref cache)
            (pcase (gethash ref cache)
              (`(,last . ,previous)
               ;; Ignore separator for last block.
               (let ((strings (list (funcall expand-body last))))
                 (dolist (i previous)
                   (let ((parameters (nth 2 i)))
                     ;; Since we're operating in reverse order, first
                     ;; push separator, then body.
                     (push (or (cdr (assq :noweb-sep parameters)) "\n")
                           strings)
                     (push (funcall expand-body i) strings)))
                 (mapconcat #'identity strings "")))
              ;; Raise an error about missing reference, or return the
              ;; empty string.
              ((guard (or org-babel-noweb-error-all-langs
                          (member lang org-babel-noweb-error-langs)))
               (error "Cannot resolve %s (see `org-babel-noweb-error-langs')"
                      (org-babel-noweb-wrap ref)))
              (_ "")))))
    (replace-regexp-in-string
     noweb-re
     (lambda (m)
       (with-current-buffer parent-buffer
         (save-match-data
           (let* ((prefix (match-string 1 m))
                  (id (match-string 3 m))
                  (evaluate (string-match-p "(.*)" id))
                  (expansion
                   (cond
                    (evaluate
                     ;; Evaluation can potentially modify the buffer
                     ;; and invalidate the cache: reset it.
                     (setq cache nil)
                     (let ((raw (org-babel-ref-resolve id)))
                       (if (stringp raw) raw (format "%S" raw))))
                    ;; Retrieve from the Library of Babel.
                    ((nth 2 (assoc-string id org-babel-library-of-babel)))
                    ;; Return the contents of headlines literally.
                    ((org-babel-ref-goto-headline-id id)
                     (org-babel-ref-headline-body))
                    ;; Look for a source block named SOURCE-NAME.  If
                    ;; found, assume it is unique; do not look after
                    ;; `:noweb-ref' header argument.
                    ((org-with-point-at 1
                       (let ((r (org-babel-named-src-block-regexp-for-name id)))
                         (and (re-search-forward r nil t)
                              (not (org-in-commented-heading-p))
                              (el-patch-swap
                                (funcall expand-body
                                         (org-babel-get-src-block-info t))
                                (mapconcat
                                 #'identity
                                 (rasen/map-regex r
                                                  (lambda (md)
                                                    (funcall expand-body
                                                             (org-babel-get-src-block-info t))))
                                 "\n"))))))
                    ;; All Noweb references were cached in a previous
                    ;; run.  Extract the information from the cache.
                    ((hash-table-p cache)
                     (funcall expand-references id cache))
                    ;; Though luck.  We go into the long process of
                    ;; checking each source block and expand those
                    ;; with a matching Noweb reference.  Since we're
                    ;; going to visit all source blocks in the
                    ;; document, cache information about them as well.
                    (t
                     (setq cache (make-hash-table :test #'equal))
                     (org-with-wide-buffer
                      (org-babel-map-src-blocks nil
                        (if (org-in-commented-heading-p)
                            (org-forward-heading-same-level nil t)
                          (let* ((info (org-babel-get-src-block-info t))
                                 (ref (cdr (assq :noweb-ref (nth 2 info)))))
                            (push info (gethash ref cache))))))
                     (funcall expand-references id cache)))))
             ;; Interpose PREFIX between every line.
             (mapconcat #'identity
                        (split-string expansion "[\n\r]")
                        (concat "\n" prefix))))))
     body t t 2)))

GC hacks

Suppress GC in early init and restore it after init is complete. (.emacs.d/early-init.el)

(setq gc-cons-threshold most-positive-fixnum)
(add-hook 'emacs-startup-hook (defun rasen/restore-gc-threshold ()
                                (setq gc-cons-threshold 800000)))

use-package

use-package is a cool emacs library that helps managing emacs configuration making it simpler and more structured. (emacs-lisp)

;; Do not ensure packages---they are installed with Nix
(setq use-package-always-ensure nil)
;; (setq use-package-verbose t)
(eval-when-compile
  (require 'use-package))
(require 'bind-key)
(require 'diminish)

package

All emacs packages are installed with Nix. (See <<install-emacs>>.) Disable usage of emacs internal archives. (.emacs.d/early-init.el)

(require 'package)
(setq package-archives nil)
(setq package-enable-at-startup nil)

General (package)

I use general to define my keybindings. (emacs-lisp)

(use-package general)

;; Definer for my leader
(general-create-definer --leader-def   :prefix "SPC")
(general-create-definer --s-leader-def :keymaps '(motion insert emacs) :prefix "s-SPC" :non-normal-prefix "s-SPC")

;; Extra-hackery to define key with multiple prefixes
(defmacro leader-def (&rest args)
  (declare (indent defun))
  `(progn (--leader-def   ,@args)
          (--s-leader-def ,@args)))

;; Definer for my leader + applied globally across all windows.
(general-create-definer s-leader-def
  :keymaps '(motion emacs insert) :prefix "SPC"
  :non-normal-prefix "s-SPC"
  :global-prefix "s-SPC")

(use-package evil
  :config
  ;; free-up prefix
  (s-leader-def :keymaps '(motion normal visual) "" nil))

Don’t clutter system

Save custom configuration in the ~/.emacs.d/custom.el file so emacs does not clutter init.el.

(setq custom-file (expand-file-name "custom.el" user-emacs-directory))
(load custom-file t)

Don’t clutter the current directory with backups. Save them in a separate directory.

(setq backup-directory-alist '(("." . "~/.emacs.d/backups")))

Don’t clutter the current directory with auto-save files.

(setq auto-save-file-name-transforms '((".*" "~/.emacs.d/backups/" t)))

Do not create lockfiles either. (I am the only user in the system and only use emacs through daemon, so that should be ok.)

(setq create-lockfiles nil)

Helpers

Emacs lisp helper functions.

Timestamp-ids are used to uniquely identify things.

(defun rasen/tsid (&optional time)
  "Return timestamp-id."
  (format-time-string "%Y%m%d%H%M%S" time "UTC"))

(defun rasen/insert-tsid ()
  "Insert timestamp-id at point."
  (interactive)
  (insert (rasen/tsid)))

Insert current date in yyyy-mm-dd format. Useful when creating dated notes or dumb commits.

(defun rasen/insert-date (arg)
  "Insert current date. With prefix ARG, insert time in ISO 8601 format as well. With double-prefix, insert time in UTC timezone."
  (interactive "p")
  (insert (format-time-string
           (if (> arg 1)
               "%FT%T%z"
             "%F")
           nil
           (if (equal arg 16) t nil))))

(general-def 'insert '(git-commit-mode-map ivy-minibuffer-map)
  "C-c ." #'rasen/insert-date)
(general-def 'ivy-minibuffer-map
  "C-c ." #'rasen/insert-date)

(general-def 'normal 'org-mode-map
  "RET ." #'rasen/insert-date)
(defun rasen/copy-file-path ()
  "Copy the current buffer's path to kill ring."
  (interactive)
  ;; TODO: optionally strip project path
  (kill-new (buffer-file-name)))
(defun rasen/org-copy-log-entry (arg)
  "Copy the current org entry as a log line with timestamp.

The transformation is as follows:

* I am entry
:PROPERTIES:
CREATED: [2020-06-01]
:END:

becomes

- [2020-06-01] I am entry

If ARG is provided, kill the entry."
  (interactive "P")
  (let* ((heading (org-get-heading))
         (created (org-entry-get (point) "CREATED"))
         (line (concat "- " created " " heading)))
    (when arg
      (org-cut-subtree)
      (current-kill 1))
    (kill-new (concat line "\n"))
    (message line)))

Shamelessly stolen from https://github.com/purcell/emacs.d.

(defun rename-this-file-and-buffer (new-name)
  "Renames both current buffer and file it's visiting to NEW-NAME."
  (interactive "FNew name: ")
  (let ((name (buffer-name))
        (filename (buffer-file-name)))
    (unless filename
      (error "Buffer '%s' is not visiting file!" name))
    (if (get-buffer new-name)
        (message "A buffer named '%s' already exists!" new-name)
      (progn
        (make-directory (file-name-directory new-name) t)
        (when (file-exists-p filename)
          (rename-file filename new-name 1))
        (rename-buffer new-name)
        (set-visited-file-name new-name)))))

(defun delete-this-file-and-buffer ()
  "Delete the current file, and kill the buffer."
  (interactive)
  (or (buffer-file-name) (error "No file is currently being edited"))
  (when (yes-or-no-p (format "Really delete '%s'?"
                             (file-name-nondirectory buffer-file-name)))
    (delete-file (buffer-file-name))
    (kill-buffer)))
(defun add-to-path (str)
  "Add an STR to the PATH environment variable."
  (setenv "PATH" (concat str ":" (getenv "PATH"))))

ivy

(use-package ivy
  :demand
  :general
  (s-leader-def
    "b"  #'ivy-switch-buffer)
  :diminish ivy-mode
  :config
  <<ivy-config>>
  )

Do not start input with ^ and ignore the case.

(setq-default ivy-initial-inputs-alist nil)
(setq-default ivy-re-builders-alist '((t . ivy--regex-ignore-order)))

Do not show ./ and ../ during file name completion.

(setq-default ivy-extra-directories nil)

The normal C-j is not placed conveniently on Workman layout, so move its function to C-e (which is qwerty k).

(general-def 'ivy-minibuffer-map
  "C-e"   #'ivy-alt-done
  "C-M-e" #'ivy-immediate-done)

Evilify ivy-occur.

(general-def
  :keymaps '(ivy-occur-mode-map ivy-occur-grep-mode-map)
  :states 'normal
  "k"    #'ivy-occur-next-line
  "j"    #'ivy-occur-previous-line
  "C-n"  #'ivy-occur-next-line
  "C-p"  #'ivy-occur-previous-line
  "RET"  #'ivy-occur-press-and-switch
  "TAB"  #'ivy-occur-press
  "C-e"  #'ivy-occur-press-and-switch
  "g r"  #'ivy-occur-revert-buffer
  "g g"  #'evil-goto-first-line
  "d"    #'ivy-occur-delete-candidate
  "r"    #'read-only-mode
  "a"    #'ivy-occur-read-action
  "c"    #'ivy-occur-toggle-calling
  "f"    #'ivy-occur-press
  "o"    #'ivy-occur-dispatch
  "q"    #'quit-window)

(general-def 'normal 'ivy-occur-grep-mode-map
  "w"    #'ivy-wgrep-change-to-wgrep-mode)

Enable ivy.

(ivy-mode 1)

smex

I use smex for improved counsel-M-x (show most frequently used commands first).

(use-package smex
  :config
  (smex-initialize))

counsel

(use-package counsel
  :demand
  :diminish counsel-mode
  :general
  (s-leader-def
    "x"  #'counsel-M-x
    "f"  #'counsel-find-file)
  ('motion
   "g r"    #'counsel-git-grep
   "g /"    #'counsel-rg)
  ('read-expression-map
   "C-r"    #'counsel-expression-history)
  :config
  ;; reset ivy initial inputs for counsel
  (setq-default ivy-initial-inputs-alist nil)
  (counsel-mode 1))

avy

Jump anywhere with a few keystrokes in tree-like way.

(use-package avy
  :bind
  :general
  ('motion
   "K"  #'avy-goto-char)
  :custom
  ;; easy workman keys (excluding pinky)
  (avy-keys '(?s ?h ?t ?n ?e ?o ?d ?r ?u ?p)))

imenu / imenu-list

Use imenu to jump to symbols in the current buffer.

(use-package imenu-list
  :general
  (:keymaps 'imenu-list-major-mode-map
   :states 'normal
   "RET"       #'imenu-list-goto-entry
   "TAB"       #'imenu-list-display-entry
   "<backtab>" #'hs-toggle-hiding
   "g r"       #'imenu-list-refresh
   "q"         #'imenu-list-quit-window))
(defun rasen/imenu-or-list (arg)
  "Invoke `counsel-imenu'. If prefix is provided, toggle imenu-list"
  (interactive "P")
  (if arg
      (imenu-list-smart-toggle)
    (counsel-imenu)))

(leader-def 'motion  "g" #'rasen/imenu-or-list)

wgrep

Edit grep buffers and apply changes to the files.

(use-package wgrep)

whitespace

A good mode to highlight whitespace issues (leading/trailing spaces/newlines) and too long lines.

(use-package whitespace
  :diminish (global-whitespace-mode
             whitespace-mode
             whitespace-newline-mode)
  :hook (prog-mode . whitespace-mode)
  :config
  (setq-default whitespace-line-column 120
                whitespace-style '(face
                                   tab-mark
                                   empty
                                   trailing
                                   lines-tail)))

whitespace-cleanup

Fix whitespaces on file save.

(use-package whitespace-cleanup-mode
  :diminish whitespace-cleanup-mode
  :config
  (global-whitespace-cleanup-mode 1))

which-key

which-key is a minor mode for Emacs that displays the key bindings following your currently entered incomplete command (a prefix) in a popup.

(use-package which-key
  :defer 2
  :diminish which-key-mode
  :config
  (which-key-mode))

Google translate

(use-package google-translate
  :general
  ('normal
   '(markdown-mode-map org-mode-map)
   "z g t" #'rasen/google-translate-at-point
   "z g T" #'google-translate-smooth-translate)

  :commands (google-translate-smooth-translate)
  :config
  (defun rasen/google-translate-at-point (arg)
    "Translate word at point. If prefix is provided, do reverse translation"
    (interactive "P")
    (if arg
        (google-translate-at-point-reverse)
      (google-translate-at-point)))

  (require 'google-translate-default-ui)
  (require 'google-translate-smooth-ui)
  (setq google-translate-show-phonetic t)

  (setq google-translate-default-source-language "en"
        google-translate-default-target-language "ru")

  (setq google-translate-translation-directions-alist '(("en" . "ru") ("ru" . "en")))
  ;; auto-toggle input method
  (setq google-translate-input-method-auto-toggling t
        google-translate-preferable-input-methods-alist '((nil . ("en"))
                                                          (russian-computer . ("ru")))))

tab-bar-mode

New in Emacs 27.

(use-package tab-bar
  :general
  ('motion
   "M-h"  #'tab-bar-switch-to-prev-tab
   "M-l"  #'tab-bar-switch-to-next-tab)
  :config
  (general-def 'normal
    "M-h"  #'tab-bar-switch-to-prev-tab
    "M-l"  #'tab-bar-switch-to-next-tab)
  (general-def '(normal visual) 'org-mode-map
    "M-h" nil
    "M-l" nil)
  (general-def '(normal visual) 'evil-org-mode-map
    "M-h" nil
    "M-l" nil)
  (general-def 'org-mode-map
    "M-h" nil
    "M-l" nil)

  (setq tab-bar-select-tab-modifiers '(meta))
  (setq tab-bar-tab-hints t)

  ;; Show tab bar only if there are >1 tab
  (setq tab-bar-show 1)
  ;; Do not show buttons
  (setq tab-bar-close-button-show nil
        tab-bar-new-button-show nil)
  ;; (tab-bar-mode)
)

Highlight current line

Highlight current line.

(global-hl-line-mode)

;; The following trick with buffer-local `global-hl-line-mode` allows
;; disabling hl-line-mode per-buffer
(make-variable-buffer-local 'global-hl-line-mode)
(defun rasen/disable-hl-line-mode ()
  (interactive)
  (setq global-hl-line-mode nil))

Scrolling

scroll-margin is a number of lines of margin at the top and bottom of a window. Scroll the window whenever point gets within this many lines of the top or bottom of the window. (scroll-conservatively should be greater than 100 to never recenter point. Value 1 helps, but eventually recenters cursor if you scroll too fast.)

(setq scroll-margin 3
      scroll-conservatively 101)

visual-fill-column

Center all text in the buffer in some modes. (That’s a nice distraction-free setup.)

(use-package visual-fill-column
  :commands (visual-fill-column-mode)
  :hook
  (markdown-mode . rasen/activate-visual-fill-column)
  (org-mode . rasen/activate-visual-fill-column)
  :init
  (defun rasen/activate-visual-fill-column ()
    (interactive)
    (setq-local fill-column 111)
    (visual-line-mode t)
    (visual-fill-column-mode t))
  :config
  (setq-default visual-fill-column-center-text t
                visual-fill-column-fringes-outside-margins nil))

Misc

Use single-key y/n instead of a more verbose yes/no.

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

Automatically add a final newline in files.

(setq-default require-final-newline t)

Environment

EXWM

Emacs is my Window Manager, thanks to EXWM.

NixOS has an EXWM module, but my feeling is that it’s too limiting. (<<nixos-section>>)

{
  environment.systemPackages = [ pkgs.xorg.xhost ];
  services.xserver.windowManager.session = lib.singleton {
    name = "exwm";
    start = ''
      xhost +SI:localuser:$USER
      exec emacs
    '';
      # exec ${pkgs.my-emacs}/bin/emacsclient -a "" -c
  };
  services.xserver.displayManager.lightdm.enable = true;
  # services.xserver.displayManager.startx.enable = true;
  services.xserver.displayManager.defaultSession = "none+exwm";
}

Initialize EXWM configuration (emacs-lisp)

(use-package exwm
  :init
  ;; these must be set before exwm is loaded
  (setq mouse-autoselect-window t
        focus-follows-mouse t)
  :config
  ;; the next two make all buffers available on all workspaces
  (setq exwm-workspace-show-all-buffers t)
  (setq exwm-layout-show-all-buffers t)

  ;; Make class name the buffer name
  (add-hook 'exwm-update-class-hook
            (lambda ()
              (exwm-workspace-rename-buffer exwm-class-name)))

  (with-eval-after-load 'evil
    (evil-set-initial-state 'exwm-mode 'motion))

  ;; do not forward anything besides keys defined with
  ;; `exwm-input-set-key' and `exwm-mode-map'
  (setq exwm-input-prefix-keys '())

  (exwm-enable))

Add a couple of helpers functions. (emacs-lisp)

(defun rasen/autostart (cmd)
  "Start CMD unless already running."
  (let ((buf-name (concat "*" cmd "*")))
    (unless (process-live-p (get-buffer-process buf-name))
      (start-process-shell-command cmd buf-name cmd))))

(defun rasen/start-command (command &optional buffer)
  "Start shell COMMAND in the background. If BUFFER is provided, log process output to that buffer."
  (start-process-shell-command command buffer command))

(defun rasen/switch-start (buffer cmd)
  "Switch to buffer with name BUFFER or start one with CMD."
  (if-let (b (get-buffer buffer))
      (switch-to-buffer b)
    (rasen/start-command cmd)))

(defun rasen/exwm-input-set-key (key command)
  "Similar to `exwm-input-set-key', but always refreshes prefix
keys. This allows defining keys from any place in config."
  (exwm-input-set-key key command)
  ;; Alternatively, try general-setq (which calls customize handler)
  (exwm-input--update-global-prefix-keys))

Window management

Common key bindings. (emacs-lisp)

(use-package evil
  :defer t
  :commands (evil-window-split
             evil-window-vsplit))

(defun rasen/exwm-next-workspace ()
  (interactive)
  ;; (let ((cur exwm-workspace-current-index)
  ;;       (max exwm-workspace-number))
  ;;   (exwm-workspace-switch (% (+ cur 1) max)))
  (other-frame 1))

(defun rasen/move-tab-other-frame ()
  (interactive)
  (tab-bar-move-tab-to-frame nil))

;; despite the fact s-SPC binds to nil, EXWM will add s-SPC to
;; global prefix key.
(exwm-input-set-key (kbd "s-SPC") nil)
(exwm-input-set-key (kbd "s-x") #'counsel-M-x)

(exwm-input-set-key (kbd "s-R") #'exwm-reset)
(exwm-input-set-key (kbd "s-Q") #'save-buffers-kill-terminal)
(exwm-input-set-key (kbd "s-r") (lambda (command)
                                  (interactive (list (read-shell-command "Run: ")))
                                  (rasen/start-command command)))
(exwm-input-set-key (kbd "s-w") #'exwm-workspace-switch)
(exwm-input-set-key (kbd "s-b") #'counsel-switch-buffer)
(exwm-input-set-key (kbd "s-q") #'kill-this-buffer)

(exwm-input-set-key (kbd "s-\\") #'exwm-input-toggle-keyboard)
(exwm-input-set-key (kbd "<s-escape>") #'rasen/switch-to-previous-buffer)
(exwm-input-set-key (kbd "<s-tab>") #'counsel-switch-buffer)

;; window management
(exwm-input-set-key (kbd "s--") #'delete-other-windows)
(exwm-input-set-key (kbd "s-0") #'delete-window)
(exwm-input-set-key (kbd "s-h") #'windmove-left)
(exwm-input-set-key (kbd "s-k") #'windmove-down)
(exwm-input-set-key (kbd "s-j") #'windmove-up)
(exwm-input-set-key (kbd "s-l") #'windmove-right)
(exwm-input-set-key (kbd "s-s") #'evil-window-split)
(exwm-input-set-key (kbd "s-v") #'evil-window-vsplit)

(exwm-input-set-key (kbd "s-.") #'rasen/exwm-next-workspace)
(exwm-input-set-key (kbd "s->") #'rasen/move-tab-other-frame) ;; s-S-.

(general-def
  :prefix-command 'rasen/tab-map
  "t" #'tab-bar-mode
  "1" #'tab-bar-select-tab
  "2" #'tab-bar-select-tab
  "3" #'tab-bar-select-tab
  "4" #'tab-bar-select-tab
  "5" #'tab-bar-select-tab
  "6" #'tab-bar-select-tab
  "7" #'tab-bar-select-tab
  "8" #'tab-bar-select-tab
  "9" #'tab-bar-select-tab
  "n" #'tab-bar-new-tab
  "h" #'tab-bar-switch-to-prev-tab
  "l" #'tab-bar-switch-to-next-tab
  ">" #'tab-bar-move-tab-to-frame
  "k" #'tab-bar-close-tab)
(s-leader-def "t" #'rasen/tab-map)
(exwm-input-set-key (kbd "s-t") #'rasen/tab-map)

(exwm-input-set-key (kbd "s-1") #'tab-bar-select-tab)
(exwm-input-set-key (kbd "s-2") #'tab-bar-select-tab)
(exwm-input-set-key (kbd "s-3") #'tab-bar-select-tab)
(exwm-input-set-key (kbd "s-4") #'tab-bar-select-tab)
(exwm-input-set-key (kbd "s-5") #'tab-bar-select-tab)
(exwm-input-set-key (kbd "s-6") #'tab-bar-select-tab)
(exwm-input-set-key (kbd "s-7") #'tab-bar-select-tab)
(exwm-input-set-key (kbd "s-8") #'tab-bar-select-tab)
(exwm-input-set-key (kbd "s-9") #'tab-bar-select-tab)

(exwm-input-set-key (kbd "<s-f1>") (lookup-key (current-global-map) (kbd "<f1>")))

(defun rasen/exwm-firefox ()
  (interactive)
  (rasen/switch-start "Firefox" "firefox"))

(defun rasen/exwm-telegram ()
  (interactive)
  (rasen/switch-start "TelegramDesktop" "telegram-desktop"))

(defun rasen/exwm-google-play-music ()
  (interactive)
  (rasen/switch-start "Google Play Music Desktop Player" "google-play-music-desktop-player"))

(defun rasen/terminal ()
  (interactive)
  (rasen/start-command "urxvt"))

;; From https://emacsredux.com/blog/2013/04/28/switch-to-previous-buffer/
(defun rasen/switch-to-previous-buffer ()
  "Switch to previously open buffer.
  Repeated invocations toggle between the two most recently open buffers."
  (interactive)
  (switch-to-buffer (other-buffer (current-buffer))))

(exwm-input-set-key (kbd "s-!") #'rasen/exwm-firefox)           ;; s-S-1
(exwm-input-set-key (kbd "s-$") #'rasen/exwm-telegram)          ;; s-S-4
(exwm-input-set-key (kbd "s-&") #'rasen/exwm-google-play-music) ;; s-S-7
(exwm-input-set-key (kbd "s-(") #'notmuch)                      ;; s-S-9
(exwm-input-set-key (kbd "<s-return>") #'vterm)
(exwm-input-set-key (kbd "<s-S-return>") #'rasen/terminal)

(exwm-input-set-key (kbd "s-z") #'exwm-layout-toggle-mode-line)
(exwm-input-set-key (kbd "s-f") #'exwm-layout-toggle-fullscreen)
(exwm-input-set-key (kbd "s-C-SPC") #'exwm-floating-toggle-floating)

(general-def 'exwm-mode-map
  "C-c" nil ;; disable default bindings

  "<f1> v" #'counsel-describe-variable)

;; Without the next line, EXWM won't intercept necessary prefix keys
;; (if you rebind them after EXWM has started)
(exwm-input--update-global-prefix-keys)

Window layout

Rules to automatically layout windows when they appear.

(setq display-buffer-alist
      '(("\\*\\(Help\\|Error\\)\\*" .
         (display-buffer-in-side-window
          (side . right)
          (slot . 1)
          (window-width . 80)
          (no-other-window . t)))
        ("\\*\\(Calendar\\)\\*" .
         (display-buffer-in-side-window
          (side . bottom)
          (slot . -1)
          ;; (window-width . 80)
          (no-other-window . t)))
        ("\\*org-roam\\*" .
         (display-buffer-in-side-window
          (side . right)
          (slot . -1)
          (window-width . 80)
          (no-other-window . t)))))

Screen locking

I use xss-lock + slock for screen locking. Actual handling is coded in Emacs.

Slock

Slock is a simple X display locker and does not crash as xscreensaver does.

Slock tries to disable OOM killer (so the locker is not killed when memory is low) and this requires a suid flag for executable. Otherwise, you get the following message:

slock: unable to disable OOM killer. Make sure to suid or sgid slock.
{
  programs.slock.enable = true;
}

xss-lock

xss-lock is a small utility to plug a screen locker into screen saver extension for X. This automatically activates selected screensaver after a period of user inactivity, or when system goes to sleep.

{
  home.packages = [
    pkgs.xss-lock
  ];
}

EXWM integration

Autostart xss-lock (emacs-lisp).

(rasen/autostart "xss-lock -n \"xset dpms force off\" slock")

Bind s-M-l to lock screen immediately.

(defun rasen/blank-screen ()
  "Blank screen after 1 second. The delay is introduced so the user
could get their hands away from the keyboard. Otherwise, the screen
would lit again immediately."
  (interactive)
  (run-at-time "1 sec" nil
               (lambda ()
                 (rasen/start-command "xset dpms force off"))))

(defun rasen/lock-screen ()
  "Lock and blank screen."
  (interactive)
  (rasen/start-command "slock")
  (rasen/blank-screen))

(rasen/exwm-input-set-key (kbd "s-M-l") #'rasen/lock-screen)

System tray

Use built-in EXWM system tray (emacs-lisp)

(use-package exwm-systemtray
  :after exwm
  :config
  (exwm-systemtray-enable))

Screenshots

I use Escrotum for screenshots.

Install it. (<<home-manager-section>>)

{
  home.packages = [ pkgs.escrotum ];
}

Bind it to Print Screen button. (emacs-lisp)

(defun rasen/screenshot ()
  (interactive)
  ;; -sC — choose selection + save to clipboard
  (rasen/start-command "escrotum -sC"))

(rasen/exwm-input-set-key (kbd "<print>") #'rasen/screenshot)

Misc

I definitely use X server:

{
  services.xserver.enable = true;
}

Use English as my only supported locale:

{
  i18n.supportedLocales = [ "en_US.UTF-8/UTF-8" ];
}

Setup timezone:

{
  time.timeZone = "Europe/Kiev";
}

Input

Keyboard

Workman

I use Workman Layout. It’s a nice non-qwerty layout that de-prioritizes two middle /columns,/ so your hands don’t rotate too often.

It looks like this:

images/20200612233931-workman.png

Even though I am a heavy user of Vim-like keybindings, I didn’t remap any keys to bring h/j/k/l to the home row—yes, they all are scattered around and I consider that a feature. (You shouldn’t be using h/j/k/l anyway.)

Though, there is one remapping that I actually do. If you look at the picture, you’ll notice that j—which usually means “down”—is above k on keyboard. I could get my mind used to it, so I remap jk functions in all applications I use (Emacs, Vim, Firefox, Zathura).

Keyboard layout

Besides Workman, I use Ukrainian layout. I also use Russian symbols, but they are on the third level (<<nixos-section>>).

{
  services.xserver.layout = "us,ua";
  services.xserver.xkbVariant = "workman,";

  # Use same config for linux console
  console.useXkbConfig = true;
}

Same setting but for Home Manager (<<home-manager-section>>)

{
  home.keyboard = {
    layout = "us,ua";
    variant = "workman,";
  };
}

Map left Caps Lock to Ctrl, and left Ctrl to switch between layout. (Shift-Ctrl triggers Caps Lock function.) I never use Caps Lock–the feature, so it’s nice to have Caps LED indicate alternate layouts.

<<nixos-section>>:

{
  services.xserver.xkbOptions = "grp:lctrl_toggle,grp_led:caps,ctrl:nocaps";
  # services.xserver.xkbOptions = "grp:caps_toggle,grp_led:caps";
}

Xkeymap

I have a slightly customized Workman+Ukrainian layout at ./Xkeymap (more keys on 3rd level). It’s quite big and isn’t particularly fun to explain, so I keep it off my main config.

Activate it on session start (<<home-manager-section>>).

{
  xsession.initExtra = ''
    xkbcomp ${./Xkeymap} $DISPLAY
  '';
}

One caveat is that it’s dropped when I activate (update) new system version, or when unplug keyboard and plug it again.

Add a small Emacs function to re-apply this configuration (emacs-lisp).

(defun rasen/set-xkb-layout ()
  (interactive)
  (rasen/autostart "xkbcomp ~/dotfiles/Xkeymap $DISPLAY"))

(rasen/set-xkb-layout)

Install xkbcomp to execute these commands. (<<home-manager-section>>)

{
  home.packages = [ pkgs.xorg.xkbcomp ];
}

Compose keys

Add some custom compose keys (<<home-manager-section>>):

{
  home.file.".XCompose".text = ''
    include "%L"

    <Multi_key> <less> <equal>           : "⇐" U21D0 # Leftwards Double Arrow
    <Multi_key> <equal> <greater>        : "⇒" U21D2 # RIGHTWARDS DOUBLE ARROW
    <Multi_key> <less> <greater> <equal> : "⇔" U21D4 # LEFT RIGHT DOUBLE ARROW
    <Multi_key> <equal> <less> <greater> : "⇔" U21D4 # LEFT RIGHT DOUBLE ARROW
    <Multi_key> <minus> <less> <greater> : "↔" U2194 # LEFT RIGHT ARROW

    <Multi_key> <s> <u> <m> : "∑"
    <Multi_key> <f> <a> : "∀"                 # for all
    <Multi_key> <t> <e> : "∃"                 # there exists
    <Multi_key> <slash> <t> <e> : "∄"
    <Multi_key> <asciitilde> <equal> : "≅"    # approximately equal
    <Multi_key> <asciitilde> <asciitilde> : "≈"   U2248   # ~ ~ ALMOST EQUAL TO
    <Multi_key> <i> <n> : "∈" U2208
    <Multi_key> <n> <i> <n> : "∉" U2209

    # White Right Pointing Index
    <Multi_key> <rght> : "☞" U261E

    <Multi_key> <o> <c> : "℃"
    <Multi_key> <o> <f> : "℉"

    <Multi_key> <x> <x> : "❌"  # Cross Mark

    <Multi_key> <apostrophe> <apostrophe> : "́" # stress

    <Multi_key> <O> <slash> : "⌀" U2300 # DIAMETER SIGN
    <Multi_key> <slash> <O> : "⌀" U2300 # DIAMETER SIGN
    <Multi_key> <r> <r> : "√" U221A # SQUARE ROOT
    <Multi_key> <r> <3> : "∛" U221B # CUBE ROOT
    <Multi_key> <m> <A> : "∀" U2200 # FOR ALL
    <Multi_key> <m> <E> : "∃" U2203 # THERE EXISTS
    <Multi_key> <m> <i> : "∊" U220A # SMALL ELEMENT OF
    <Multi_key> <m> <d> : "∂" U2202 # PARTIAL DIFFERENTIAL
    <Multi_key> <m> <D> : "∆" U2206 # INCREMENT, Laplace operator
    <Multi_key> <m> <S> : "∑" U2211 # N-ARY SUMMATION, Sigma
    <Multi_key> <m> <I> : "∫" U222B # INTEGRAL
    <Multi_key> <m> <minus> : "−" U2212 # MINUS SIGN
    <Multi_key> <equal> <asciitilde> : "≈" U2248 # ALMOST EQUAL TO
    <Multi_key> <asciitilde> <equal> : "≈" U2248 # ALMOST EQUAL TO
    <Multi_key> <underscore> <underscore> : "‾" U023E # OVERLINE
    <Multi_key> <equal> <slash>  	: "≠"   U2260 # NOT EQUAL TO
    <Multi_key> <slash> <equal>  	: "≠"   U2260 # NOT EQUAL TO
    <Multi_key> <minus> <equal> 	: "≡"   U2261 # IDENTICAL TO
    <Multi_key> <equal> <minus> 	: "≡"   U2261 # IDENTICAL TO
    <Multi_key> <m> <less> <equal> : "≤" U2264 # LESS-THAN OR EQUAL TO
    <Multi_key> <m> <greater> <equal> : "≥" U2265 # GREATER-THAN OR EQUAL TO
    <Multi_key> <m> <o> <o> : "∞" # infty
    <Multi_key> <m> <_> <i> : "ᵢ" # subscript i
    <Multi_key> <m> <^> <i> : "ⁱ" # superscript i
    <Multi_key> <m> <_> <minus> : "₋" # subscript minus
    <Multi_key> <m> <^> <minus> : "⁻" # superscript minus
    <Multi_key> <m> <_> <plus> : "₊" # subscript plus
    <Multi_key> <m> <^> <plus> : "⁺" # superscript plus
    <Multi_key> <m> <asterisk> : "∘" # ring (function compose) operator
    <Multi_key> <m> <period> : "∙" # dot operator
    <Multi_key> <m> <asciitilde> : "∝" # proportional to
    <Multi_key> <q> <e> <d> : "∎" # q.e.d.
  '';
}

xcape

Make short press on left control behave as Escape (<<home-manager-section>>):

{
  services.xcape = {
    enable = true;
    mapExpression = {
      Control_L = "Escape";
    };
  };
}

Emacs quail

Emacs has built-in capability to change keyboard layout (for insert state only), which is triggered by C-\. In order to work properly, Emacs needs to know my keyboard layout.

(use-package quail
  :ensure nil ; built-in
  :config
  (add-to-list 'quail-keyboard-layout-alist
               '("workman" . "\
                              \
  1!2@3#4$5%6^7&8*9(0)-_=+`~  \
  qQdDrRwWbBjJfFuUpP;:[{]}\\|  \
  aAsShHtTgGyYnNeEoOiI'\"      \
  zZxXmMcCvVkKlL,<.>/?        \
                              "))
  (quail-set-keyboard-layout "workman"))

Mouse

I use Razer Naga Chroma with 12-button thumb cluster. I use these buttons are used to switch browser tabs, close/open them, go back and forth in history so that sometimes I can browse web only using one hand.

Python listener

I have a small Python script at ./naga to connect to mouse as input device and report button pressed to Emacs.

The script is pretty simple (./naga/naga/__init__.py).

#!/usr/bin/env python3
import evdev
import subprocess


def value_to_string(val):
    if val == evdev.events.KeyEvent.key_up:
        return "up"
    if val == evdev.events.KeyEvent.key_down:
        return "down"
    if val == evdev.events.KeyEvent.key_hold:
        return "hold"
    return None


def main():
    device = evdev.InputDevice(
        "/dev/input/by-id/usb-Razer_Razer_Naga_Chroma-if02-event-kbd"
    )
    print(device)

    device.grab()

    for event in device.read_loop():
        if event.type == evdev.ecodes.EV_KEY:
            subprocess.call(
                [
                    "emacsclient",
                    "--eval",
                    '(rasen/razer {} "{}")'.format(
                        event.code - 1, value_to_string(event.value)
                    ),
                ]
            )

setup.py to make it installable (./naga/setup.py).

#!/usr/bin/env python
from setuptools import setup

setup(
    name='naga',
    version='1.0',
    url='https://github.com/rasenduby/dotfiles',
    packages=['naga'],
    entry_points={
        'console_scripts': [
            'naga=naga:main',
        ],
    },
    license='MIT',
    install_requires=[
        'evdev',
    ],
)

And default.nix to make it installable with Nix (./naga/default.nix).

{ lib, python3Packages }:
python3Packages.buildPythonApplication {
  name = "naga-1.0";

  src = lib.cleanSource ./.;

  propagatedBuildInputs = [
    python3Packages.evdev
  ];
}

Expose naga as Flake packages (<<flake-packages>>).

{
  naga = pkgs.callPackage ./naga { };
}

Finally install it (<<home-manager-section>>).

{
  home.packages = [ pkgs.naga ];
}

And I believe I need to add my user to input group to fix permissions (<<nixos-section>>).

{
  users.users.rasen.extraGroups = [ "input" ];
}

Activate it on EXWM start (emacs-lisp).

(defun rasen/naga ()
  (interactive)
  (rasen/autostart "naga"))

(rasen/naga)

Start naga on plugdev

Emacs handler

Emacs determines what to do with each keypress.

(defun rasen/exwm-send-keys (keys)
  (dolist (key (append keys nil))
    (exwm-input--fake-key key)))

(defun rasen/razer (code value)
  (with-current-buffer (window-buffer (selected-window))
    ;; (message "(razer %s %s) class-name=%s" code value exwm-class-name)
    (when (string= value "down")
      (cond ((= code 11) (rasen/switch-to-previous-buffer))
            ((or (string= exwm-class-name "Firefox") (string= exwm-class-name "Google-chrome"))
             (cond
              ((= code 1) (rasen/exwm-send-keys (kbd "<M-left>"))) ;; back (history)
              ((= code 2) (rasen/exwm-send-keys (kbd "<C-prior>"))) ;; prev tab
              ((= code 3) (rasen/exwm-send-keys (kbd "<C-next>"))) ;; next tab
              ((= code 4) (rasen/exwm-send-keys (kbd "<M-right>")));; next (history)
              ((= code 5) (rasen/exwm-send-keys (kbd "C-S-t"))) ;; restore last closed tab
              ((= code 6) (rasen/exwm-send-keys (kbd "C-w"))) ;; close tab
              ((= code 12) (rasen/exwm-send-keys (kbd "<C-tab>"))) ;; switch to previous tab
              ))
            ((string= exwm-class-name "TelegramDesktop")
             (cond
              ((= code 1) (rasen/exwm-send-keys (kbd "<escape>"))) ;; deselect conversation
              ((= code 2) (rasen/exwm-send-keys (kbd "<C-S-tab>"))) ;; prev conversation
              ((= code 3) (rasen/exwm-send-keys (kbd "<C-tab>"))) ;; next conversation
              ))
            (t (message "razer-unhandled %s" code))))))

Network

NetworkManager

(<<nixos-section>>)

{
  networking = {
    hostName = name;

    networkmanager = {
      enable = true;
      wifi.powersave = false;
    };

    # disable wpa_supplicant
    wireless.enable = false;
  };

  users.extraUsers.rasen.extraGroups = [ "networkmanager" ];
}

Install network manager applet for user. (<<home-manager-section>>)

{
  home.packages = [ pkgs.networkmanagerapplet ];
}

Auto-start nm-applet (emacs-lisp)

(rasen/autostart "nm-applet")

SSH

(<<nixos-section>>)

{
  services.openssh = {
    enable = true;
    passwordAuthentication = false;
  };
}

Mosh

Mosh (mobile shell) is a cool addition to ssh.

{
  programs.mosh.enable = true;
}

dnsmasq

Use dnsmasq as a DNS cache.

(<<nixos-section>>)

{
  services.dnsmasq = {
    enable = true;

    # These are used in addition to resolv.conf
    servers = [
      "8.8.8.8"
      "8.8.4.4"
    ];

    extraConfig = ''
      interface=lo
      bind-interfaces
      listen-address=127.0.0.1
      cache-size=1000

      no-negcache
    '';
  };
}

Firewall

Enable firewall. This blocks all ports for ingress traffic and pings.

(<<nixos-section>>)

{
  networking.firewall = {
    enable = true;
    allowPing = false;

    connectionTrackingModules = [];
    autoLoadConntrackHelpers = false;
  };
}

Services

Locate

Update locate database daily.

{
  services.locate = {
    enable = true;
    localuser = "rasen";
  };
}

Gitolite

{
  services.gitolite = {
    enable = true;
    user = "git";
    adminPubkey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDHH15uiQw3jBbrdlcRb8wOr8KVltuwbHP/JOFAzXFO1l/4QxnKs6Nno939ugULM7Lu0Vx5g6FreuCOa2NMWk5rcjIwOzjrZnHZ7aoAVnE7H9scuz8NGnrWdc1Oq0hmcDxdZrdKdB6CPG/diGWNZy77nLvz5JcX1kPLZENPeApCERwR5SvLecA4Es5JORHz9ssEcf8I7VFpAebfQYDu+VZZvEu03P2+5SXv8+5zjiuxM7qxzqRmv0U8eftii9xgVNC7FaoRBhhM7yKkpbnqX7IeSU3WeVcw4+d1d8b9wD/sFOyGc1xAcvafLaGdgeCQGU729DupRRJokpw6bBRQGH29 rasen@omicron";
  };
}

Syncthing

I use Syncthing to sync my org-mode files to my phone.

{
  services.syncthing = {
    enable = true;
    user = "rasen";
    dataDir = "/home/rasen/.config/syncthing";
    configDir = "/home/rasen/.config/syncthing";
    openDefaultPorts = true;
  };
}

Docker

Backup

I use borg for backups.

(let
  commonOptions = {
    repo = "borg@10.13.0.3:.";
    encryption.mode = "keyfile-blake2";
    encryption.passCommand = "cat /root/secrets/borg";
    compression = "auto,lzma,9";
    doInit = false;
    environment = { BORG_RSH = "ssh -i /root/.ssh/borg"; };
    # UTC timestamp
    dateFormat = "-u +%Y-%m-%dT%H:%M:%S";
  };
in {
  services.borgbackup.jobs."all" = commonOptions // {
    archiveBaseName = "${config.networking.hostName}";
    paths = [
      "/var/lib/gitolite/"
      "/home/rasen/backup/"
      "/home/rasen/.ssh/"
      "/home/rasen/.gnupg/"
      "/home/rasen/.password-store/"
      "/home/rasen/dotfiles/"
      "/home/rasen/org/"

      # Mail
      "/home/rasen/Mail/"
      "/home/rasen/.mbsync/"
    ];
    exclude = [
      # Scanning notmuch takes too much time and doesn't make much
      # sense as it is easily replicable
      "/home/rasen/Mail/.notmuch"
    ];
  };

  # Start backup on boot if missed one while laptop was off
  systemd.timers.borgbackup-job-all.timerConfig = {
    Persistent = true;
  };
})

direnv

flake

{
  xdg.configFile."direnv/lib/use_flake.sh".text = ''
    use_flake() {
      watch_file flake.nix
      watch_file flake.lock
      eval "$(nix print-dev-env --profile "$(direnv_layout_dir)/flake-profile")"
    }
  '';
}

direnv + lorri

direnv allows having per-directory environment configuration. You can think of automatic virtualenv, but it’s more general and supports unloading.

(<<home-manager-section>>)

{
  programs.direnv.enable = true;
  services.lorri.enable = true;
}

Enable Emacs integration. (emacs-lisp)

(use-package direnv
  :config
  (direnv-mode))

VirtualBox

{
  virtualisation.virtualbox.host.enable = true;
  users.extraGroups.vboxusers.members = ["rasen"];
}

Hardware

Do not suspend on AC

{
  services.logind = {
    lidSwitchDocked = "ignore";
    lidSwitchExternalPower = "ignore";
  };
}

Autorandr

Configure EXWM to use autorandr. (emacs-lisp)

(use-package exwm-randr
  :after exwm
  :config
  (setq exwm-workspace-number 2)
  (setq exwm-randr-workspace-output-plist '(0 "eDP-1"
                                              1 "DP-1"
                                              1 "DP-3"))

  (add-hook 'exwm-randr-screen-change-hook
            (defun rasen/autorandr ()
              (interactive)
              (rasen/start-command "autorandr -c" "*autorandr*")))

  (exwm-randr-enable))

(<<home-manager-section>>)

{
  programs.autorandr = {
    enable = true;
    profiles =
      let
        omicron = "00ffffffffffff004d104a14000000001e190104a51d11780ede50a3544c99260f505400000001010101010101010101010101010101cd9180a0c00834703020350026a510000018a47480a0c00834703020350026a510000018000000fe0052584e3439814c513133335a31000000000002410328001200000b010a202000cc";
        work = "00ffffffffffff004d108d1400000000051c0104a52213780ea0f9a95335bd240c5157000000010101010101010101010101010101014dd000a0f0703e803020350058c210000018000000000000000000000000000000000000000000fe00464e564452804c513135364431000000000002410328011200000b010a202000ee";
        home-monitor = "00ffffffffffff0010acc0a042524530031c010380351e78eae245a8554da3260b5054a54b00714f8180a9c0a940d1c0e10001010101a36600a0f0701f80302035000f282100001a000000ff004438565846383148304552420a000000fc0044454c4c205032343135510a20000000fd001d4c1e8c1e000a202020202020018802032ef15390050402071601141f1213272021220306111523091f07830100006d030c001000003c200060030201023a801871382d40582c25000f282100001e011d8018711c1620582c25000f282100009e04740030f2705a80b0588a000f282100001e565e00a0a0a02950302035000f282100001a0000000000000000008a";
        work-monitor = "00ffffffffffff0010acc2d0545741312c1b010380351e78eaad75a9544d9d260f5054a54b008100b300d100714fa9408180d1c00101565e00a0a0a02950302035000e282100001a000000ff004d59334e44374234314157540a000000fc0044454c4c205032343138440a20000000fd0031561d711c000a202020202020010302031bb15090050403020716010611121513141f2065030c001000023a801871382d40582c45000e282100001e011d8018711c1620582c25000e282100009ebf1600a08038134030203a000e282100001a7e3900a080381f4030203a000e282100001a00000000000000000000000000000000000000000000000000000000d8";
      in {
      "omicron" = {
        fingerprint = {
          eDP-1 = omicron;
        };
        config = {
          eDP-1 = {
            enable = true;
            primary = true;
            position = "0x0";
            mode = "3200x1800";
            rate = "60.00";
          };
        };
      };
      "omicron-home" = {
        fingerprint = {
          eDP-1 = omicron;
          DP-1 = home-monitor;
        };
        config = {
          eDP-1 = {
            enable = false;
            primary = true;
            position = "320x2160";
            mode = "3200x1800";
            rate = "60.00";
          };
          DP-1 = {
            enable = true;
            primary = true;
            position = "0x0";
            mode = "3840x2160";
            rate = "60.00";
          };
        };
      };
      "omicron-home-monitor" = {
        fingerprint = {
          DP-1 = home-monitor;
        };
        config = {
          DP-1 = {
            enable = true;
            primary = true;
            position = "0x0";
            mode = "3840x2160";
            rate = "60.00";
          };
        };
      };
    };
  };
}

Screen brightness

xbacklight stopped working recently. acpilight is a drop-in replacement.

{
  hardware.acpilight.enable = true;
  environment.systemPackages = [ pkgs.acpilight ];
  users.extraUsers.rasen.extraGroups = [ "video" ];
}

For Home Manager–managed hosts.

{
  home.packages = [ pkgs.acpilight ];
}

Bind it to keys (emacs-lisp).

(rasen/exwm-input-set-key (kbd "<XF86MonBrightnessUp>")
                          (lambda () (interactive) (rasen/start-command "xbacklight -inc 10")))
(rasen/exwm-input-set-key (kbd "<XF86MonBrightnessDown>")
                          (lambda () (interactive) (rasen/start-command "xbacklight -dec 10")))

Redshift

Redshift adjusts the color temperature of the screen according to the position of the sun.

Blue light blocks melatonin (sleep harmone) secretion, so you feel less sleepy when you stare at computer screen. Redshift blocks some blue light (making screen more red), which should improve melatonin secretion and restore sleepiness (which is a good thing).

{
  services.redshift = {
    enable = true;
  };
  location.provider = "geoclue2";
}

PipeWire

Use PipeWire as audio server.

{
  # PipeWire requires pulseaudio to be disabled.
  hardware.pulseaudio.enable = false;

  security.rtkit.enable = true;
  services.pipewire = {
    enable = true;
    alsa.enable = true;
    alsa.support32Bit = true;
    pulse.enable = true;


    media-session.config.bluez-monitor.rules = [
      {
        # Matches all cards
        matches = [ { "device.name" = "~bluez_card.*"; } ];
        actions = {
          "update-props" = {
            "bluez5.reconnect-profiles" = [ "hfp_hf" "hsp_hs" "a2dp_sink" ];
            # mSBC is not expected to work on all headset + adapter combinations.
            "bluez5.msbc-support" = true;
            # SBC-XQ is not expected to work on all headset + adapter combinations.
            "bluez5.sbc-xq-support" = true;
          };
        };
      }
      {
        matches = [
          # Matches all sources
          { "node.name" = "~bluez_input.*"; }
          # Matches all outputs
          { "node.name" = "~bluez_output.*"; }
        ];
        actions = {
          "node.pause-on-idle" = false;
        };
      }
    ];
  };
}

pavucontrol is PulseAudio Volume Control—a nice utility for controlling pulseaudio settings. (<<home-manager-section>>)

{
  home.packages = [ pkgs.pavucontrol ];
}

Bind volume control commands. (emacs-lisp)

(rasen/exwm-input-set-key (kbd "<XF86AudioMute>")
                          (lambda () (interactive) (rasen/start-command "amixer set Master toggle")))
(rasen/exwm-input-set-key (kbd "<XF86AudioRaiseVolume>")
                          (lambda () (interactive) (rasen/start-command "amixer set Master 2%+")))
(rasen/exwm-input-set-key (kbd "<XF86AudioLowerVolume>")
                          (lambda () (interactive) (rasen/start-command "amixer set Master 2%-")))

Bluetooth

I have a bluetooth headset, so this enables bluetooth audio in NixOS.

(<<nixos-section>>)

{
  hardware.bluetooth.enable = true;
}

ADB

I need to access my Android device. (<<nixos-section>>)

{
  services.udev.packages = [ pkgs.android-udev-rules ];
  programs.adb.enable = true;
  users.users.rasen.extraGroups = ["adbusers"];
}

fwupd

fwupd is a service that allows applications to update firmware. (<<nixos-section>>)

{
  services.fwupd.enable = true;
}

Execute the following command to update firmware.

fwupdmgr get-updates

Browsers

Firefox is default, Chrome for backup.

{
  home.packages = [
    pkgs.firefox
    pkgs.google-chrome
  ];
}

Tridactyl

Tridactyl is a Firefox plugin that provides Vim-like bindings.

Here is my config. (<<tridactylrc>>)

" drop all existing configuration
sanitize tridactyllocal tridactylsync

bind J scrollline -10
bind K scrollline 10
bind j scrollline -2
bind k scrollline 2

Link tridactyl config to the place the tridactyl can find it. (<<home-manager-section>>)

{
  xdg.configFile."tridactyl/tridactylrc".text = ''
    <<tridactylrc>>
  '';
}

Edit text in browser

I use GhostText firefox extension.

atomic-chrome Emacs extension is compatible with it. (emacs-lisp)

(use-package atomic-chrome
  :config
  (setq atomic-chrome-default-major-mode 'markdown-mode)
  (setq atomic-chrome-buffer-open-style 'frame)
  (atomic-chrome-start-server))

Evil-mode

General

(use-package undo-fu
  :commands
  (undo-fu-only-undo
   undo-fu-only-redo))

(use-package evil
  :init
  (setq evil-want-integration t
        evil-want-keybinding nil)
  :custom
  (evil-undo-system 'undo-fu)
  ;; revert to undo-redo when switching back to emacs-28
  ;; (evil-undo-system 'undo-redo)
  :config
  <<evil-config>>
  (evil-mode 1))

Hard way: prohibit usage of keybindings I have more efficient bindings for.

(defmacro rasen/hard-way (key)
  `(lambda () (interactive) (error "Don't use this key! Use %s instead" ,key)))

Swap . and ;.

(general-def 'normal
  ";"    #'evil-repeat
  "."    nil
  "C-;"  #'evil-repeat-pop
  "C-."  nil)

(general-def 'motion
  "."    #'evil-repeat-find-char
  ";"    nil
  "g."   #'goto-last-change
  "g;"   nil)
(s-leader-def
  ";"  #'eval-expression)

Close other window.

(defun rasen/quit-other ()
  (interactive)
  (other-window 1)
  (quit-window))

(s-leader-def
  "q"  #'rasen/quit-other)

Move to beginning/end of line with H and L respectively.

(defun rasen/smart-move-beginning-of-line (arg)
  "Move point back to indentation of beginning of line.

Move point to the first non-whitespace character on this line.
If point is already there, move to the beginning of the line.
Effectively toggle between the first non-whitespace character and
the beginning of the line.

If ARG is not nil or 1, move forward ARG - 1 lines first.  If
point reaches the beginning or end of the buffer, stop there."
  (interactive "^p")
  (setq arg (or arg 1))

  ;; Move lines first
  (when (/= arg 1)
    (let ((line-move-visual nil))
      (forward-line (1- arg))))

  (let ((orig-point (point)))
    (back-to-indentation)
    (when (= orig-point (point))
      (move-beginning-of-line 1))))

(general-def 'motion
  "H"  #'rasen/smart-move-beginning-of-line
  "L"  #'evil-end-of-line)

Save buffer with SPC SPC.

(defun rasen/save-buffer (arg)
  "Save current buffer.  With PREFIX, save all buffers."
  (interactive "P")
  (if arg
      (save-some-buffers)
    (save-buffer)))

(s-leader-def 'normal
  "SPC" #'rasen/save-buffer)
(s-leader-def
  "s-SPC" #'save-some-buffers)

Swap k and j

With workman layout, j is located on qwerty y and k—on qwerty n; thus j is higher than k, and it is not convenient to press lower key for going up. Just swap them.

(general-def 'motion
  "k"    #'evil-next-visual-line
  "j"    #'evil-previous-visual-line
  "gk"   #'evil-next-line
  "gj"   #'evil-previous-line)

(general-def 'operator
  "k"    #'evil-next-line
  "j"    #'evil-previous-line
  "gk"   #'evil-next-visual-line
  "gj"   #'evil-previous-visual-line)

(general-def 'motion
  "C-h"  #'windmove-left
  "C-k"  #'windmove-down
  "C-j"  #'windmove-up
  "C-l"  #'windmove-right)

(general-swap-key nil 'motion
  "C-w j" "C-w k")

evil-numbers

I use Vim’s C-a and C-x (increment/decrement number at point) a lot. evil-numbers provides that functionality for evil.

(use-package evil-numbers
  :after evil
  :general
  ('normal
   "C-a" #'evil-numbers/inc-at-pt
   "C-x" #'evil-numbers/dec-at-pt))

Now, remap C-x to RET. (Because C-x is used for decrementing numbers.)

(general-def 'motion
  "RET" (lookup-key (current-global-map) (kbd "C-x")))
;; Unmap it from magit
(general-def magit-file-mode-map
  "C-x" nil)

evil-collection

evil-collection is a collection of evil bindings for different modes.

(require 'warnings)
(add-to-list 'warning-suppress-types '(evil-collection))

(use-package evil-collection
  :after (evil)
  :init
  (setq evil-want-integration t
        evil-want-keybinding nil)
  :config
  (defun rasen/rotate-keys (_mode mode-keymaps &rest _rest)
    (evil-collection-translate-key 'normal mode-keymaps
      "k" "j"
      "j" "k"
      "gk" "gj"
      "gj" "gk"
      (kbd "M-j") (kbd "M-k")
      (kbd "M-k") (kbd "M-j")
      (kbd "C-j") nil ; used for window-management
      (kbd "C-k") nil ; used for window-management
      "." ";"
      ";" "."))
  (add-hook 'evil-collection-setup-hook #'rasen/rotate-keys)

  (setq evil-collection-mode-list
        '(dired
          compile
          flycheck
          help
          js2-mode
          ;; notmuch bindings aren't that cool and are less efficient than native
          ;; keymap
          ;; notmuch
          magit
          python
          racer
          restclient
          tide
          typescript-mode
          vterm
          which-key
          xref))

  (setq evil-collection-magit-use-y-for-yank t)

  ;; Evilify magit-blame.
  (general-def 'normal magit-blame-read-only-mode-map
    "k"    #'evil-next-visual-line
    "j"    #'evil-previous-visual-line
    "C-k"  #'magit-blame-next-chunk
    "C-j"  #'magit-blame-previous-chunk
    "gk"   #'magit-blame-next-chunk-same-commit
    "gj"   #'magit-blame-previous-chunk-same-commit)

  (general-def magit-blame-read-only-mode-map "SPC" nil) ;; expose my leader

  (require 'evil-collection-magit)
  (general-def
    :states `(,evil-collection-magit-state visual)
    :keymaps 'magit-mode-map
    "j"    #'evil-previous-visual-line
    "k"    #'evil-next-visual-line
    "C-j"  #'magit-section-backward
    "C-k"  #'magit-section-forward
    "gj"   #'magit-section-backward-sibling
    "gk"   #'magit-section-forward-sibling)

  (evil-collection-init))

evil-surrond

(use-package evil-surround
  :config
  (global-evil-surround-mode t))

calc

(use-package calc ; built-in
  :general
  (leader-def 'motion
    "="  #'quick-calc
    "+"  #'calc)
  ('motion
   "g ="  #'quick-calc
   "g +"  #'calc))

Evilify compile mode

(use-package compile ; built-in
  :config
  (setq compilation-scroll-output t))

And evil commands to go to navigate errors.

(leader-def 'motion
  ","  #'previous-error
  "."  #'next-error)
(general-def 'motion
  "M-,"    #'previous-error
  "M-."    #'next-error)

Evilify minibuffer

Not really “evilify.”

(general-def 'minibuffer-local-map
  ;; Finish input with C-e ("e" in Workman is qwerty's "k")
  "C-e"  #'exit-minibuffer)

Evilify shell mode

Default bindings for RET prevent many of my commands from working. Remap RET to C-RET.

(general-def 'shell-mode-map
  "RET"         nil
  "<C-return>"  #'comint-send-input)

lispyville

(use-package lispyville
  :hook
  ((clojure-mode emacs-lisp-mode lisp-mode) . lispyville-mode)
  :config
  (lispyville-set-key-theme
   '(operators
     c-w
     ;; < and >
     slurp/barf-cp
     (atom-movement t)
     commentary
     ;; wrap with M-(, M-[, or M-{
     wrap
     additional
     ;; M-o open below list, M-O open above list
     additional-insert))

  ;; override drag directions
  (lispyville--define-key 'normal
    (kbd "M-j") #'lispyville-drag-backward
    (kbd "M-k") #'lispyville-drag-forward))

Org-mode

General

(use-package org
  :mode ("\\.org$" . org-mode)
  :general
  ("C-c l"  #'org-store-link)
  (s-leader-def
    "c"  #'org-capture
    "a"  #'org-agenda

    "o"  #'org-clock-out
    "l"  #'org-clock-in-last
    "j"  #'org-clock-goto)
  (leader-def 'normal 'org-mode-map
    "t"  #'rasen/org-todo
    "s"  #'org-schedule
    "d"  #'org-deadline
    "i"  #'org-clock-in

    "T"  #'rasen/org-do-today

    "w"  #'rasen/org-refile-hydra/body
    "r"  #'org-archive-subtree-default)
  (leader-def 'motion 'org-agenda-map
    "t"  #'rasen/org-agenda-todo)
  ('normal 'org-mode-map "RET n s" #'org-narrow-to-subtree)
  ('(insert normal) 'org-mode-map
   "C-c ,"  #'org-time-stamp-inactive)
  ('org-mode-map
   ;; tabs
   "M-l" nil
   "M-h" nil)
  :gfhook 'flyspell-mode
  :init
  <<org-init>>
  :config
  <<org-config>>
  )

Do not indent inside tasks

(setq org-adapt-indentation nil)

Do not indent org-babel blocks.

(setq org-edit-src-content-indentation 0)

Do not indent tags.

(setq org-tags-column 0)

When entering tags, offer tags from all agenda files. (This is the closes to global tag tracking I could find.))

(setq org-complete-tags-always-offer-all-agenda-tags t)
(setq org-ellipsis "")

By default, show all of org file. (This can be changed on a per-file basis with #+STARTUP:.)

(setq org-startup-folded nil)

Make the table header float if scrolled out of view.

(setq org-table-header-line-p t)

Hide emphasis markers (asterisks and slashes).

(setq org-hide-emphasis-markers t)

Allow emphasis marks to follow ndash/mdash, and proper quotes.

;; allow ndash/mdash (–, —), and proper quotes (’, “, ”) before/after
;; emphasis markers.
;;
;; (copy-modified from original `org-emphasis-regexp-components' definition)
(org-set-emph-re 'org-emphasis-regexp-components
                 '("-–—[:space:]('\"’“”{"
                   "-–—[:space:].,:!?;'\"’“”)}\\["
                   "[:space:]"
                   "."
                   1))

Open pdfs in external viewer:

(add-to-list 'org-file-apps '("\\.pdf\\'" . "zathura %s"))

Use whitespace-mode in Org (but don’t show too long lines).

(add-hook 'org-mode-hook (lambda ()
                           (setq-local whitespace-style '(face
                                                          tab-mark
                                                          empty
                                                          trailing))
                           (whitespace-mode t)))

My directory for org files.

(setq rasen/org-directory "~/org")

My helper to find all org files in a directory.

(defun rasen/org-files-in-dir (dir)
  (f-files dir
           (lambda (file) (or (f-ext? file "org")
                              (and (f-ext? file "gpg")
                                   (f-ext? (f-no-ext file) "org"))))
           nil))

Package for f-files and f-ext? functions.

(use-package f
  :commands (f-files f-ext? f-no-ext))

Todo

A special function that marks the task as done yesterday if prefix is supplied (useful for habits).

(defun rasen/org-todo (&optional arg)
  "As `org-todo' but calls `org-todo-yesterday' when ARG is non-nil."
  (interactive "P")
  (if arg
      (org-todo-yesterday)
    (org-todo)))

(defun rasen/org-agenda-todo (&optional arg)
  "As `org-agenda-todo' but calls `org-agenda-todo-yesterday' when ARG is non-nil."
  (interactive "P")
  (if arg
      (org-agenda-todo-yesterday)
    (org-agenda-todo)))

Use the following states: TODO NEXT DONE CANCELED WAIT.

(setq-default org-todo-keywords
              '((sequence "TODO(t)" "NEXT(n!)" "|" "DONE(d!)")
                (sequence "BUILD(b!)" "|")
                (sequence "|" "CANCELED(c!)")
                (sequence "WAIT(w!)" "|")))
(setq-default org-use-fast-todo-selection t)

When repeated task is finished, go back to TODO state.

(setq-default org-todo-repeat-to-state "NEXT")

Log state changes to “LOGBOOK” drawer.

(setq-default org-log-into-drawer t)

Save CLOSED timestamp when task is done.

(setq org-log-done t)

Disable force-logging repeated tasks because otherwise you’ll get duplicated log lines.

(setq org-log-repeat nil)

Fontify the whole line for done tasks.

(setq org-fontify-done-headline t)

Import org-expiry for org-expiry-insert-created—this inserts CREATED property.

(require 'org-expiry)
(setq org-expiry-inactive-timestamps t)
(org-expiry-insinuate)

Schedule task for today and mark it NEXT. I use this a lot during daily planning.

(defun rasen/org-do-today (&optional arg)
  "Schedule task for today and mark it NEXT.

If prefix is supplied, select different scheduled time."
  (interactive "P")
  (org-schedule nil (unless arg "."))
  (org-todo "NEXT"))

A command to fold everything except NEXT items.

(defun rasen/org-occur-next ()
  (interactive)
  (let ((org-highlight-sparse-tree-matches nil))
    (org-occur (concat "^" org-outline-regexp " *" "NEXT" "\\>"))))

(general-def '(motion normal) 'org-mode-map
  "z n" #'rasen/org-occur-next)

Disable highlighting for org-occur.

(setq org-highlight-sparse-tree-matches nil)

Highlight projects

Fontify all headlines with the :PROJECT: tag.

(defface rasen/org-project-face
  '((t :weight bold))
  "Face for org-mode projects.")

(font-lock-add-keywords 'org-mode
                        `((,(concat "^\\*+ \\(.*\\) :\\(" org-tag-re ":\\)*PROJECT:.*$")
                           (0 'rasen/org-project-face prepend)))
                        t)

Note that the face is overridden in Color theme section.

Clocking

Remove clocks with 0 duration.

(setq-default org-clock-out-remove-zero-time-clocks t)

Save more last clocks.

(setq-default org-clock-history-length 10)

Capture

I use an extension that adds page url to the title (used for page tracking). Strip it down here

(defun rasen/strip-url-from-title (title)
  (message "stripping: %s" title)
  (replace-regexp-in-string
   " @ [^ ]*$"
   ""
   (replace-regexp-in-string " \\[[^]]*\\]\\[[^]]*\\]$" "" title)))

My capture templates.

(setq rasen/org-refile-file (concat rasen/org-directory "/refile-" system-name ".org"))
(setq org-capture-templates
      `(("u"
         "Task: Read this URL"
         entry
         (file rasen/org-refile-file)
         ,(concat "* TODO %(rasen/strip-url-from-title \"%:description\")\n"
                  ":PROPERTIES:\n"
                  ":CREATED:  %U\n"
                  ":END:\n"
                  "%:link\n")
         :immediate-finish t)

        ("w"
         "Capture web snippet"
         entry
         (file rasen/org-refile-file)
         ,(concat "* %(rasen/strip-url-from-title \"%:description\")\n"
                  ":PROPERTIES:\n"
                  ":CREATED:  %U\n"
                  ":SOURCE_URL: %:link\n"
                  ":END:\n"
                  "#+begin_quote\n"
                  "%i\n"
                  "#+end_quote\n"
                  "%?\n")
         :immediate-finish t)

        ("j" "Journal entry" plain
         (file+datetree+prompt "~/org/journal.org")
         ,(concat
           ;; %U does not work here because timestamp is hijacked by
           ;; %file+datetime+prompt
           "%(format-time-string (org-time-stamp-format t t))"
           "\n"))

        ("t" "todo" entry (file rasen/org-refile-file)
         "* TODO %?\n:PROPERTIES:\n:CREATED:  %U\n:END:\n" :clock-in nil :clock-resume t)

        ("T" "today" entry (file rasen/org-refile-file)
         "* NEXT %?\nSCHEDULED: %t\n:PROPERTIES:\n:CREATED:  %U\n:END:\n" :clock-in nil :clock-resume t)

        ("m" "meeting" entry (file rasen/org-refile-file)
         "* %?   :meeting:\n:PROPERTIES:\n:CREATED:  %U\n:END:\n" :clock-in t :clock-resume t)

        ("n" "note" entry (file rasen/org-refile-file)
         "* %?\n:PROPERTIES:\n:CREATED:  %U\n:END:\n")

        ("l" "link" entry (file rasen/org-refile-file)
         "* %a\n:PROPERTIES:\n:CREATED:  %U\n:END:\n"
         :immediate-finish t)))

(defun rasen/org-capture-link ()
  (interactive)
  (org-capture nil "l"))

Enable org-protocol.

(require 'org-protocol)

%l in org-capture fails with multiline context, so use only the first line as a context.

(setq org-context-in-file-links 1)

org-capture keybindings

Instanly go into insert mode on capture.

(add-hook 'org-capture-mode-hook 'evil-insert-state)
(general-def
  :keymaps 'org-capture-mode-map
  :states 'normal
  "'"      #'org-capture-finalize)
(leader-def 'normal 'org-capture-mode-map
  "w" #'org-capture-refile)

Capturing images

(use-package org-download
  :config
  (setq org-download-method 'directory)
  ;; Do not prepend heading name to the file path
  (setq-default org-download-heading-lvl nil)
  ;; "download" screenshots from clipboard
  (setq org-download-screenshot-method "xclip -selection clipboard -t image/png -o > %s")

  ;; Prefix downloaded files with tsid.
  (setq org-download-file-format-function
        (defun rasen/org-download-file-format (filename)
          (concat (rasen/tsid) "-" filename))))

datetree

An interactive command to jump to a specific datetree entry in the current buffer. I used this as a lightweight way to keep a journal.

;; adapted from org-capture module

(defun rasen/org-datetree-entry (arg)
  "Add a date-tree entry in the current file. Interactive version."
  (interactive "P")
  (let ((d (calendar-gregorian-from-absolute
            (if arg
                ;; Current date, possibly corrected for late night
                ;; workers.
                (org-today)
              (progn;; Prompt for date.
                (let ((prompt-time (org-read-date
                                    nil t nil "Date for tree entry:")))
                  (cond ((and (or (not (boundp 'org-time-was-given))
                                  (not org-time-was-given))
                              (not (= (time-to-days prompt-time) (org-today))))
                         ;; Use 00:00 when no time is given for another
                         ;; date than today?
                         (apply #'encode-time 0 0
                                org-extend-today-until
                                (cl-cdddr (decode-time prompt-time))))
                        ((string-match "\\([^ ]+\\)--?[^ ]+[ ]+\\(.*\\)"
                                       org-read-date-final-answer)
                         ;; Replace any time range by its start.
                         (apply #'encode-time
                                (org-read-date-analyze
                                 (replace-match "\\1 \\2" nil nil
                                                org-read-date-final-answer)
                                 prompt-time (decode-time prompt-time))))
                        (t prompt-time))
                  (time-to-days prompt-time)))))))
    (org-datetree-find-date-create d)))

cliplink

(use-package org-cliplink
  :config
  ;; I don't like titles clipping at 80. I'd rather get the full title
  ;; and edit it manually.
  (setq org-cliplink-max-length 200))

Refile

(defun rasen/org-refile-files ()
  (rasen/org-files-in-dir rasen/org-directory))

;; non-nil values work bad with ivy
(setq-default org-refile-use-outline-path 'file)
(setq-default org-outline-path-complete-in-steps nil)

;; Allow refiling to projects and honeypots only. The rest of refiling
;; is handled by hydra.
(setq org-refile-targets
      '((org-agenda-files . (:tag . "honeypot"))
        (org-agenda-files . (:tag . "PROJECT"))))

Setting org-refile-use-outline-path to ~’file~ prepends the file names to refile targets but also has a side effect of allowing refiling to the top level of these files. Patch org-refile-get-targets to disable that.

(el-patch-defun org-refile-get-targets (&optional default-buffer)
  "Produce a table with refile targets."
  (let ((case-fold-search nil)
        ;; otherwise org confuses "TODO" as a kw and "Todo" as a word
        (entries (or org-refile-targets '((nil . (:level . 1)))))
        targets tgs files desc descre)
    (message "Getting targets...")
    (with-current-buffer (or default-buffer (current-buffer))
      (dolist (entry entries)
        (setq files (car entry) desc (cdr entry))
        (cond
         ((null files) (setq files (list (current-buffer))))
         ((eq files 'org-agenda-files)
          (setq files (org-agenda-files 'unrestricted)))
         ((and (symbolp files) (fboundp files))
          (setq files (funcall files)))
         ((and (symbolp files) (boundp files))
          (setq files (symbol-value files))))
        (when (stringp files) (setq files (list files)))
        (cond
         ((eq (car desc) :tag)
          (setq descre (concat "^\\*+[ \t]+.*?:" (regexp-quote (cdr desc)) ":")))
         ((eq (car desc) :todo)
          (setq descre (concat "^\\*+[ \t]+" (regexp-quote (cdr desc)) "[ \t]")))
         ((eq (car desc) :regexp)
          (setq descre (cdr desc)))
         ((eq (car desc) :level)
          (setq descre (concat "^\\*\\{" (number-to-string
                                          (if org-odd-levels-only
                                              (1- (* 2 (cdr desc)))
                                            (cdr desc)))
                               "\\}[ \t]")))
         ((eq (car desc) :maxlevel)
          (setq descre (concat "^\\*\\{1," (number-to-string
                                            (if org-odd-levels-only
                                                (1- (* 2 (cdr desc)))
                                              (cdr desc)))
                               "\\}[ \t]")))
         (t (error "Bad refiling target description %s" desc)))
        (dolist (f files)
          (with-current-buffer (if (bufferp f) f (org-get-agenda-file-buffer f))
            (or
             (setq tgs (org-refile-cache-get (buffer-file-name) descre))
             (progn
               (when (bufferp f)
                 (setq f (buffer-file-name (buffer-base-buffer f))))
               (setq f (and f (expand-file-name f)))
               (el-patch-remove
                 (when (eq org-refile-use-outline-path 'file)
                   (push (list (and f (file-name-nondirectory f)) f nil nil) tgs)))
               (when (eq org-refile-use-outline-path 'buffer-name)
                 (push (list (buffer-name (buffer-base-buffer)) f nil nil) tgs))
               (when (eq org-refile-use-outline-path 'full-file-path)
                 (push (list (and (buffer-file-name (buffer-base-buffer))
                                  (file-truename (buffer-file-name (buffer-base-buffer))))
                             f nil nil) tgs))
               (org-with-wide-buffer
                (goto-char (point-min))
                (setq org-outline-path-cache nil)
                (while (re-search-forward descre nil t)
                  (beginning-of-line)
                  (let ((case-fold-search nil))
                    (looking-at org-complex-heading-regexp))
                  (let ((begin (point))
                        (heading (match-string-no-properties 4)))
                    (unless (or (and
                                 org-refile-target-verify-function
                                 (not
                                  (funcall org-refile-target-verify-function)))
                                (not heading))
                      (let ((re (format org-complex-heading-regexp-format
                                        (regexp-quote heading)))
                            (target
                             (if (not org-refile-use-outline-path) heading
                               (mapconcat
                                #'identity
                                (append
                                 (pcase org-refile-use-outline-path
                                   (`file (list
                                           (and (buffer-file-name (buffer-base-buffer))
                                                (file-name-nondirectory
                                                 (buffer-file-name (buffer-base-buffer))))))
                                   (`full-file-path
                                    (list (buffer-file-name
                                           (buffer-base-buffer))))
                                   (`buffer-name
                                    (list (buffer-name
                                           (buffer-base-buffer))))
                                   (_ nil))
                                 (mapcar (lambda (s) (replace-regexp-in-string
                                                      "/" "\\/" s nil t))
                                         (org-get-outline-path t t)))
                                "/"))))
                        (push (list target f re (org-refile-marker (point)))
                              tgs)))
                    (when (= (point) begin)
                      ;; Verification function has not moved point.
                      (end-of-line)))))))
            (when org-refile-use-cache
              (org-refile-cache-put tgs (buffer-file-name) descre))
            (setq targets (append tgs targets))))))
    (message "Getting targets...done")
    (delete-dups (nreverse targets))))

Refiling with hydras

Adapted from Fast refiling in org-mode with hydras | Josh Moller-Mara. Extended to support refiling by outline path.

(require 'hydra)

(defun rasen/concat (sequence separator)
  (mapconcat 'identity sequence separator))

(defun rasen/org-refile-exact (file path &optional arg)
  "Refile to a specific location.

With a `C-u' ARG argument, jump to that location."
  (let* ((pos (and path (org-find-olp (cons file path))))
         (rfloc (list (rasen/concat path "/") file nil pos)))

    (if (and (eq major-mode 'org-agenda-mode)
             ;; Don't use org-agenda-refile if we're just jumping
             (not (and arg (listp arg))))
        (org-agenda-refile nil rfloc)
      (org-refile arg nil rfloc))))

(defun rasen/refile (file path &optional arg)
  "Refile to PATH in FILE. Clean up org-capture if it's activated.

With a `C-u` ARG, just jump to the headline."
  (interactive "P")
  (let ((is-capturing (and (boundp 'org-capture-mode) org-capture-mode)))
    (cond
     ((and arg (listp arg))             ;Are we jumping?
      (rasen/org-refile-exact file path arg))

     ;; Are we in org-capture-mode?
     (is-capturing
      (rasen/org-capture-refile-but-with-args file path arg))

     (t
      (rasen/org-refile-exact file path arg)))

    (when (or arg is-capturing)
      (setq hydra-deactivate t))))

(defun rasen/org-capture-refile-but-with-args (file path &optional arg)
  "Copied from `org-capture-refile' since it doesn't allow passing arguments. This does."
  (unless (eq (org-capture-get :type 'local) 'entry)
    (error
     "Refiling from a capture buffer makes only sense for `entry'-type templates"))
  (let ((pos (point))
        (base (buffer-base-buffer (current-buffer)))
        (org-capture-is-refiling t)
        (kill-buffer (org-capture-get :kill-buffer 'local)))
    (org-capture-put :kill-buffer nil)
    (org-capture-finalize)
    (save-window-excursion
      (with-current-buffer (or base (current-buffer))
        (org-with-wide-buffer
         (goto-char pos)
         (rasen/org-refile-exact file path arg))))
    (when kill-buffer (kill-buffer base))))

(defmacro rasen/make-refile-hydra (hydraname name &rest options)
  (declare (indent defun))
  `(defhydra ,hydraname (:foreign-keys run :exit t)
     ,name

     ,@(mapcar (lambda (x)
                 (let ((key  (nth 0 x))
                       (name (nth 1 x))
                       (file (nth 2 x))
                       (path (nthcdr 3 x)))
                   (if (stringp file)
                       `(,key (rasen/refile ,file ',path current-prefix-arg) ,name)
                     `(,key ,file ,name))))
               options)

     ("q" nil "cancel")))

(rasen/make-refile-hydra rasen/org-refile-hydra-someday "Someday"
  ("l"  "Latte List" "~/org/plan.org"       "Someday/Maybe" "Latte List")
  ("u"  "Uniorg"     "~/org/plan.org"       "Someday/Maybe" "Uniorg")
  ("n"  "Notes"      "~/org/plan.org"       "Someday/Maybe" "Notes")
  ("a"  "Writing"    "~/org/plan.org"       "Someday/Maybe" "Writing / website")
  ("c"  "Computer"   "~/org/plan.org"       "Someday/Maybe" "Computer")
  ("r"  "Reading"    "~/org/plan.org"       "Someday/Maybe" "Reading")
  ("s"  "Misc"       "~/org/plan.org"       "Someday/Maybe" "Misc"))

(rasen/make-refile-hydra rasen/org-refile-hydra "Refile"
  ("p"  "Projects"   "~/org/plan.org"       "Projects")

  ("n"  "To do"      "~/org/plan.org"       "To do")
  ("s"  "Someday"    rasen/org-refile-hydra-someday/body)
  ("t"  "Tickler"    "~/org/plan.org"       "Tickler")
  ("W"  "Waiting"    "~/org/plan.org"       "Waiting")
  ("r"  "Resources"  "~/org/resources.org")
  ("b"  "Books"      "~/org/books.org")

  ("w" "select"
   (if (eq major-mode 'org-agenda-mode)
       (org-agenda-refile current-prefix-arg)
     (org-refile current-prefix-arg))))

(leader-def 'normal 'org-mode-map         "w" #'rasen/org-refile-hydra/body)
(leader-def 'motion 'org-agenda-mode-map  "w" #'rasen/org-refile-hydra/body)
(leader-def 'normal 'org-capture-mode-map "w" #'rasen/org-refile-hydra/body)

Refile last but before archive

I like my archive sibling to be the last child. The default org-refile ignores that at refiles all entries after archive.

So here is a little patch to refile before archive sibling if it is present.

(defun rasen/org-goto-last-child ()
  "Goto the last child, even if it is invisible.
Return t when a child was found.  Otherwise don't move point and return nil."
  (when (org-goto-first-child)
    (while (org-goto-sibling))
    t))

(defun rasen/org-goto-last-archive ()
  (and (rasen/org-goto-last-child)
       (string= org-archive-sibling-heading (org-get-heading t t t t))
       (member org-archive-tag (org-get-tags))
       (point)))

(require 'org-archive) ; for org-archive-sibling-heading

(el-patch-feature org)
(el-patch-defun org-refile (&optional arg default-buffer rfloc msg)
  "Move the entry or entries at point to another heading.

The list of target headings is compiled using the information in
`org-refile-targets', which see.

At the target location, the entry is filed as a subitem of the
target heading.  Depending on `org-reverse-note-order', the new
subitem will either be the first or the last subitem.

If there is an active region, all entries in that region will be
refiled.  However, the region must fulfill the requirement that
the first heading sets the top-level of the moved text.

With a `\\[universal-argument]' ARG, the command will only visit the target \
location
and not actually move anything.

With a prefix `\\[universal-argument] \\[universal-argument]', go to the \
location where the last
refiling operation has put the subtree.

With a numeric prefix argument of `2', refile to the running clock.

With a numeric prefix argument of `3', emulate `org-refile-keep'
being set to t and copy to the target location, don't move it.
Beware that keeping refiled entries may result in duplicated ID
properties.

RFLOC can be a refile location obtained in a different way.  It
should be a list with the following 4 elements:

1. Name - an identifier for the refile location, typically the
headline text
2. File - the file the refile location is in
3. nil - used for generating refile location candidates, not
needed when passing RFLOC
4. Position - the position in the specified file of the
headline to refile under

MSG is a string to replace \"Refile\" in the default prompt with
another verb.  E.g. `org-copy' sets this parameter to \"Copy\".

See also `org-refile-use-outline-path'.

If you are using target caching (see `org-refile-use-cache'), you
have to clear the target cache in order to find new targets.
This can be done with a `0' prefix (`C-0 C-c C-w') or a triple
prefix argument (`C-u C-u C-u C-c C-w')."
  (interactive "P")
  (if (member arg '(0 (64)))
      (org-refile-cache-clear)
    (let* ((actionmsg (cond (msg msg)
                            ((equal arg 3) "Refile (and keep)")
                            (t "Refile")))
           (regionp (org-region-active-p))
           (region-start (and regionp (region-beginning)))
           (region-end (and regionp (region-end)))
           (org-refile-keep (if (equal arg 3) t org-refile-keep))
           pos it nbuf file level reversed)
      (setq last-command nil)
      (when regionp
        (goto-char region-start)
        (beginning-of-line)
        (setq region-start (point))
        (unless (or (org-kill-is-subtree-p
                     (buffer-substring region-start region-end))
                    (prog1 org-refile-active-region-within-subtree
                      (let ((s (point-at-eol)))
                        (org-toggle-heading)
                        (setq region-end (+ (- (point-at-eol) s) region-end)))))
          (user-error "The region is not a (sequence of) subtree(s)")))
      (if (equal arg '(16))
          (org-refile-goto-last-stored)
        (when (or
               (and (equal arg 2)
                    org-clock-hd-marker (marker-buffer org-clock-hd-marker)
                    (prog1
                        (setq it (list (or org-clock-heading "running clock")
                                       (buffer-file-name
                                        (marker-buffer org-clock-hd-marker))
                                       ""
                                       (marker-position org-clock-hd-marker)))
                      (setq arg nil)))
               (setq it
                     (or rfloc
                         (let (heading-text)
                           (save-excursion
                             (unless (and arg (listp arg))
                               (org-back-to-heading t)
                               (setq heading-text
                                     (replace-regexp-in-string
                                      org-link-bracket-re
                                      "\\2"
                                      (or (nth 4 (org-heading-components))
                                          ""))))
                             (org-refile-get-location
                              (cond ((and arg (listp arg)) "Goto")
                                    (regionp (concat actionmsg " region to"))
                                    (t (concat actionmsg " subtree \""
                                               heading-text "\" to")))
                              default-buffer
                              (and (not (equal '(4) arg))
                                   org-refile-allow-creating-parent-nodes)))))))
          (setq file (nth 1 it)
                pos (nth 3 it))
          (when (and (not arg)
                     pos
                     (equal (buffer-file-name) file)
                     (if regionp
                         (and (>= pos region-start)
                              (<= pos region-end))
                       (and (>= pos (point))
                            (< pos (save-excursion
                                     (org-end-of-subtree t t))))))
            (error "Cannot refile to position inside the tree or region"))
          (setq nbuf (or (find-buffer-visiting file)
                         (find-file-noselect file)))
          (if (and arg (not (equal arg 3)))
              (progn
                (pop-to-buffer-same-window nbuf)
                (goto-char (cond (pos)
                                 ((org-notes-order-reversed-p) (point-min))
                                 (t (point-max))))
                (org-show-context 'org-goto))
            (if regionp
                (progn
                  (org-kill-new (buffer-substring region-start region-end))
                  (org-save-markers-in-region region-start region-end))
              (org-copy-subtree 1 nil t))
            (with-current-buffer (setq nbuf (or (find-buffer-visiting file)
                                                (find-file-noselect file)))
              (setq reversed (org-notes-order-reversed-p))
              (org-with-wide-buffer
               (if pos
                   (progn
                     (goto-char pos)
                     (setq level (org-get-valid-level (funcall outline-level) 1))
                     (goto-char
                      (if reversed
                          (or (outline-next-heading) (point-max))
                        (or (el-patch-add (save-excursion (rasen/org-goto-last-archive)))
                            (save-excursion (org-get-next-sibling))
                            (org-end-of-subtree t t)
                            (point-max)))))
                 (setq level 1)
                 (if (not reversed)
                     (goto-char (point-max))
                   (goto-char (point-min))
                   (or (outline-next-heading) (goto-char (point-max)))))
               (unless (bolp) (newline))
               (org-paste-subtree level nil nil t)
               (cond
                ((not org-log-refile))
                (regionp
                 (org-map-region
                  (lambda nil
                    (org-add-log-setup 'refile nil nil 'time))
                  (point)
                  (+
                   (point)
                   (- region-end region-start))))
                (t
                 (org-add-log-setup 'refile nil nil org-log-refile)))
               (and org-auto-align-tags
                    (let ((org-loop-over-headlines-in-active-region nil))
                      (org-align-tags)))
               (let ((bookmark-name (plist-get org-bookmark-names-plist
                                               :last-refile)))
                 (when bookmark-name
                   (with-demoted-errors
                       (bookmark-set bookmark-name))))
               ;; If we are refiling for capture, make sure that the
               ;; last-capture pointers point here
               (when (bound-and-true-p org-capture-is-refiling)
                 (let ((bookmark-name (plist-get org-bookmark-names-plist
                                                 :last-capture-marker)))
                   (when bookmark-name
                     (with-demoted-errors
                         (bookmark-set bookmark-name))))
                 (move-marker org-capture-last-stored-marker (point)))
               (when (fboundp 'deactivate-mark) (deactivate-mark))
               (run-hooks 'org-after-refile-insert-hook)))
            (unless org-refile-keep
              (if regionp
                  (delete-region (point) (+ (point) (- region-end region-start)))
                (org-preserve-local-variables
                 (delete-region
                  (and (org-back-to-heading t) (point))
                  (min (1+ (buffer-size)) (org-end-of-subtree t t) (point))))))
            (when (featurep 'org-inlinetask)
              (org-inlinetask-remove-END-maybe))
            (setq org-markers-to-move nil)
            (message "%s to \"%s\" in file %s: done" actionmsg
                     (car it)
                     file)))))))

Archive

(setq-default org-archive-default-command 'org-archive-to-archive-sibling)

Agenda

Set my org files location.

(setq org-directory "~/org"
      org-default-notes-file rasen/org-refile-file
      org-agenda-files (rasen/org-files-in-dir "~/org"))

Show agenda as the only window and restore window layout on quit (so that agenda does not mess up with my layout).

(setq org-agenda-window-setup 'only-window)
(setq org-agenda-restore-windows-after-quit t)

Configure my agenda view.

(setq org-agenda-span 6)

Configure stuck projects.

(add-to-list 'org-tags-exclude-from-inheritance "PROJECT")
(setq org-stuck-projects
      '("+PROJECT/-TODO-DONE-CANCELED-WAIT" ("NEXT" "WAIT") nil ""))

Do not align tags in agenda.

(setq org-agenda-tags-column 0)

Do not show project tags.

(setq org-agenda-hide-tags-regexp "PROJECT\\|fc\\|suspended")
(use-package org-super-agenda
  :config
  (general-def org-super-agenda-header-map
    "k" #'org-agenda-next-line
    "j" #'org-agenda-previous-line)

  ;; cache agenda, and only rebuild on request
  (setq org-agenda-sticky t)

  (setq org-agenda-block-separator nil
        org-agenda-compact-blocks t
        org-agenda-time-grid '((daily today require-timed) (1000 1100 1200 1300 1400 1500 1600 1700 1800 1900 2000 2100 2200 2300) "......"  "----------------")
        ;; org-agenda-time-grid '((daily today require-timed) nil "......"  "----------------")
        )

  (setq org-agenda-custom-commands
        '(("o" "Overview"
           ((agenda "" (;; start from yesterday
                        (org-agenda-start-day "-1d")

                        ;; show 6 days
                        (org-agenda-span 6)

                        ;; show closed items
                        (org-agenda-show-log t)
                        (org-agenda-log-mode-items '(closed))

                        ;; Show habits on each day. (Useful if today
                        ;; is closed but you still want to see the
                        ;; habit graph.)
                        ;; (org-habit-show-habits-only-for-today nil)

                        (org-super-agenda-groups
                         '(;; 1. time-grid
                           ;; 2. scheduled today
                           ;; 3. deadline today
                           ;; 4. habits
                           ;; 5. rest (anniversaries, etc.)
                           ;; 6. deadline reminders
                           (:name none ;; habits
                            :habit t
                            :order 4)
                           (:name none ;; time-grid
                            :time-grid t
                            :order 1)
                           (:name none ;; don't show closed items—they are still seen in the log
                            :discard (:todo ("DONE" "CANCELED")))
                           (:name none ;; scheduled today
                            :scheduled today
                            :order 2)
                           (:name none ;; deadline today
                            :deadline today
                            :order 3)
                           (:name none ;; deadline reminders
                            :deadline t
                            :scheduled t
                            :order 6)
                           (:name none ;; everything else
                            :anything t
                            :order 5)))))
            (alltodo "" ((org-agenda-overriding-header "")
                         (org-super-agenda-groups
                          '(;; drop scheduled items—they are shown in
                            ;; agenda view
                            (:discard (:scheduled t))

                            ;; 8. Next items
                            ;; 9. Active projects (NEXT/WAIT)
                            ;; 11. Active books (NEXT)
                            ;; 12. Waiting for items (WAIT)
                            (:name "Books"
                             :and (:category "books"
                                   :todo "NEXT")
                             :order 11)
                            (:name "Projects"
                             :and (:tag "PROJECT"
                                   :todo "NEXT")
                             :and (:tag "PROJECT"
                                   :todo "WAIT")
                             :order 9)
                            (:name "Next"
                             :todo "NEXT"
                             :order 8)
                            (:todo "WAIT"
                             :order 12)
                            (:discard (:anything t))))))

            ;; inbox items (they must have a “CREATED” property to be considered an item)
            (search "+{:CREATED:}" ((org-agenda-files (mapcar (lambda (x) (concat rasen/org-directory "/" x))
                                                              '("refile-omicron.org"
                                                                "orgzly.org")))
                                    (org-agenda-overriding-header "")
                                    (org-super-agenda-groups
                                     '((:name "Inbox"
                                        :auto-category t
                                        :anything t)))))))

          ("N" tags "+TODO=\"NEXT\"-PROJECT|+TODO=\"WAIT\"-PROJECT")
          ("n" todo-tree "NEXT")
          ("p" "active projects" tags "+PROJECT/+NEXT")
          ("P" "all projects" tags "+PROJECT/-DONE-CANCELED")))
  (org-super-agenda-mode))

Allow NEXT projects to stuck

org-agenda-list-stuck-projects marks project as unstuck if its header matches any of specified keywords. This makes all NEXT projects automatically unstuck.

Fix this by skipping the first line (project title) in org-agenda-skip-function.

(el-patch-feature org-agenda)
(el-patch-defun org-agenda-list-stuck-projects (&rest ignore)
  "Create agenda view for projects that are stuck.
Stuck projects are project that have no next actions.  For the definitions
of what a project is and how to check if it stuck, customize the variable
`org-stuck-projects'."
  (interactive)
  (let* ((org-agenda-overriding-header
          (or org-agenda-overriding-header "List of stuck projects: "))
         (matcher (nth 0 org-stuck-projects))
         (todo (nth 1 org-stuck-projects))
         (tags (nth 2 org-stuck-projects))
         (gen-re (org-string-nw-p (nth 3 org-stuck-projects)))
         (todo-wds
          (if (not (member "*" todo)) todo
            (org-agenda-prepare-buffers (org-agenda-files nil 'ifmode))
            (org-delete-all org-done-keywords-for-agenda
                            (copy-sequence org-todo-keywords-for-agenda))))
         (todo-re (and todo
                       (format "^\\*+[ \t]+\\(%s\\)\\>"
                               (mapconcat #'identity todo-wds "\\|"))))
         (tags-re (cond ((null tags) nil)
                        ((member "*" tags) org-tag-line-re)
                        (tags
                         (let ((other-tags (format "\\(?:%s:\\)*" org-tag-re)))
                           (concat org-outline-regexp-bol
                                   ".*?[ \t]:"
                                   other-tags
                                   (regexp-opt tags t)
                                   ":" other-tags "[ \t]*$")))
                        (t nil)))
         (re-list (delq nil (list todo-re tags-re gen-re)))
         (skip-re
          (if (null re-list)
              (error "Missing information to identify unstuck projects")
            (mapconcat #'identity re-list "\\|")))
         (org-agenda-skip-function
          ;; Skip entry if `org-agenda-skip-regexp' matches anywhere
          ;; in the subtree.
          `(lambda ()
             (and (save-excursion
                    (let ((case-fold-search nil)
                          (el-patch-add (subtree-end (save-excursion (org-end-of-subtree t)))))
                      (el-patch-add (forward-line))
                      (re-search-forward
                       ,skip-re
                       (el-patch-swap
                         (save-excursion (org-end-of-subtree t))
                         subtree-end)
                       t)))
                  (progn (outline-next-heading) (point))))))
    (org-tags-view nil matcher)
    (setq org-agenda-buffer-name (buffer-name))
    (with-current-buffer org-agenda-buffer-name
      (setq org-agenda-redo-command
            `(org-agenda-list-stuck-projects ,current-prefix-arg))
      (let ((inhibit-read-only t))
        (add-text-properties
         (point-min) (point-max)
         `(org-redo-cmd ,org-agenda-redo-command))))))

Babel

Code-highlight (fontify) org-babel (#+begin_src) blocks.

(setq org-src-fontify-natively t)

Do not confirm evaluation for emacs-lisp.

(defun rasen/org-confirm-babel-evaluate (lang body)
  (not (member lang '("emacs-lisp"))))

(setq org-confirm-babel-evaluate 'rasen/org-confirm-babel-evaluate)

Load more languages:

(org-babel-do-load-languages 'org-babel-load-languages
                             '((shell . t)))

Latex preview

(setq org-latex-packages-alist
      '(;; Use mhchem for chemistry formulas
        ("" "mhchem" t)
        ;; Use tikz-cd for category theory formulas
        ("" "tikz-cd" t)))

;; Store all preview in external directory
(setq org-preview-latex-image-directory (expand-file-name "cache/ltximg/" user-emacs-directory))

;; Use imagemagick instead of dvipng (dvipng does not work with tikz)
(setq org-preview-latex-default-process 'imagemagick)

;; Enable latex preview by default
(setq org-startup-with-latex-preview t)

;; The latest imagemagick incorrectly trims images when density is
;; odd. My density is 277. Hard-code -density option to the closest
;; even number, so latex images are properly trimmed.
(plist-put (alist-get 'imagemagick org-preview-latex-process-alist)
           :image-converter '("convert -density 278 -trim -antialias %f -quality 100 %O"))

Image preview

;; Scale inline images by default
(setq org-image-actual-width '(800))
;; Show inline images by default
(setq org-startup-with-inline-images t)

Export

Fix exporting for confluence.

ox-confluence has an issue with verbatim—it doesn’t redefine verbatim translation, so org-ascii-verbatim is used. The following makes org-ascii-verbatim produce proper confluence fixed-width block.

(add-to-list 'org-modules 'ox-confluence)
(setq org-ascii-verbatim-format "\{\{%s\}\}")

(defun rasen/org-ox-confluence ()
  (interactive)
  (save-excursion
    (save-restriction
      (when (region-active-p)
        (narrow-to-region (region-beginning) (region-end)))

      (goto-char (point-min))
      (perform-replace "-" "&#45;"
                       nil              ; replace all
                       nil              ; not regex
                       nil              ; replace on word boundaries
                       )
      (goto-char (point-min))
      (perform-replace "_" "&#95;"
                       nil              ; replace all
                       nil              ; not regex
                       nil              ; replace on word boundaries
                       )
      (goto-char (point-min))
      (perform-replace "{" "&#123;"
                       nil              ; replace all
                       nil              ; not regex
                       nil              ; replace on word boundaries
                       )
      (goto-char (point-min))
      (perform-replace "}" "&#125;"
                       nil              ; replace all
                       nil              ; not regex
                       nil              ; replace on word boundaries
                       )
      (goto-char (point-min))
      (perform-replace "[" "&#91;"
                       nil              ; replace all
                       nil              ; not regex
                       nil              ; replace on word boundaries
                       )
      (goto-char (point-min))
      (perform-replace "]" "&#93;"
                       nil              ; replace all
                       nil              ; not regex
                       nil              ; replace on word boundaries
                       ))))

(setq rasen/confluence-block-known-languages
      '("actionscript3"
        "applescript"
        "bash"
        "c#"
        "cpp"
        "css"
        "coldfusion"
        "delphi"
        "diff"
        "erl" ; Erlang
        "groovy"
        "xml" ; and HTML
        "java"
        "jfx" ; Java FX
        "js"
        "php"
        "perl"
        "text"
        "powershell"
        "py"
        "ruby"
        "sql"
        "sass"
        "scala"
        "vb" ; Visual Basic
        "yml"))

(require 'ox-confluence)
(el-patch-defun org-confluence--block (language theme contents)
  (concat (el-patch-swap "\{code:theme=" "\{code") (el-patch-remove theme)
          (when (el-patch-swap language (member language rasen/confluence-block-known-languages)) (format (el-patch-swap "|language=%s" ":language=%s") language))
          "}\n"
          contents
          "\{code\}\n"))

Crypt

Allow encrypted entries in org files.

(require 'org-crypt)
(org-crypt-use-before-save-magic)
(add-to-list 'org-tags-exclude-from-inheritance "crypt")
(setq org-crypt-key "rasen.dubi@gmail.com")
(add-hook 'org-babel-pre-tangle-hook 'org-decrypt-entries t)

org-list

I always use either - or 1. style for lists. Make org-cycle-list-bullet ignore the rest of styles (*, +, 1)), so switching between ordered/unordered list is always one command away:

(el-patch-defun org-cycle-list-bullet (&optional which)
  "Cycle through the different itemize/enumerate bullets.
This cycle the entire list level through the sequence:

   `-'  ->  `+'  ->  `*'  ->  `1.'  ->  `1)'

If WHICH is a valid string, use that as the new bullet.  If WHICH
is an integer, 0 means `-', 1 means `+' etc.  If WHICH is
`previous', cycle backwards."
  (interactive "P")
  (unless (org-at-item-p) (error "Not at an item"))
  (save-excursion
    (beginning-of-line)
    (let* ((struct (org-list-struct))
           (parents (org-list-parents-alist struct))
           (prevs (org-list-prevs-alist struct))
           (list-beg (org-list-get-first-item (point) struct prevs))
           (bullet (org-list-get-bullet list-beg struct))
           (alpha-p (org-list-use-alpha-bul-p list-beg struct prevs))
           (case-fold-search nil)
           (current (cond
                     ((string-match "[a-z]\\." bullet) "a.")
                     ((string-match "[a-z])" bullet) "a)")
                     ((string-match "[A-Z]\\." bullet) "A.")
                     ((string-match "[A-Z])" bullet) "A)")
                     ((string-match "\\." bullet) "1.")
                     ((string-match ")" bullet) "1)")
                     (t (org-trim bullet))))
           ;; Compute list of possible bullets, depending on context.
           (bullet-list
            (append '("-" (el-patch-remove "+"))
                    (el-patch-remove
                      ;; *-bullets are not allowed at column 0.
                      (unless (looking-at "\\S-") '("*")))
                    ;; Description items cannot be numbered.
                    (unless (or (eq org-plain-list-ordered-item-terminator ?\))
                                (org-at-item-description-p))
                      '("1."))
                    (el-patch-remove
                      (unless (or (eq org-plain-list-ordered-item-terminator ?.)
                                  (org-at-item-description-p))
                        '("1)"))
                      (unless (or (not alpha-p)
                                  (eq org-plain-list-ordered-item-terminator ?\))
                                  (org-at-item-description-p))
                        '("a." "A."))
                      (unless (or (not alpha-p)
                                  (eq org-plain-list-ordered-item-terminator ?.)
                                  (org-at-item-description-p))
                        '("a)" "A)")))))
           (len (length bullet-list))
           (item-index (- len (length (member current bullet-list))))
           (get-value (lambda (index) (nth (mod index len) bullet-list)))
           (new (cond
                 ((member which bullet-list) which)
                 ((numberp which) (funcall get-value which))
                 ((eq 'previous which) (funcall get-value (1- item-index)))
                 (t (funcall get-value (1+ item-index))))))
      ;; Use a short variation of `org-list-write-struct' as there's
      ;; no need to go through all the steps.
      (let ((old-struct (copy-tree struct)))
        (org-list-set-bullet list-beg struct (org-list-bullet-string new))
        (org-list-struct-fix-bul struct prevs)
        (org-list-struct-fix-ind struct parents)
        (org-list-struct-apply-struct struct old-struct)))))

org-checklist

Setting property RESET_CHECK_BOXES on a periodic task to t will clear all checkboxes when the task is closed.

(require 'org-checklist)

Habits

(require 'org-habit)
(setq org-habit-show-habits-only-for-today t)
(setq org-habit-preceding-days 25)
(setq org-habit-following-days 3)

adaptive-wrap

Better line wrapping. (Use proper wrap-prefix in lists, etc.)

(use-package adaptive-wrap
  :config
  (add-hook 'org-mode-hook #'adaptive-wrap-prefix-mode))

org-id

Use timestamps as ids.

(setq org-id-method 'ts)

Configure org-store-link to prefer ids for headlines.

(setq org-id-link-to-org-use-id 'create-if-interactive)

Override org-id-new to use rasen/tsid as ids. (They have less precision but that’s enough for me.) By using tsid, I can easily switch headline-nodes to file-nodes—id becomes the filename.

(el-patch-defun org-id-new (&optional prefix)
  "Create a new globally unique ID.

An ID consists of two parts separated by a colon:
- a prefix
- a unique part that will be created according to `org-id-method'.

PREFIX can specify the prefix, the default is given by the variable
`org-id-prefix'.  However, if PREFIX is the symbol `none', don't use any
prefix even if `org-id-prefix' specifies one.

So a typical ID could look like \"Org:4nd91V40HI\"."
  (let* ((prefix (if (eq prefix 'none)
		     ""
		   (concat (or prefix org-id-prefix) ":")))
	 unique)
    (if (equal prefix ":") (setq prefix ""))
    (cond
     ((memq org-id-method '(uuidgen uuid))
      (setq unique (org-trim (shell-command-to-string org-id-uuid-program)))
      (unless (org-uuidgen-p unique)
	(setq unique (org-id-uuid))))
     ((eq org-id-method 'org)
      (let* ((etime (org-reverse-string (org-id-time-to-b36)))
	     (postfix (if org-id-include-domain
			  (progn
			    (require 'message)
			    (concat "@" (message-make-fqdn))))))
	(setq unique (concat etime postfix))))
     ((eq org-id-method 'ts)
      (let ((ts (el-patch-swap (format-time-string org-id-ts-format)
                               (rasen/tsid)))
	    (postfix (if org-id-include-domain
			 (progn
			   (require 'message)
			   (concat "@" (message-make-fqdn))))))
	(setq unique (concat ts postfix))))
     (t (error "Invalid `org-id-method'")))
    (concat prefix unique)))

Highlight org-id links with different color.

(defface rasen/org-id-link
  '((t :inherit org-link))
  "Face for org-id links.")

(org-link-set-parameters "id" :face 'rasen/org-id-link)

Actual style is overridden in Color theme section.

org-roam

(use-package org-roam
  :after org
  :diminish
  :general
  (s-leader-def
    "n r" #'org-roam-buffer-toggle
    "n f" #'org-roam-node-find
    "n j" #'org-roam-dailies-goto-date
    "n t" #'org-roam-dailies-goto-today
    "n ." #'org-roam-dailies-goto-today
    "n ," #'org-roam-dailies-goto-yesterday)
  (:keymaps 'org-mode-map
   :states 'normal
   "SPC ," #'org-roam-dailies-goto-previous-note
   "SPC ." #'org-roam-dailies-goto-next-note)
  (:keymaps 'org-mode-map
   :states '(insert visual)
   "C-c i" #'org-roam-node-insert
   ;; C-i is interpreted as TAB
   "C-c TAB" #'org-roam-node-insert)

  :init
  ;; yes, I have migrated to v2
  (setq org-roam-v2-ack t)

  :config

  (setq org-roam-directory (concat rasen/org-directory "/roam")
        ;; move `org-roam-db-location' off roam directory, so syncthing does not sync it
        org-roam-db-location (expand-file-name "cache/org-roam.db" user-emacs-directory))

  <<org-roam-exclude-org-fc>>

  <<org-roam-node-display>>

  <<org-roam-slip-boxes>>

  <<org-roam-dailies>>

  <<org-roam-protocol>>

  <<org-roam-graph>>

  <<org-roam-buffer>>

  <<org-roam-kebab-slugs>>

  <<org-roam-update-ids>>

  <<org-roam-new-node>>

  <<org-roam-update-title>>

  (org-roam-db-autosync-enable))

org-roam-exclude-org-fc

Do not treat org-fc cards as nodes.

(setq org-roam-db-node-include-function
      (defun rasen/org-roam-include ()
        ;; exclude org-fc headlines from org-roam
        (not (member "fc" (org-get-tags)))))

org-roam-slip-boxes

Slip boxes are basically different directories that I split notes into.

(defconst rasen/slip-boxes
  '(;; Default slip-box with permanent notes
    ("d" "default"     ""         "${rasen/capture-tsid}")

    ;; "Life project"—everything that doesn't fit in other slip
    ;; boxes. Examples are: my gratitude journal, small projects,
    ;; article drafts, idea list.
    ("l" "life"        "life/"    "${rasen/capture-tsid}")

    ;; Posts
    ("p" "posts"       "posts/"   "${slug}"    "#+DATE: %<%Y-%m-%d>\n#+LAST_MODIFIED: \n#+PUBLISHED: false")

    ;; Literature notes
    ("b" "bibliograpic" "biblio/" "${citekey}" "#+LAST_MODIFIED: \n#+DATE: %<%Y-%m-%d>\n"))
  "My slip boxes. Format is a list of (capture-key name directory filename extra-template).")

;; one capture template per slip-box
(setq org-roam-capture-templates
      (mapcar (lambda (x)
                (let ((key  (nth 0 x))
                      (name (nth 1 x))
                      (dir  (nth 2 x))
                      (filename (nth 3 x))
                      (extra-template (nth 4 x)))
                  `(,key ,name plain "%?"
                         :if-new (file+head
                                  ,(concat dir filename ".org")
                                  ,(concat "#+TITLE: ${title}\n"
                                           extra-template))
                         :immediate-finish t
                         :unnarrowed t)))
              rasen/slip-boxes))

(defun rasen/capture-tsid (node)
  "A hack definition to workaround that org-roam passes a node argument."
  (rasen/tsid))

And a helper command to move notes between slip boxes.

(defun rasen/move-to-slip-box (slip-box)
  "Move file to specified SLIP-BOX."
  (interactive (list (completing-read "Move to slip-box: "
                                      (mapcar (lambda (x) (nth 2 x)) rasen/slip-boxes))))
  (let* ((filename (buffer-file-name))
         (directory (file-name-directory filename))
         (name (file-name-nondirectory filename))
         (new-name (f-join org-roam-directory slip-box name)))
    (rasen/roam-rename new-name)))

;; TODO: with org-roam-v2 this probably can be simplified
(defun rasen/roam-rename (new-name)
  "Move file to NEW-NAME. `org-roam' takes care of adjusting all links."
  (let ((filename (buffer-file-name)))
    (unless filename
      (error "Buffer '%s' is not visiting file!" (buffer-name)))
    (rename-file filename new-name)
    (set-visited-file-name new-name t)
    (revert-buffer t t t)
    ;; trigger save-buffer for org-roam to regenerate `org-roam-buffer'.
    (set-buffer-modified-p t)
    (save-buffer)))

And some extra refile helpers for dealing with inbox.

;; TODO: replace hard-coded paths with querying org-roam by title.

(defun rasen/refile-weight ()
  "Refile current item as weight log."
  (interactive)
  (save-excursion
    (save-window-excursion
      (rasen/org-copy-log-entry t)
      (find-file (concat org-roam-directory "/life/20200620011908.org"))
      (goto-char (point-max))
      (yank))))

(defun rasen/refile-gratitude ()
  (interactive)
  (save-excursion
    (save-window-excursion
      (let* ((element (org-element-at-point))
             (created (org-element-property :CREATED element))
             (cbeg (org-element-property :contents-begin element))
             (cend (org-element-property :contents-end element))
             (contents (buffer-substring cbeg cend)))
        (org-cut-subtree)
        (current-kill 1)

        (find-file (concat org-roam-directory "/life/20200620010632.org"))
        (org-datetree-find-date-create
         (calendar-gregorian-from-absolute
          (time-to-days (org-read-date nil t created))))

        (next-line)
        (insert contents)))))

org-roam-node-display

Configure how nodes are shown in the prompt (when searching, creating new, inserting link).

Show slip box and path within file. e.g., (life) My file > sub-note.

(setq org-roam-node-display-template "${hierarchy:*} ${tags:10}")

(cl-defmethod org-roam-node-filetitle ((node org-roam-node))
  "Return the file TITLE for the node."
  (org-roam-get-keyword "TITLE" (org-roam-node-file node)))

(cl-defmethod org-roam-node-hierarchy ((node org-roam-node))
  "Return the hierarchy for the node."
  (let ((title (org-roam-node-title node))
        (olp (org-roam-node-olp node))
        (level (org-roam-node-level node))
        (directories (org-roam-node-directories node))
        (filetitle (org-roam-node-filetitle node)))
    (concat
     (if directories (format "(%s) " directories))
     (if (> level 0) (concat filetitle " > "))
     (if (> level 1) (concat (string-join olp " > ") " > "))
     title)))

(cl-defmethod org-roam-node-directories ((node org-roam-node))
  (if-let ((dirs (file-name-directory (file-relative-name (org-roam-node-file node) org-roam-directory))))
      (string-join (f-split dirs) "/")
    nil))

org-roam-buffer

Configure org-roam-buffer (buffer with backlinks).

Show all sections:

(setq org-roam-mode-section-functions
      (list #'org-roam-backlinks-section
            #'org-roam-reflinks-section
            #'org-roam-unlinked-references-section))

Make it prettier—enable word-wrap and variable pitch font.

(add-hook 'org-roam-mode-hook #'visual-line-mode)
(add-hook 'org-roam-mode-hook #'variable-pitch-mode)

org-roam-dailies

I moved my journaling into org-roam and this provides more context when reviewing my notes (unlinked references show where that date was mentioned).

All daily notes are stored in a separate directory so they do not mix up with normal notes. Nothing fancy here.

(require 'org-roam-dailies)
(setq org-roam-dailies-directory "life/journal/")
(setq org-roam-dailies-capture-templates
      `(("j" "journal" plain "%U\n%?"
         :if-new (file+head "%<%Y-%m-%d>.org"
                            ;; Adding day of week to title makes
                            ;; unlinked references search weaker, so
                            ;; store it in metadata.
                            ,(concat
                              "#+TITLE: %<%Y-%m-%d>\n"
                              "- day of week :: %<%A>\n"
                              "\n"
                              "* Events\n"
                              "* Decisions\n"
                              "* Tomorrow\n")))))

org-roam-protocol

org-roam-protocol allows opening a note corresponding to the current URL directly from the browser (as well as capturing selection into it).

First, add the following snippet as a bookmarklet:

javascript:location.href = 'org-protocol://roam-ref?' + new URLSearchParams({
  template: 'r',
  ref: location.href.split('#')[0],
  title: encodeURIComponent(document.title),
  body: encodeURIComponent(window.getSelection())
})

Firefox is annoying with asking permissions to allow org-protocol urls, but you can disable that if you go to about:config and toggle security.external_protocol_requires_permission off (only if you know what you’re doing).

Configure template. rasen/capture-quote-body bit inserts the selection as a quote if something is selected and does nothing otherwise.

(require 'org-roam-protocol)

(setq org-roam-capture-ref-templates
      `(("r" "ref" plain "%(rasen/capture-quote-body)%?"
         :if-new (file+head "biblio/${rasen/capture-tsid}.org"
                            ,(concat "#+TITLE: ${title}\n"
                                     "#+DATE: %<%Y-%m-%d>\n"))
         :empty-lines 1
         :immediate-finish t
         :jump-to-captured t
         :unnarrowed t)))

(defun rasen/capture-quote-body ()
  "Quote selection only if it is present."
  (let ((body (plist-get org-roam-capture--info :body)))
    (when (not (string-empty-p body))
      (concat "#+begin_quote\n" body "\n#+end_quote"))))

When I take notes on the page, I want the note to be open in a split window. I also don’t want to be in the capture process, because you quickly get into recursive capture and it just takes extra time to quit them. :immediate-finish and :jump-to-captured do that. However, because I’m on EXWM, the default behavior for :jump-to-capture is to open the note in the current window, which is my browser.

I patch org-goto-marker-or-bmk so that it does not necessarily use the same window. This is used for org-roam-capture-ref to open the new note in a split window instead of reusing the browser window. (This might cause some other parts of org-mode to behave weirdly, but I have to see.)

(el-patch-defun org-goto-marker-or-bmk (marker &optional bookmark)
  "Go to MARKER, widen if necessary.  When marker is not live, try BOOKMARK."
  (if (and marker (marker-buffer marker)
	   (buffer-live-p (marker-buffer marker)))
      (progn
	((el-patch-swap pop-to-buffer-same-window pop-to-buffer) (marker-buffer marker))
	(when (or (> marker (point-max)) (< marker (point-min)))
	  (widen))
	(goto-char marker)
	(org-show-context 'org-goto))
    (if bookmark
	(bookmark-jump bookmark)
      (error "Cannot find location"))))

org-roam-graph

Install graphviz globally:

{
  home.packages = [ pkgs.graphviz ];
}

Emacs configuration:

(require 'org-roam-graph)

;; better defaults for graph view
;; (setq org-roam-graph-executable (executable-find "dot"))
;; (setq org-roam-graph-executable (executable-find "neato"))
;; (setq org-roam-graph-executable (executable-find "fdp"))
(setq org-roam-graph-executable (executable-find "sfdp"))
(setq org-roam-graph-extra-config '(("concentrate" . "true")
                                    ("overlap" . "prism100")
                                    ("overlap_scaling" . "-8")
                                    ;; ("pack" . "true")
                                    ("sep" . "20.0")
                                    ("esep" . "0.0")
                                    ;; ("esep" . "0.01")
                                    ;; ("splines" . "true")
                                    ("splines" . "polyline")
                                    ))

(setq org-roam-graph-node-extra-config
      '(("id"
         ("shape" . "rectangle")
         ("style" . "bold,rounded,filled")
         ("fillcolor" . "#EEEEEE")
         ("color" . "#C9C9C9")
         ("fontcolor" . "#111111"))
        ("http"
         ("style" . "rounded,filled")
         ("fillcolor" . "#EEEEEE")
         ("color" . "#C9C9C9")
         ("fontcolor" . "#0A97A6"))
        ("https"
         ("shape" . "rounded,filled")
         ("fillcolor" . "#EEEEEE")
         ("color" . "#C9C9C9")
         ("fontcolor" . "#0A97A6"))))
(setq org-roam-graph-edge-extra-config nil)

<<org-roam-graph-exclude-node>>

org-roam-graph-exclude-node

Patch org-roam to allow a custom function to exclude nodes.

(require 'cl)
(require 'el-patch)

(defvar rasen/org-roam-graph-exclude-node (lambda (id node type) nil)
  "Function to exclude nodes from org-roam-graph.")

(defun rasen/org-roam-graph--filter-edges (edges &optional nodes-table)
  (let ((nodes-table (or nodes-table (org-roam--nodes-table))))
    (seq-filter (pcase-lambda (`(,source ,dest ,type))
                  (let ((source-node (gethash source nodes-table))
                        (dest-node (gethash dest nodes-table)))
                    (not (or (funcall rasen/org-roam-graph-exclude-node source source-node "id")
                             (funcall rasen/org-roam-graph-exclude-node dest dest-node type)))))
                edges)))

(el-patch-defun org-roam-graph--dot (&optional edges all-nodes)
  "Build the graphviz given the EDGES of the graph.
If ALL-NODES, include also nodes without edges."
  (let ((org-roam-directory-temp org-roam-directory)
        (nodes-table (org-roam--nodes-table))
        (seen-nodes (list))
        (edges (el-patch-let (($orig (or edges (org-roam-db-query [:select :distinct [source dest type] :from links]))))
                 (el-patch-swap
                   $orig
                   (rasen/org-roam-graph--filter-edges $orig)))))
    (with-temp-buffer
      (setq-local org-roam-directory org-roam-directory-temp)
      (insert "digraph \"org-roam\" {\n")
      (dolist (option org-roam-graph-extra-config)
        (insert (org-roam-graph--dot-option option) ";\n"))
      (insert (format " edge [%s];\n"
                      (mapconcat (lambda (var)
                                   (org-roam-graph--dot-option var nil "\""))
                                 org-roam-graph-edge-extra-config
                                 ",")))
      (pcase-dolist (`(,source ,dest ,type) edges)
        (unless (member type org-roam-graph-link-hidden-types)
          (pcase-dolist (`(,node ,node-type) `((,source "id")
                                               (,dest ,type)))
            (unless (member node seen-nodes)
              (insert (org-roam-graph--format-node
                       (or (gethash node nodes-table) node) node-type))
              (push node seen-nodes)))
          (insert (format "  \"%s\" -> \"%s\";\n"
                          (xml-escape-string source)
                          (xml-escape-string dest)))))
      (when all-nodes
        (maphash (lambda (id node)
                   (unless (el-patch-let (($orig (member id seen-nodes)))
                             (el-patch-swap
                               $orig
                               (or $orig
                                   (funcall rasen/org-roam-graph-exclude-node id node "id"))))
                     (insert (org-roam-graph--format-node node "id"))))
                 nodes-table))
      (insert "}")
      (buffer-string))))

Exclude all links that are not nodes (non-id) as well as links from non-permanent directories.

(setq rasen/org-roam-graph-exclude-node
      (defun rasen/org-roam-graph-exclude-node (id node type)
        (or (not (string-equal type "id"))
            (and node
                 (string-match "/\\(life\\|biblio\\)/" (org-roam-node-file node))))))

org-roam-kebab-slugs

Patch slug function so it uses kebab-case instead of snake-case.

(el-patch-defun org-roam-node-slug (node)
  "Return the slug of NODE."
  (let ((title (org-roam-node-title node))
        (slug-trim-chars '(;; Combining Diacritical Marks https://www.unicode.org/charts/PDF/U0300.pdf
                           768 ; U+0300 COMBINING GRAVE ACCENT
                           769 ; U+0301 COMBINING ACUTE ACCENT
                           770 ; U+0302 COMBINING CIRCUMFLEX ACCENT
                           771 ; U+0303 COMBINING TILDE
                           772 ; U+0304 COMBINING MACRON
                           774 ; U+0306 COMBINING BREVE
                           775 ; U+0307 COMBINING DOT ABOVE
                           776 ; U+0308 COMBINING DIAERESIS
                           777 ; U+0309 COMBINING HOOK ABOVE
                           778 ; U+030A COMBINING RING ABOVE
                           780 ; U+030C COMBINING CARON
                           795 ; U+031B COMBINING HORN
                           803 ; U+0323 COMBINING DOT BELOW
                           804 ; U+0324 COMBINING DIAERESIS BELOW
                           805 ; U+0325 COMBINING RING BELOW
                           807 ; U+0327 COMBINING CEDILLA
                           813 ; U+032D COMBINING CIRCUMFLEX ACCENT BELOW
                           814 ; U+032E COMBINING BREVE BELOW
                           816 ; U+0330 COMBINING TILDE BELOW
                           817 ; U+0331 COMBINING MACRON BELOW
                           )))
    (cl-flet* ((nonspacing-mark-p (char)
                                  (memq char slug-trim-chars))
               (strip-nonspacing-marks (s)
                                       (ucs-normalize-NFC-string
                                        (apply #'string (seq-remove #'nonspacing-mark-p
                                                                    (ucs-normalize-NFD-string s)))))
               (cl-replace (title pair)
                           (replace-regexp-in-string (car pair) (cdr pair) title)))
      (let* ((pairs `(el-patch-swap (("[^[:alnum:][:digit:]]" . "_") ;; convert anything not alphanumeric
                                     ("__*" . "_") ;; remove sequential underscores
                                     ("^_" . "") ;; remove starting underscore
                                     ("_$" . "")) ;; remove ending underscore
                                    (("[^[:alnum:][:digit:]]" . "-") ;; convert anything not alphanumeric
                                     ("--*" . "-") ;; remove sequential dashes
                                     ("^-" . "") ;; remove starting dash
                                     ("-$" . "")))) ;; remove ending dash
             (slug (-reduce-from #'cl-replace (strip-nonspacing-marks title) pairs)))
        (downcase slug)))))

org-roam-update-ids

A helper function for the next time org-id fucks up its database.

(defun rasen/org-roam-update-ids ()
  "Update all org-ids in org-roam-directory."
  (interactive)
  (org-id-update-id-locations
   (directory-files-recursively org-roam-directory "\\.org$")))

org-roam-new-node

A function to create a new node without prompting for a title. Sometimes I want to create a new node and I am not sure what the title will be—I discover it while I write the note.

(defun rasen/org-roam-new-node (&optional keys)
  (interactive)
  (org-roam-capture-
   :keys keys
   :node (org-roam-node-create :title "")
   :props '(:finalize find-file)))

(defun rasen/org-roam-new-node-default ()
  (interactive)
  (rasen/org-roam-new-node "d"))

(defun rasen/org-roam-new-node-life ()
  (interactive)
  (rasen/org-roam-new-node "l"))

(s-leader-def "n n" #'rasen/org-roam-new-node)
(s-leader-def "n d" #'rasen/org-roam-new-node-default)
(s-leader-def "n l" #'rasen/org-roam-new-node-life)

org-roam-update-title

;; TODO: this only handles file renames. Make it work for headline nodes too.
(defun rasen/org-roam-title-update ()
  "Update the title of the current node and patch all incoming links."
  (interactive)
  (save-restriction
    (save-excursion
      (goto-char (point-min))
      (let* ((old-node (org-roam-node-at-point))
             (old-title (and old-node (org-roam-node-title old-node)))
             (old-id (and old-node (org-roam-node-id old-node)))
             (new-title (cdar (org-collect-keywords '("title") '("title")))))
        (when (not (string-equal old-title new-title))
          (message "renaming: %s -> %s" old-title new-title)
          (let ((files (org-roam-db-query [:select (unique file)
                                           :from links
                                           :inner-join nodes
                                           :on (= links:source nodes:id)
                                           :where (= dest $s1)
                                           :and (= type "id")]
                                          old-id))
                (replace-re (rx "[[id:"
                                (literal old-id)
                                "]["
                                (literal old-title)
                                "]"))
                (new-link (concat "[[id:" old-id "][" new-title "]")))
            (mapc (pcase-lambda (`(,file))
                    (with-current-buffer (find-file-noselect file)
                      (goto-char (point-min))
                      (while (re-search-forward replace-re nil t)
                        (replace-match new-link))))
                  files)))))))

(add-hook 'org-mode-hook
          (defun rasen/setup-title-hook ()
            (add-hook 'before-save-hook #'rasen/org-roam-title-update 0 t)
            (add-hook 'after-save-hook #'org-roam-db-update-file 0 t)))

org-roam-ui

(use-package org-roam-ui
  :after org-roam
  :commands (org-roam-ui-mode)
  :config
  (setq org-roam-ui-update-on-save t
        org-roam-ui-sync-theme nil
        org-roam-ui-open-on-start t))

org-ref

(use-package ivy-bibtex
  :config
  (push (cons 'org-mode #'bibtex-completion-format-citation-org-cite) bibtex-completion-format-citation-functions)
  (defun rasen/ivy-cite ()
    (interactive)
    (let ((ivy-bibtex-default-action #'ivy-bibtex-insert-citation))
      (call-interactively #'ivy-bibtex))))
(use-package org-ref
  :after org-roam
  :config
  (let* ((bib-file-name '("books.bib" "papers.bib" "online.bib"))
         (bib-directory (expand-file-name "biblio" org-roam-directory))
         (bib-files-directory (expand-file-name "files/" bib-directory))
         (bib-files (mapcar (lambda (x) (expand-file-name x bib-directory)) bib-file-name)))

    (setq reftex-default-bibliography bib-files)
    ;; (setq org-ref-default-bibliography bib-files)
    (setq bibtex-completion-bibliography bib-files)

    ;; (setq org-ref-bibliography-notes bib-directory)
    (setq bibtex-completion-notes-path bib-directory)

    ;; (setq org-ref-pdf-directory bib-files-directory)
    (setq bibtex-completion-library-path `(,bib-files-directory)))

  (require 'org-ref-ivy)

  (require 'org-ref-url-utils)
  (require 'org-ref-isbn)
  (general-def 'normal 'bibtex-mode-map
    "C-c C-c" #'org-ref-clean-bibtex-entry
    "C-c c"   #'org-ref-clean-bibtex-entry
    "C-c s"   #'bibtex-sort-buffer
    "C-c n"   #'org-ref-open-bibtex-notes

    ;; (a)ttach pdf
    "C-c a"   #'org-ref-bibtex-assoc-pdf-with-entry
    "C-c f"   #'org-ref-bibtex-pdf

    ;; (o)nline
    "C-c o"   #'org-ref-url-html-to-bibtex
    "C-c i"   #'isbn-to-bibtex)

  (setq org-ref-completion-library 'org-ref-ivy-cite)

  ;; Rules for automatic key generation
  (setq bibtex-autokey-year-length 4
        bibtex-autokey-name-year-separator ""
        bibtex-autokey-year-title-separator "-"
        bibtex-autokey-titleword-separator "-"
        bibtex-autokey-titlewords 5
        bibtex-autokey-titlewords-stretch 1
        bibtex-autokey-titleword-length 5)

  (setq bibtex-dialect 'biblatex)

  ;; `isbn-to-bibtex' fails with "(wrong-type-argument stringp nil)"
  ;; error down in `org-ref-isbn-clean-bibtex-entry' functions. This
  ;; happens because temporary buffer is not in `bibtex-mode', so
  ;; `bibtex-entry-head' variable is not set.
  ;;
  ;; Prepend `bibtex-mode' to the list of processors, so the next ones
  ;; work correctly.
  (add-hook 'org-ref-isbn-clean-bibtex-entry-hook #'bibtex-mode)

  ;; Do not fill entries. (It works badly with urls.)
  (el-patch-defun bibtex-fill-field-bounds (bounds justify &optional move)
    "Fill BibTeX field delimited by BOUNDS.
If JUSTIFY is non-nil justify as well.
If optional arg MOVE is non-nil move point to end of field."
    (let ((end-field (copy-marker (bibtex-end-of-field bounds))))
      (if (not justify)
          (goto-char (bibtex-start-of-text-in-field bounds))
        (goto-char (bibtex-start-of-field bounds))
        (forward-char) ; leading comma
        (bibtex-delete-whitespace)
        (insert "\n")
        (indent-to-column (+ bibtex-entry-offset
                             bibtex-field-indentation))
        (re-search-forward "[ \t\n]*=" end-field)
        (replace-match "=")
        (forward-char -1)
        (if bibtex-align-at-equal-sign
            (indent-to-column
             (+ bibtex-entry-offset (- bibtex-text-indentation 2)))
          (insert " "))
        (forward-char)
        (bibtex-delete-whitespace)
        (if bibtex-align-at-equal-sign
            (insert " ")
          (indent-to-column bibtex-text-indentation)))
      (el-patch-remove
        ;; Paragraphs within fields are not preserved.  Bother?
        (fill-region-as-paragraph (line-beginning-position) end-field
                                  default-justification nil (point)))
      (if move (goto-char end-field)))))

org-ref-cite

(use-package citeproc)

(use-package oc
  :config
  (require 'oc-csl))

(use-package org-ref-cite
  :after org-ref
  :config
  (setq org-cite-global-bibliography bibtex-completion-bibliography
        org-cite-insert-processor 'org-ref-cite
        org-cite-follow-processor 'org-ref-cite
        org-cite-activate-processor 'org-ref-cite)

  ;; blatantly re-define function to suppress errors
  (defun org-ref-cite-activate (&rest args)
    "Run all the activation functions in `org-ref-cite-activation-functions'.
Argument CITATION is an org-element holding the references."
    (cl-loop for activate-func in org-ref-cite-activation-functions
             do
             (apply activate-func args)))

  ;; (defadvice org-activate-links (around org-activate-links-ignore-errors)
  ;;   "Ignore errors in `org-activate-links'."
  ;;   (ignore-error wrong-number-of-arguments ad-do-it))
  ;; (ad-deactivate 'org-activate-links)
  ;; (ad-activate 'org-activate-links)
  )

org-roam-bibtex

Citations and bibliography tools for org-mode.

(use-package org-roam-bibtex
  :diminish
  :after org-roam
  :config
  (require 'org-ref)
  (org-roam-bibtex-mode))

toc-org

Generate Table of Contents for this file.

(use-package toc-org
  :commands (toc-org-mode toc-org-insert-toc))

org-fc

Flashcards/spaced repetition system for org-mode. It works with many files better than org-drill (and many files is what I have with org-roam).

(use-package org-fc
  :config
  (require 'org-fc-keymap-hint)

  (setq org-fc-directories (list (expand-file-name "roam" rasen/org-directory)))
  (setq org-fc-review-history-file (expand-file-name "org-fc-history.tsv" rasen/org-directory))

  ;; (setq org-fc-stats-review-min-box 2)

  (el-patch-defun org-fc-index-flatten-card (card)
    "Flatten CARD into a list of positions.
Relevant data from the card is included in each position
element."
    (mapcar
     (lambda (pos)
       (list
        :filetitle (plist-get card :filetitle)
        :tags (plist-get card :tags)
        :path (plist-get card :path)
        :id (plist-get card :id)
        (el-patch-add :suspended (plist-get card :suspended))
        :type (plist-get card :type)
        :due (plist-get pos :due)
        :position (plist-get pos :position)))
     (plist-get card :positions)))

  (defun rasen/org-fc-upcoming-histogram (&optional context)
    "Draw a histogram of upcoming review."
    (interactive (list (org-fc-select-context)))
    (let* ((positions (seq-filter (lambda (x)
                                    (not (plist-get x :suspended)))
                                  (org-fc-index-positions (org-fc-index (or context 'all)))))
           (total (length positions))
           (sorted (seq-sort
                    (lambda (x y) (time-less-p
                                   (plist-get x :due)
                                   (plist-get y :due)))
                    positions))
           (next-review (plist-get (car sorted) :due))
           (next-review-diff (time-to-seconds (time-subtract next-review nil)))
           (grouped (seq-group-by
                     (lambda (x)
                       (format-time-string "%F" (plist-get x :due)))
                     sorted))
           (grouped-count (mapcar (lambda (x)
                                    (cons (car x) (length (cdr x))))
                                  grouped)))
      (with-output-to-temp-buffer "*org-fc-upcoming*"
        (princ (format-time-string "Next review: %F %T" next-review))
        (if (> next-review-diff 0)
            (princ (format " (in %s)\n" (format-seconds "%D %H %z%M" next-review-diff)))
          (princ " (ready)\n"))
        (princ (format "Total positions: %s\n\n" total))
        (princ "Upcoming reviews:\n")
        (mapc
         (lambda (x)
           (princ (car x))
           (princ (format " %3s " (cdr x)))
           (princ (make-string (cdr x) ?+))
           (princ "\n"))
         grouped-count))
      (switch-to-buffer-other-window "*org-fc-upcoming*")))

  (general-define-key
   :definer 'minor-mode
   :states 'normal
   :keymaps 'org-fc-review-flip-mode
   "RET" 'org-fc-review-flip
   "n" 'org-fc-review-flip
   "s" 'org-fc-review-suspend-card
   "q" 'org-fc-review-quit)

  (general-define-key
   :definer 'minor-mode
   :states 'normal
   :keymaps 'org-fc-review-rate-mode
   "a" 'org-fc-review-rate-again
   "h" 'org-fc-review-rate-hard
   "g" 'org-fc-review-rate-good
   ;; There seems to be an issue binding "g" and it still behaves as
   ;; a prefix for other commands in rate mode.
   ;;
   ;; Bind `org-fc-review-rate-good' to "n" as well to workaround
   ;; this.
   "n" 'org-fc-review-rate-good
   "e" 'org-fc-review-rate-easy
   "s" 'org-fc-review-suspend-card
   "q" 'org-fc-review-quit)

  (general-def 'normal 'org-fc-review-edit-mode-map
    "'"   #'org-fc-review-resume)

  (general-def 'normal 'org-fc-dashboard-mode-map
    "r"   #'org-fc-dashboard-review
    "g"   (defun rasen/org-fc-dashboard ()
            (interactive)
            (org-fc-dashboard org-fc-context-all))
    "h"   #'rasen/org-fc-upcoming-histogram
    "q"   #'quit-window)

  <<org-fc-review-todos>>)

org-fc review todos

My modifications to support reviewing arbitrary todo lists with spaced repetition.