• Stars
    star
    209
  • Rank 188,325 (Top 4 %)
  • Language
    Nix
  • Created about 11 years ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

My dotfiles

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" "aarch64-darwin"];

      # 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>>
      <<flake-outputs-nix-darwin>>
    ];
}

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
    '';
  };
}

For nix-darwin systems:

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

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

experimental-features = nix-command flakes

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-21.05";
};

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 = { lib, ... }: {
        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 = "21.05";
  };
  homeManagerConfigurations = genHosts homeManagerHosts mkHomeManagerConfiguration;
})
{
  options.hostname = lib.mkOption {
    default = null;
    type = lib.types.nullOr lib.types.str;
    description = "hostname so that other home-manager options can depend on it.";
  };
}

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;
  };
}

Integrate home-manager into nix-darwin.

{
  imports = [inputs.home-manager.darwinModules.home-manager];
  home-manager = {
    useUserPackages = false;
    useGlobalPkgs = true;
    users.rasen = { ... }: {
      imports = [
        inputs.self.lib.home-manager-common
        { hostname = config.networking.hostName; }
      ];
    };
    # users.rasen = inputs.self.lib.home-manager-common;
  };
}

nix-darwin

Add nix-darwin to flake inputs.

darwin = {
  url = "github:lnl7/nix-darwin";
  inputs.nixpkgs.follows = "nixpkgs";
};

Add helper function to differentiate linux-only config:

(final: prev: {
  lib = prev.lib // {
    linux-only = prev.lib.mkIf final.stdenv.isLinux;
  };
})
(let
  darwinHosts = {
    <<darwin-host>>
  };

  mkDarwinConfiguration = { name, system, modules ? [] }:
    inputs.darwin.lib.darwinSystem {
      inherit system;
      modules = modules ++ [
        { networking.hostName = name; }
        self.darwin-common
      ];
    };

in {
  darwin-common = { lib, pkgs, config, ... }: {
    imports = [
      <<darwin-section>>
    ];
  };

  darwinConfigurations = genHosts darwinHosts mkDarwinConfiguration;
})
{
  nixpkgs.config = {
    allowUnfree = true;
  };
  nixpkgs.overlays = self.overlays.aarch64-darwin;
  services.nix-daemon.enable = true;

  system.stateVersion = 4;
}

Fix linking applications

LnL7/nix-darwin#226 Disable taking control of ~/Applications folder by berbiche

({ lib, config, ... }: {
  system.activationScripts.applications.text = lib.mkForce ''
    # Set up applications.
    echo "setting up ~/Applications..." >&2
    mkdir -p ~/Applications
    if [ ! -e ~/Applications/Nix\ Apps -o -L ~/Applications/Nix\ Apps ]; then
      ln -sfn ${config.system.build.applications}/Applications ~/Applications/Nix\ Apps
    else
      echo "warning: ~/Applications/Nix Apps is not owned by nix-darwin, skipping App linking..." >&2
    fi
  '';
})

And enable this in home-manager (home-manager/linkapps.nix at master · nix-community/home-manager · GitHub):

({ config, lib, pkgs, ... }:

{
  config = lib.mkIf pkgs.stdenv.hostPlatform.isDarwin {
    # Install MacOS applications to the user environment.
    home.file."Applications/Home Manager Apps".source = let
      apps = pkgs.buildEnv {
        name = "home-manager-applications";
        paths = config.home.packages;
        pathsToLink = "/Applications";
      };
    in "${apps}/Applications";
  };
})

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 = "21.05";
    }

    <<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 = pkgs.lib.linux-only (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.settings.sandbox = true;
}

Users

I’m the only user of the system:

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

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

Machines

macOS

Users

{
  users.users.rasen = {
    description = "Oleksii Shmalko";
    home = "/Users/rasen/";
  };
}

xbar integration function

(defun rasen/xbar (base &optional arg)
  "Return a string for xbar plugin integration or perform the action."
  ;; (message "(rasen/xbar %S %S)" base arg)
  (pcase arg
    ('clock-out (org-clock-out))
    ('clock-in-last (org-clock-in-last))

    ('nil (if (org-clock-is-active)
              (concat
               (format "[%s] %s | length=50\n" (org-duration-from-minutes (org-clock-get-clocked-time)) org-clock-heading)
               "---\n"
               (format "Clock out | shell=%s | param1=clock-out\n" base))

            (concat
             (format "not clocking %s | color=gray | size=12\n"
                     (cond
                      (break--work-timer (format "w:%s" (break--timer-to-string break--work-timer)))
                      (break--rest-timer (format "r:%s" (break--timer-to-string break--rest-timer)))
                      (t "")))
             "---\n"
             (format "Clock in last | shell=%s | param1=clock-in-last\n" base))))))

org-clock.1s.sh

#!/usr/bin/env bash
~/.nix-profile/bin/emacs --batch --eval "(progn (require 'server) (princ (server-eval-at \"server\" '(rasen/xbar \"$0\"${1:+" '$1"}))))"

Yabai integration

{
  services.yabai = {
    enable = true;
    package = pkgs.yabai;
    config = {
      layout = "bsp";
      window_shadow = "float";
      window_gap = 10;
      focus_follows_mouse = "autoraise";
      mouse_follows_focus = "on";
      mouse_modifier = "fn";
      mouse_action1 = "move";
      mouse_action2 = "resize";
    };
    extraConfig = ''
      yabai -m rule --add app=Emacs manage=on
    '';
  };
}

[https://gist.github.com/ethan-leba/760054f36a2f7c144c6b06ab6458fae6]

{
  environment.systemPackages = [ pkgs.skhd ];
  services.skhd = {
    enable = true;
    skhdConfig = ''
      :: default : emacsclient -e '(message "default mode")'
      :: escaping : emacsclient -e '(message "escaping mode")'
      # :: e : emacsclient -e '(message "escape mode")'
      # :: es ; e
      # #default < lcmd - 0x2a ; e
      # e < lcmd - 0x2a ; default

      default < lcmd - 0x2A ; escaping
      escaping < lcmd - 0x2A ; default

      # escaping < lcmd - k ~

      default < cmd + ctrl - r : yabai -m space --layout $(yabai -m query --spaces --space | jq -r 'if .type == "bsp" then "float" else "bsp" end')
      default < lcmd + ctrl - h : yabai -m window --ratio rel:-0.05
      default < lcmd + ctrl - l : yabai -m window --ratio rel:+0.05
      default < lcmd + shift - h : yabai -m window --swap west
      default < lcmd + shift - j : yabai -m window --swap north
      default < lcmd + shift - k : yabai -m window --swap south
      default < lcmd + shift - l : yabai -m window --swap east
      default < lcmd - h [
        * : yabai -m window --focus west
        "Emacs" ~
      ]
      default < lcmd - j [
        * : yabai -m window --focus north
        "Emacs" ~
      ]
      default < lcmd - k [
        * : yabai -m window --focus south
        "Emacs" ~
        "Slack" ~
        "Telegram" ~
      ]
      default < lcmd - l [
        * : yabai -m window --focus east
        "Emacs" ~
      ]
    '';
  };
}

Emacs integration for yabai:

(defun yabai-move-on-error (direction move-fn)
  (interactive)
  (condition-case nil
      (funcall move-fn)
    (user-error (start-process "yabai" "*yabai*" "yabai" "-m" "window" "--focus" direction))))

(defun yabai-windmove-left ()
  (interactive)
  (yabai-move-on-error "west" #'windmove-left))

(defun yabai-windmove-right ()
  (interactive)
  (yabai-move-on-error "east" #'windmove-right))

(defun yabai-windmove-up ()
  (interactive)
  (yabai-move-on-error "north" #'windmove-up))

(defun yabai-windmove-down ()
  (interactive)
  (yabai-move-on-error "south" #'windmove-down))

Machines

omicron

This is my small Dell XPS 13 running NixOS.

{
  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.settings.max-jobs = 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
  '';
}

bayraktar

bayraktar is macbook pro managed with nix-darwin.

bayraktar = {
  system = "aarch64-darwin";
};

Work email

{
  accounts.email.accounts = lib.mkIf (config.hostname == "bayraktar") (lib.mkForce {
    fluxon = {
      realName = "Oleksii Shmalko";
      address = "[email protected]";
      flavor = "gmail.com";
      primary = true;

      passwordCommand = "pass fluxon/google.com/[email protected]/email";
      maildir.path = "fluxon";

      msmtp.enable = true;
      notmuch.enable = true;
      mbsync.enable = true;
      mbsync.create = "maildir";
    };
  });
  programs.mbsync.extraConfig = lib.mkForce "";
}

Emacs

Install Emacs

I use emacs 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 =
    if pkgs.stdenv.isDarwin
    then pkgs.emacs.overrideAttrs (old: {
      patches =
        (old.patches or [])
        ++ [
          # Fix OS window role so that yabai can pick up emacs
          (pkgs.fetchpatch {
            url = "https://raw.githubusercontent.com/d12frosted/homebrew-emacs-plus/master/patches/emacs-28/fix-window-role.patch";
            sha256 = "sha256-+z/KfsBm1lvZTZNiMbxzXQGRTjkCFO4QPlEK35upjsE=";
          })
          # Use poll instead of select to get file descriptors
          # (pkgs.fetchpatch {
          #   url = "https://raw.githubusercontent.com/d12frosted/homebrew-emacs-plus/master/patches/emacs-29/poll.patch";
          #   sha256 = "sha256-jN9MlD8/ZrnLuP2/HUXXEVVd6A+aRZNYFdZF8ReJGfY=";
          # })
          # Enable rounded window with no decoration
          (pkgs.fetchpatch {
            url = "https://raw.githubusercontent.com/d12frosted/homebrew-emacs-plus/master/patches/emacs-28/no-titlebar-and-round-corners.patch";
            sha256 = "sha256-RYdjAf1c43Elh7ad4kujPnrCX8qY7ZWxufJfCc0QW00=";
          })
          # Make emacs aware of OS-level light/dark mode
          (pkgs.fetchpatch {
            url = "https://raw.githubusercontent.com/d12frosted/homebrew-emacs-plus/master/patches/emacs-28/system-appearance.patch";
            sha256 = "sha256-oM6fXdXCWVcBnNrzXmF0ZMdp8j0pzkLE66WteeCutv8=";
          })
        ];
      configureFlags =
        (old.configureFlags or [])
        ++ [
          "LDFLAGS=-headerpad_max_install_names"
        ];
    })
    else pkgs.emacs.override {
      withX = true;
      # select lucid toolkit
      toolkit = "lucid";
      withGTK2 = false; withGTK3 = false;
    };
  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
      el-patch
      elpy
      emojify
      envrc
      epresent
      evil
      evil-collection
      evil-numbers
      evil-org
      evil-surround
      evil-swap-keys
      exec-path-from-shell
      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
      ol-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
      svelte-mode
      terraform-mode
      tide
      toc-org
      typescript-mode
      use-package
      visual-fill-column
      vterm
      vue-mode
      w3m
      web-mode
      wgrep
      which-key
      whitespace-cleanup-mode
      writegood-mode
      yaml-mode
      yasnippet
      zig-mode

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

      epkgs.elpaPackages.eglot

      (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 # chemistry
          tikz-cd # category theory diagrams
          # required for org export
          wrapfig
          capt-of
        ;
      })
      pkgs.ghostscript
    ]
  );

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

  emacs-final = ((pkgs.emacsPackagesFor 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))

Install ripgrep (rg) to user environment:

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

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."
  (interactive (list (read-shell-command "Run: ")))
  (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") #'yabai-windmove-left)
(exwm-input-set-key (kbd "s-k") #'yabai-windmove-down)
(exwm-input-set-key (kbd "s-j") #'yabai-windmove-up)
(exwm-input-set-key (kbd "s-l") #'yabai-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.lib.linux-only [
    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.lib.linux-only [ 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 j↔k 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";
}

On macOS, right option acts as AltGr. Make Emacs ignore it, so it can work correctly:

(setq ns-right-alternate-modifier 'none)

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 = pkgs.stdenv.isLinux;
    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.lib.linux-only [ 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.lib.linux-only [ 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;
  };
}

On Darwin:

{
  environment.systemPackages = [ pkgs.syncthing ];
}

Docker

Backup

I use borg for backups.

(let
  commonOptions = {
    # repo = "[email protected]:.";
    repo = "/run/media/ext-data/borg";
    removableDevice = true;

    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";

    prune.keep = {
      daily = 7;
      weekly = 4;
      monthly = 12;
      yearly = -1;
    };
  };
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/"
      "/home/rasen/syncthing/"

      # 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 = lib.mkForce true;
  };
})

Mount external drive when needed.

{
  # Prepare mount point
  system.activationScripts = {
    ensure-ext-data = {
      text = ''
        mkdir -p /run/media/ext-data
      '';
      deps = [];
    };
  };

  fileSystems."/run/media/ext-data" = {
    device = "/dev/disk/by-uuid/63972645-dbc8-4543-b854-91038b2da6cb";
    fsType = "ext4";
    options = [
      "noauto"                       # do not mount on boot
      "nofail"
      "x-systemd.automount"          # mount when needed
      "x-systemd.device-timeout=1ms" # device should be plugged already—do not wait for it
      "x-systemd.idle-timout=5m"     # unmount after 5 min of inactivity
    ];
  };
}

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;
  programs.direnv.nix-direnv.enable = true;
  services.lorri.enable = pkgs.stdenv.isLinux;
}

Enable Emacs integration. (emacs-lisp)

(use-package direnv
  :after exec-path-from-shell
  :config
  (direnv-mode))

Better (?) direnv integration via purcell/envrc. (emacs-lisp)

(use-package envrc
  :disabled t
  :after exec-path-from-shell
  :config
  (envrc-global-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";
        home-monitor-2 = "00ffffffffffff004c2d767135305943341f0103804024782a6115ad5045a4260e5054bfef80714f810081c081809500a9c0b300010108e80030f2705a80b0588a0078682100001e000000fd0030901eff8f000a202020202020000000fc004c53323841473730304e0a2020000000ff0048345a524330303236380a2020017f02034bf14761103f04035f762309070783010000e305c0006b030c001000b83c200020016dd85dc401788053003090c354056d1a0000020f3090000461045a04e6060501615a00e30f4100565e00a0a0a029503020350078682100001a6fc200a0a0a055503020350078682100001a0000000000000000000000000000000037";
        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;
          DP-1 = {
            enable = true;
            primary = true;
            position = "0x0";
            mode = "3840x2160";
            rate = "60.00";
          };
        };
      };
      "omicron-home-2" = {
        fingerprint = {
          eDP-1 = omicron;
          DP-1 = home-monitor-2;
        };
        config = {
          eDP-1.enable = false;
          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";
          };
        };
      };
      omicron-home-monitor-2 = {
        fingerprint = {
          DP-1 = home-monitor-2;
        };
        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.lib.linux-only [ 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.lib.linux-only [ pkgs.pavucontrol ];
}
(defun rasen/pavucontrol ()
  (interactive)
  (rasen/switch-start "Pavucontrol" "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.lib.linux-only [
    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
  :disabled t
  :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-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")
      "." ";"
      ";" "."))
  (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-status-mode-map
    "C-j"  #'magit-section-backward-sibling
    "C-k"  #'magit-section-forward-sibling
    "gj"   #'magit-section-backward
    "gk"   #'magit-section-forward
    "j"    #'evil-previous-visual-line
    "k"    #'evil-next-visual-line
    )

  (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 scheme-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))

scheme

(use-package scheme
  :config
  (put 'module 'scheme-indent-function 'defun))

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\\'" . ,(if (eq system-type 'darwin)
                                  "open %s"
                                "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 t :clock-resume t :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"))))
(add-to-list 'org-tags-exclude-from-inheritance "honeypot")

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"
  ("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)
(setq-default org-archive-default-command #'org-archive-subtree)

Agenda

Set my org files location.

(setq org-directory "~/org"
      org-default-notes-file rasen/org-refile-file
      org-agenda-files (cons "~/org/roam/fluxon/index.org" (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)))))))

          ("f" "Fluxon"
           ((agenda "" ((org-agenda-files '("~/org/roam/fluxon/index.org" "~/org/refile-bayraktar.org"))
                        ;; 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-files '("~/org/roam/fluxon/index.org" "~/org/refile-bayraktar.org"))
                         (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 '("~/org/refile-bayraktar.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)

(when (string= (system-name) "omicron")
  ;; 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 "[email protected]")
(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}")

    ;; Work notes
    ("f" "fluxon"      "fluxon/"  "${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)
(s-leader-def "n F" (defun rasen/org-roam-new-node-fluxon ()
                      (interactive)
                      (rasen/org-roam-new-node "f")))

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 (and old-title (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 oc
  :config
  (let* ((bib-file-name '("books.bib" "papers.bib" "online.bib"))
         (bib-directory (expand-file-name "roam/biblio/" org-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 org-cite-global-bibliography bib-files)

    (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)))

  (setq bibtex-completion-pdf-open-function
        (defun rasen/find-file-external (filename)
          (start-process filename nil
                         (if (eq system-type 'darwin) "open" "xdg-open") (expand-file-name filename)))))

(use-package org-ref
  :after org-roam
  :config

  (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)

  (setq org-ref-insert-cite-function
        (lambda ()
          (org-cite-insert nil)))

  ;; 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
;;              (ignore-error wrong-number-of-arguments (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.

(defcustom rasen/org-fc-todos-context `(:paths (,(expand-file-name "resources.org" rasen/org-directory))
                                        :filter (type "nocard"))
  "Context in which to search for todo cards.")

(defcustom rasen/org-fc-writing-inbox-context `(:paths ("~/org/roam/life/20200907034408.org")
                                                :filter (type "nocard"))
  "Context in which to search for writing inbox cards.")

;; override all to exclude nocard
(setq org-fc-context-all '(:paths all
                           :filter (not (type "nocard"))))
(setq org-fc-context-dashboard org-fc-context-all)

(defvar rasen/org-fc-todos-current-context rasen/org-fc-todos-context)

;; Special org-fc card type with no flipping
(defun org-fc-type-nocard-init ()
  "Mark headline as a no-card."
  (interactive)
  (org-fc--init-card "nocard")
  (org-fc-review-data-update '("single")))

(defun org-fc-type-nocard-setup (_position)
  "Prepare a no-card for review."
  (interactive)
  (org-fc-noop))

(org-fc-register-type
 'nocard
 #'org-fc-type-nocard-setup
 #'org-fc-noop
 #'org-fc-noop)

(defvar rasen/org-fc-todos-current-card nil)
(defun rasen/org-fc-todos-next-card ()
  "Present a single card from the current buffer for review."
  (interactive)
  (save-buffer)
  (let* ((index (org-fc-index rasen/org-fc-todos-current-context))
         (cards (org-fc-index-filter-due index))
         (positions (org-fc-index-shuffled-positions cards)))
    (if (null positions)
        (progn
          (message "No todos due right now")
          (setq rasen/org-fc-todos-current-card nil)
          (setq hydra-deactivate t))
      (rasen/org-fc-todos-present-position (car positions)))))

(defun rasen/org-fc-todos-present-position (card)
  (let* ((path (plist-get card :path))
         (id (plist-get card :id))
         (type (plist-get card :type))
         (position (plist-get card :position)))
    (let ((buffer (find-buffer-visiting path)))
      (with-current-buffer (find-file path)
        (goto-char (point-min))
        (org-fc-id-goto id path)
        (org-reveal)

        (setq rasen/org-fc-todos-current-card card)
        (setq org-fc-timestamp (time-to-seconds (current-time)))))))

(el-patch-defun (el-patch-swap org-fc-review-update-data rasen/org-fc-todos-review-update-data) (path id position rating delta)
  "Update the review data of the card.
Also add a new entry in the review history file.  PATH, ID,
POSITION identify the position that was reviewed, RATING is a
review rating and DELTA the time in seconds between showing and
rating the card."
  (org-fc-with-point-at-entry
   ;; If the card is marked as a demo card, don't log its reviews and
   ;; don't update its review data
   (unless (member org-fc-demo-tag (org-get-tags))
     (let* ((data (org-fc-get-review-data))
            (current (assoc position data #'string=)))
       (unless current
         (error "No review data found for this position"))
       (let ((ease (string-to-number (cl-second current)))
             (box (string-to-number (cl-third current)))
             (interval (string-to-number (cl-fourth current))))
         (el-patch-remove (org-fc-review-history-add
                           (list
                            (org-fc-timestamp-now)
                            path
                            id
                            position
                            (format "%.2f" ease)
                            (format "%d" box)
                            (format "%.2f" interval)
                            (symbol-name rating)
                            (format "%.2f" delta)
                            (symbol-name org-fc-algorithm))))
         (cl-destructuring-bind (next-ease next-box next-interval)
             (org-fc-sm2-next-parameters ease box interval rating)
           (setcdr
            current
            (list (format "%.2f" next-ease)
                  (number-to-string next-box)
                  (format "%.2f" next-interval)
                  (org-fc-timestamp-in next-interval)))
           (org-fc-set-review-data data)))))))

(defun rasen/org-fc-todos-rate (rating)
  "Rate the card at point with RATING."
  (if-let ((card rasen/org-fc-todos-current-card))
      (if (string= (plist-get card :id) (org-id-get))
          (let* ((path (plist-get card :path))
                 (id (plist-get card :id))
                 (position (plist-get card :position))
                 (now (time-to-seconds (current-time)))
                 (delta (- now org-fc-timestamp)))
            (rasen/org-fc-todos-review-update-data path id position rating delta)
            (rasen/org-fc-todos-next-card))
        (message "Flashcard ID mismatch"))
    (message "No todos review is in progress")))
(defun rasen/org-fc-todos-rate-soon ()
  (interactive)
  (rasen/org-fc-todos-rate 'hard))
(defun rasen/org-fc-todos-rate-normal ()
  (interactive)
  (rasen/org-fc-todos-rate 'good))
(defun rasen/org-fc-todos-rate-later ()
  (interactive)
  (rasen/org-fc-todos-rate 'easy))
(defun rasen/org-fc-todos-today ()
  (interactive)
  (save-excursion
    (rasen/org-fc-todos-rate 'again))
  (save-excursion
    (rasen/org-do-today))
  (save-buffer)
  (rasen/org-fc-todos-next-card))

(defun rasen/org-fc-todos-suspend ()
  (interactive)
  (org-fc-suspend-card)
  (save-buffer)
  (rasen/org-fc-todos-next-card))

(defun rasen/org-fc-todos-delete ()
  (interactive)
  (org-cut-subtree)
  (save-buffer)
  (rasen/org-fc-todos-next-card))

(defun rasen/org-fc-todos-update-fc ()
  (when (member org-fc-flashcard-tag (org-get-tags nil 'local))
    (cond
     ((string= "NEXT" org-state) (org-toggle-tag org-fc-suspended-tag 'on))
     ((string= "TODO" org-state) (org-toggle-tag org-fc-suspended-tag 'off))
     ((string= "DONE" org-state) (org-toggle-tag org-fc-suspended-tag 'on)))))

(add-hook 'org-after-todo-state-change-hook #'rasen/org-fc-todos-update-fc)

(defun rasen/org-fc-todos-estimate-next-interval (rating)
  (condition-case err
      (org-fc-with-point-at-entry
       (let* ((data (org-fc-get-review-data))
              (current (assoc (plist-get rasen/org-fc-todos-current-card :position) data #'string=)))
         (let ((ease (string-to-number (cl-second current)))
               (box (string-to-number (cl-third current)))
               (interval (string-to-number (cl-fourth current))))
           (cl-destructuring-bind (next-ease next-box next-interval) (org-fc-sm2-next-parameters ease box interval rating)
             next-interval))))
    (error 0)))

(defun rasen/org-fc-todos-hydra-hint (name rating)
  (format "%s (%.2f days)" name (rasen/org-fc-todos-estimate-next-interval rating)))

(defhydra rasen/hydra-todos-review ()
  "todos review"
  ("q" nil "quit")
  ("s" #'rasen/org-fc-todos-rate-soon   (rasen/org-fc-todos-hydra-hint "soon"   'hard))
  ("n" #'rasen/org-fc-todos-rate-normal (rasen/org-fc-todos-hydra-hint "normal" 'good))
  ("l" #'rasen/org-fc-todos-rate-later  (rasen/org-fc-todos-hydra-hint "later"  'easy))
  ("o" #'org-open-at-point "open")
  ("t" #'rasen/org-fc-todos-today "today")
  ("S" #'rasen/org-fc-todos-suspend "suspend")
  ("d" #'rasen/org-fc-todos-delete "delete"))

(defun rasen/org-fc-todos-review-with-context (ctx)
  (setq rasen/org-fc-todos-current-context ctx)
  (rasen/org-fc-todos-next-card)
  (when rasen/org-fc-todos-current-card
    (rasen/hydra-todos-review/body)))

(defun rasen/todos-review ()
  (interactive)
  (rasen/org-fc-todos-review-with-context rasen/org-fc-todos-context))

(defun rasen/writing-review ()
  (interactive)
  (rasen/org-fc-todos-review-with-context rasen/org-fc-writing-inbox-context))

toggle markup/view

(defun rasen/toggle-org-view-mode (arg)
  (interactive "P")
  ;; partially stolen from `org-toggle-link-display'
  (if org-link-descriptive
      (progn
        (remove-from-invisibility-spec '(org-link))
        (setq-local org-hide-emphasis-markers nil)
        (setq-local org-pretty-entities nil)
        (org-clear-latex-preview (point-min) (point-max))
        (org-remove-inline-images)
        (valign-mode -1)
        (when arg
          (org-variable-pitch-minor-mode -1)
          (valign-mode -1))
        (message "org-view-mode disabled"))

    (add-to-invisibility-spec '(org-link))
    (setq org-hide-emphasis-markers t)
    (setq-local org-pretty-entities t)
    (org--latex-preview-region (point-min) (point-max))
    (org-display-inline-images)
    (org-variable-pitch-minor-mode t)
    (valign-mode t)
    (message "org-view-mode enabled"))
  (setq-local org-link-descriptive (not org-link-descriptive))
  (font-lock-fontify-buffer))

(leader-def 'normal 'org-mode-map
  "\\" #'rasen/toggle-org-view-mode)

Evilify org-mode

(use-package evil-org
  :after org
  :diminish
  :init
  ;; https://github.com/Somelauw/evil-org-mode/issues/93
  (fset 'evil-redirect-digit-argument 'ignore)

  :custom
  ;; https://github.com/Somelauw/evil-org-mode/issues/93
  (add-to-list 'evil-digit-bound-motions 'evil-org-beginning-of-line)
  (evil-define-key 'motion 'evil-org-mode
    (kbd "0") 'evil-org-beginning-of-line)

  ;; swap j/k
  (evil-org-movement-bindings '((up . "j")
                                (down . "k")
                                (left . "h")
                                (right . "l")))
  :config
  (add-hook 'org-mode-hook 'evil-org-mode)
  (add-hook 'evil-org-mode-hook
            (lambda ()
              (evil-org-set-key-theme)))
  (require 'evil-org-agenda)
  (evil-org-agenda-set-keys)

  ;; when editing code blocks, use current window and do not
  ;; reorganize my frame
  (setq org-src-window-setup 'current-window)

  (general-def 'normal org-mode-map
    "'"        #'org-edit-special
    "C-c '"    (rasen/hard-way "'")
    "go"       #'org-open-at-point
    "C-c C-o"  (rasen/hard-way "go"))

  ;; open file links in the same window
  (push '(file . find-file) org-link-frame-setup)

  (general-def 'normal org-src-mode-map
    "'"      #'org-edit-src-exit
    "C-c '"  (rasen/hard-way "'"))

  (general-def 'motion org-agenda-mode-map
    "k"    #'org-agenda-next-line
    "j"    #'org-agenda-previous-line
    "gk"   #'org-agenda-next-item
    "gj"   #'org-agenda-previous-item
    "C-k"  #'org-agenda-next-item
    "C-j"  #'org-agenda-previous-item

    "K"    #'org-agenda-priority-down
    "J"    #'org-agenda-priority-up

    "M-k"  #'org-agenda-drag-line-forward
    "M-j"  #'org-agenda-drag-line-backward)

  (general-def 'motion org-agenda-mode-map
    "SPC"      nil ;; unset prefix
    "go"       #'org-agenda-open-link
    "gl"       #'org-agenda-log-mode)
  (leader-def 'motion org-agenda-mode-map
    "SPC"  #'org-save-all-org-buffers
    "s"    #'org-agenda-schedule
    "d"    #'org-agenda-deadline
    "w"    #'rasen/org-refile-hydra/body
    "t"    #'rasen/org-agenda-todo))

Use emacs-state in org-lint buffers.

(evil-set-initial-state 'org-lint--report-mode 'emacs)
(evil-set-initial-state 'epa-key-list-mode 'emacs)

Timestamps

Configure time-stamp for org-mode, so time-stamp command would update #+LAST_MODIFIED: or #+DATE: keyword value (whichever is found first) using ISO8601 format.

(use-package time-stamp
  :config
  (add-hook 'org-mode-hook
            (defun rasen/set-time-stamp ()
              (interactive)
              ;; Either DATE or LAST_MODIFIED—whichever comes first, wins.
              (setq-local time-stamp-pattern "^#\\+\\(DATE\\|LAST_MODIFIED\\): %%$")
              (setq-local time-stamp-format "%Y-%02m-%02dT%02H:%02M:%02S%5z"))))

Fix focus steal when inserting “.” in date field

I have an issue with EXWM and org-mode: whenever I am asked to input a date (schedule, deadline) and I put dot (“.”) as a first character, it is not inserted and the focus is stolen to the tray icons and I have to manually refocus minibuffer to continue input.

I have debugged the issue to be caused by org-read-date-minibuffer-local-map binding for ., which is defined as:

(org-defkey map (kbd ".")
            (lambda () (interactive)
              ;; Are we at the beginning of the prompt?
              (if (looking-back "^[^:]+: "
                                (let ((inhibit-field-text-motion t))
                                  (line-beginning-position)))
                  (org-eval-in-calendar '(calendar-goto-today))
                (insert "."))))

I am not sure how but the if-branch causes the issue. (Somehow, it causes mouse cursor to jump.) So… just redefine the binding:

(org-defkey org-read-date-minibuffer-local-map (kbd ".") #'self-insert-command)

With self-insert-command, it works just fine. Well, the calendar doesn’t jump to today but who cares.

Mail setup

My email load is handled by mbsync (to download email), msmtp (to send mail), and notmuch (to tag and browse it).

Applications are configured with Home Manager, and notmuch frontend is configured in Emacs.

Applications

{
  # Store mails in ~/Mail
  accounts.email.maildirBasePath = "Mail";

  accounts.email.accounts.as = {
    realName = "Oleksii Shmalko";
    address = "[email protected]";
    flavor = "plain";

    userName = "[email protected]";
    imap.host = "imap.secureserver.net";
    imap.port = 993;
    imap.tls.enable = true;
    smtp.host = "smtpout.secureserver.net";
    smtp.port = 465;
    smtp.tls.enable = true;

    passwordCommand = "pass [email protected]";
    maildir.path = "alexeyshmalko";

    msmtp.enable = true;
    notmuch.enable = true;
    mbsync.enable = true;
    mbsync.create = "maildir";
  };

  # Use mbsync to fetch email. Configuration is constructed manually
  # to keep my current email layout.
  programs.mbsync = {
    enable = true;
    extraConfig = lib.mkBefore ''
      MaildirStore local
      Path ~/Mail/
      Inbox ~/Mail/INBOX
      SubFolders Verbatim
    '';
  };

  # Notmuch for email browsing, tagging, and searching.
  programs.notmuch = {
    enable = true;
    new.ignore = [
      ".mbsyncstate"
      ".mbsyncstate.lock"
      ".mbsyncstate.new"
      ".mbsyncstate.journal"
      ".uidvalidity"
      "dovecot-uidlist"
      "dovecot-keywords"
      "dovecot.index"
      "dovecot.index.log"
      "dovecot.index.log.2"
      "dovecot.index.cache"
      "/^archive/"
    ];
  };

  # msmtp for sending mail
  programs.msmtp.enable = true;

  # My Maildir layout predates home-manager configuration, so I do not
  # use mbsync config generation from home-manager, to keep layout
  # compatible.
  imports =
    let
      emails = [
        { name = "gmail";   email = "[email protected]";    path = "Personal"; primary = true; }
        { name = "ps";      email = "[email protected]"; path = "protocolstandard"; }
        { name = "egoless"; email = "[email protected]";         path = "egoless"; }
      ];
      mkGmailBox = { name, email, path, ... }@all: {
        accounts.email.accounts.${name} = {
          realName = "Oleksii Shmalko";
          address = email;
          flavor = "gmail.com";

          passwordCommand = "pass imap.gmail.com/${email}";
          maildir.path = path;

          msmtp.enable = true;
          notmuch.enable = true;
        } // (removeAttrs all ["name" "email" "path"]);

        programs.mbsync.extraConfig = ''
          IMAPAccount ${name}
          Host imap.gmail.com
          User ${email}
          PassCmd "pass imap.gmail.com/${email}"
          SSLType IMAPS
          CertificateFile /etc/ssl/certs/ca-certificates.crt

          IMAPStore ${name}-remote
          Account ${name}

          Channel sync-${name}-all
          Far :${name}-remote:"[Gmail]/All Mail"
          Near :local:${path}/all
          Create Both
          SyncState *

          Channel sync-${name}-spam
          Far :${name}-remote:"[Gmail]/Spam"
          Near :local:${path}/spam
          Create Both
          SyncState *

          Channel sync-${name}-sent
          Far :${name}-remote:"[Gmail]/Sent Mail"
          Near :local:${path}/sent
          Create Both
          SyncState *

          Group sync-${name}
          Channel sync-${name}-all
          Channel sync-${name}-spam
          Channel sync-${name}-sent

        '';
      };
    in map mkGmailBox emails;
}

Interface

(use-package notmuch
  :config
  ;; (setq mm-text-html-renderer 'shr)

  (setq notmuch-archive-tags '("-unread"))
  (setq notmuch-saved-searches
        '(
          (:name "unread-inbox" :query "tag:inbox and tag:unread" :key "u")
          ;; (:name "unread" :query "tag:unread and not tag:nixos and not tag:rust" :key "u")
          (:name "unread-egoless" :query "tag:egoless and tag:unread" :key "e")
          (:name "unread-nixos" :query "tag:unread and tag:nixos and not tag:nixpkgs" :key "n")
          (:name "unread-nixpkgs" :query "tag:unread and tag:nixpkgs" :key "p")
          (:name "unread-participating" :query "tag:unread and tag:participating" :key "t")
          (:name "unread-doctoright" :query "tag:unread and tag:doctoright" :key "d")
          (:name "unread-rss" :query "tag:unread and tag:rss" :key "r")
          (:name "unread-other" :query "tag:unread and not tag:nixos and not tag:inbox and not tag:doctoright and not tag:rss" :key "o")
          (:name "later" :query "tag:later" :key "l")
          (:name "flagged" :query "tag:flagged" :key "F")
          (:name "personal" :query "tag:personal" :key "P")
          (:name "doctoright" :query "tag:doctoright" :key "D")
          (:name "sent" :query "tag:sent" :key "s")
          (:name "drafts" :query "tag:draft" :key "f")
          (:name "all mail" :query "*" :key "a")))
  (setq notmuch-hello-sections
        '(;; notmuch-hello-insert-header
          notmuch-hello-insert-saved-searches
          ;; notmuch-hello-insert-search
          notmuch-hello-insert-alltags
          notmuch-hello-insert-recent-searches
          ;; notmuch-hello-insert-footer
          ))
  (setq-default notmuch-show-indent-content nil)

  (defun rasen/mbsync ()
    (interactive)
    (let ((mbsync-cmd (if (string= (system-name) "omicron")
                          "mbsync sync-gmail & r2e run & wait; /home/rasen/dotfiles/notmuch.sh"
                        "mbsync -a && ~/dotfiles/notmuch.sh")))
      (async-shell-command mbsync-cmd "*mbsync*")))

  ;; bind q in shell to hide buffer
  (general-def 'motion shell-mode-map
    "q" #'quit-window)

  (defun rasen/notmuch-search-mute ()
    (interactive)
    (notmuch-search-tag '("+muted"))
    (notmuch-search-archive-thread))

  (general-def notmuch-hello-mode-map "f" 'rasen/mbsync)

  (general-def
    :keymaps '(notmuch-hello-mode-map
               notmuch-search-mode-map
               notmuch-show-mode-map)
    "g" 'notmuch-refresh-all-buffers)

  (general-def 'notmuch-search-mode-map
    "k" #'notmuch-search-archive-thread
    "m" #'rasen/notmuch-search-mute)
  (general-def 'notmuch-show-mode-map
    "k" #'notmuch-show-archive-thread-then-next)
  ;; remap old function
  (general-def '(notmuch-search-mode-map
                 notmuch-show-mode-map)
    "K" #'notmuch-tag-jump)

  (general-def 'notmuch-show-mode-map
    "C" #'rasen/org-capture-link)

  (general-def 'notmuch-show-mode-map
    "M-u" (lambda ()
            (interactive)
            (notmuch-show-tag '("+unread"))))

  ;; notmuch-tag-formats

  (setq-default notmuch-tagging-keys
                '(("a" notmuch-archive-tags "Archive")
                  ("u" notmuch-show-mark-read-tags "Mark read")
                  ("m" ("+muted") "Mute")
                  ("f" ("+flagged") "Flag")
                  ("s" ("+spam" "-inbox") "Mark as spam")
                  ("d" ("+deleted" "-inbox") "Delete")))

  ;; support for linking notmuch mails in org-mode
  (require 'ol-notmuch))

Open html links in browser:

{
  home.file.".mailcap".text = ''
    text/html; firefox %s
    application/pdf; zathura %s
  '';
}

Emacs

(setq user-full-name "Oleksii Shmalko"
      user-mail-address "[email protected]")

Email sending.

(use-package message
  :ensure nil ; built-in
  :config
  (setq message-send-mail-function 'message-send-mail-with-sendmail
        message-sendmail-f-is-evil t
        message-sendmail-envelope-from nil ; 'header
        message-sendmail-extra-arguments '("--read-envelope-from"))

  (setq mml-secure-smime-sign-with-sender t)
  (setq mml-secure-openpgp-sign-with-sender t)

  ;; Add signature by default
  (add-hook 'message-setup-hook 'mml-secure-message-sign-pgpmime)
  ;; Verify other's signatures
  (setq mm-verify-option 'always))

(use-package sendmail
  :ensure nil ; built-in
  :config
  (setq mail-specify-envelope-from nil
        send-mail-function 'message-send-mail-with-sendmail
        sendmail-program "msmtp"))

rss2email

Install rss2email to deliver RSS feeds to my maildir.

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

Applications

Here go applications (almost) every normal user needs.

GPG

{
  programs.gnupg.agent = {
    enable = true;
    enableSSHSupport = true;
    pinentryFlavor = "qt";
  };

  ## is it no longer needed?
  #
  # systemd.user.sockets.gpg-agent-ssh = {
  #   wantedBy = [ "sockets.target" ];
  #   listenStreams = [ "%t/gnupg/S.gpg-agent.ssh" ];
  #   socketConfig = {
  #     FileDescriptorName = "ssh";
  #     Service = "gpg-agent.service";
  #     SocketMode = "0600";
  #     DirectoryMode = "0700";
  #   };
  # };

  services.pcscd.enable = true;
}

Install on macOS:

{
  environment.systemPackages = [ pkgs.gnupg ];
  programs.gnupg.agent = {
    enable = true;
    enableSSHSupport = true;
  };
}

Request passwords in Emacs minibuffer. (emacs-lisp)

(setq epa-pinentry-mode 'loopback)

Fix epg–filter-revoked-keys wrongly filtering out my keys

Reported as: bug#46138: 28.0.50; epg–filter-revoked-keys filters out valid keys

Temporary workaround:

(defun epg--filter-revoked-keys (keys)
  keys)

Yubikey

{
  environment.systemPackages = [
    pkgs.yubikey-manager
    pkgs.yubikey-personalization
    pkgs.yubikey-personalization-gui
  ];

  services.udev.packages = [
    pkgs.yubikey-personalization
    pkgs.libu2f-host
  ];
}
{
  home.packages = [
    pkgs.stable.yubikey-manager
    pkgs.stable.yubikey-personalization
  ];
}

password-store

Install password-store along with one-time password extension.

{
  home.packages = [
    (pkgs.pass.withExtensions (exts: [ exts.pass-otp exts.pass-audit exts.pass-genphrase ]))
    pkgs.qrencode
  ];
}

Install browserpass firefox extension backend.

{
  programs.browserpass = {
    enable = true;
    browsers = ["firefox" "chrome"];
  };
}

Integration with Emacs. (emacs-lisp)

(use-package ivy-pass
  :commands (ivy-pass))

(use-package pass
  :commands (pass))

KDE apps

I don’t use full KDE but some apps are definitely nice.

{
  home.packages = pkgs.lib.linux-only [
    pkgs.gwenview
    pkgs.dolphin
    # pkgs.kdeFrameworks.kfilemetadata
    pkgs.filelight
    pkgs.shared-mime-info
  ];
}

KDE apps might have issues with mime types without this:

{
  environment.pathsToLink = [ "/share" ];
}

Zathura

Zathura is a cool document viewer with Vim-like bindings.

{
  programs.zathura = {
    enable = true;
    options = {
      incremental-search = true;
    };

    # Swap j/k (for Workman layout)
    extraConfig = ''
      map j scroll up
      map k scroll down
    '';
  };
}

User applications

{
  home.packages = [
    (pkgs.lib.linux-only pkgs.google-play-music-desktop-player)
    (pkgs.lib.linux-only pkgs.tdesktop) # Telegram
    pkgs.feh

    pkgs.mplayer
    (pkgs.lib.linux-only pkgs.smplayer)
  ];
}

Development

Vim

Install Vim as my backup editor. (<<nixos-section>>)

{
  environment.systemPackages = [
    pkgs.vim_configurable
  ];
}

For Home Manager–managed hosts.

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

Link its configuration.

{
  home.file.".vim".source = ./.vim;
  home.file.".vimrc".source = ./.vim/init.vim;
}

Terminal / shell

rxvt-unicode

I use urxvt as my terminal emulator.

{
  programs.urxvt = {
    enable = true;
    iso14755 = false;

    fonts = [
      "-*-terminus-medium-r-normal-*-32-*-*-*-*-*-iso10646-1"
    ];

    scroll = {
      bar.enable = false;
      lines = 65535;
      scrollOnOutput = false;
      scrollOnKeystroke = true;
    };
    extraConfig = {
      "loginShell" = "true";
      "urgentOnBell" = "true";
      "secondaryScroll" = "true";

      # Molokai color theme
      "background" = "#101010";
      "foreground" = "#d0d0d0";
      "color0" = "#101010";
      "color1" = "#960050";
      "color2" = "#66aa11";
      "color3" = "#c47f2c";
      "color4" = "#30309b";
      "color5" = "#7e40a5";
      "color6" = "#3579a8";
      "color7" = "#9999aa";
      "color8" = "#303030";
      "color9" = "#ff0090";
      "color10" = "#80ff00";
      "color11" = "#ffba68";
      "color12" = "#5f5fee";
      "color13" = "#bb88dd";
      "color14" = "#4eb4fa";
      "color15" = "#d0d0d0";
    };
  };
}

Urxvt gets its setting from .Xresources file. If you ever want to reload it on-the-fly, type the following (or press C-c C-c if you’re reading this document in emacs now):

xrdb ~/.Xresources

vterm

vterm is a terminal emulator using libvterm. Which allows running it within Emacs.

(use-package vterm
  :commands vterm
  :config
  (setq vterm-kill-buffer-on-exit t)

  (add-hook 'vterm-mode-hook #'rasen/disable-hl-line-mode)
  ;; (push 'vterm-mode evil-motion-state-modes)
  (general-def 'vterm-mode-map
    "<f1>" nil
    "<f2>" nil
    "<f3>" nil
    "<f4>" nil
    "<f5>" nil
    "<f6>" nil
    "<f7>" nil
    "<f8>" nil
    "<f9>" nil
    "<f10>" nil
    "<f11>" nil
    "<f12>" nil
    "<return>" nil
    "RET" nil)
  (general-def 'normal 'vterm-mode-map
    "RET" nil)
  (general-def 'insert 'vterm-mode-map
    "<f1>" #'vterm--self-insert
    "<f2>" #'vterm--self-insert
    "<f3>" #'vterm--self-insert
    "<f4>" #'vterm--self-insert
    "<f5>" #'vterm--self-insert
    "<f6>" #'vterm--self-insert
    "<f7>" #'vterm--self-insert
    "<f8>" #'vterm--self-insert
    "<f9>" #'vterm--self-insert
    "<f10>" #'vterm--self-insert
    "<f11>" #'vterm--self-insert
    "<f12>" #'vterm--self-insert
    "C-h" #'vterm--self-insert
    "RET" #'vterm-send-return
    "C-c" nil
    "C-c C-c" #'vterm-send-C-c)
  (general-def '(insert normal) 'vterm-mode-map
    "<C-return>" #'vterm-send-return
    "C-c C-z" #'evil-collection-vterm-toggle-send-escape
    "C-c z" #'evil-collection-vterm-toggle-send-escape)

  (setq vterm-buffer-name-string "vterm %s"))

Fish configuration to work with vterm (<<home-manager-section>>):

{
  programs.fish = {

    interactiveShellInit = ''
      function vterm_prompt_end;
        vterm_printf '51;A'(whoami)'@'(hostname)':'(pwd)
      end
      functions --copy fish_prompt vterm_old_fish_prompt
      function fish_prompt --description 'Write out the prompt; do not replace this. Instead, put this at end of your file.'
        # Remove the trailing newline from the original prompt. This is done
        # using the string builtin from fish, but to make sure any escape codes
        # are correctly interpreted, use %b for printf.
        printf "%b" (string join "\n" (vterm_old_fish_prompt))
        vterm_prompt_end
      end
    '';

    functions.vterm_printf = ''
      function vterm_printf;
        if [ -n "$TMUX" ]
          # tell tmux to pass the escape sequences through
          # (Source: http://permalink.gmane.org/gmane.comp.terminal-emulators.tmux.user/1324)
          printf "\ePtmux;\e\e]%s\007\e\\" "$argv"
        else if string match -q -- "screen*" "$TERM"
          # GNU screen (screen, screen-256color, screen-256color-bce)
          printf "\eP\e]%s\007\e\\" "$argv"
        else
          printf "\e]%s\e\\" "$argv"
        end
      end
    '';

    functions.vterm_cmd = ''
      function vterm_cmd --description 'Run an emacs command among the ones been defined in vterm-eval-cmds.'
          set -l vterm_elisp ()
          for arg in $argv
            set -a vterm_elisp (printf '"%s" ' (string replace -a -r '([\\\\"])' '\\\\\\\\$1' $arg))
          end
          vterm_printf '51;E'(string join "" $vterm_elisp)
      end
    '';

    # Use current directory as title. The title is picked up by
    # `vterm-buffer-name-string` in emacs.
    #
    # prompt_pwd is like pwd, but shortens directory name:
    # /home/rasen/dotfiles -> ~/dotfiles
    # /home/rasen/prg/project -> ~/p/project
    functions.fish_title = ''
      function fish_title
        prompt_pwd
      end
    '';
  };
}

Run command in vterm. Adapter from r/emacs: Run shell command in vterm. (emacs-lisp)

(defun rasen/vterm-start--vterm-kill (process event)
  "A process sentinel. Kills PROCESS's buffer if it is live."
  (let ((b (process-buffer process)))
    (and (buffer-live-p b)
         (kill-buffer b))))

(defun rasen/vterm-start (command)
  "Execute string COMMAND in a new vterm.

Interactively, prompt for COMMAND with the current buffer's file
name supplied. When called from Dired, supply the name of the
file at point.

Like `async-shell-command`, but run in a vterm for full terminal features.

The new vterm buffer is named in the form `*foo bar.baz*`, the
command and its arguments in earmuffs.

When the command terminates, the shell remains open, but when the
shell exits, the buffer is killed."
  (interactive
   (list
    (let* ((f (cond (buffer-file-name)
                    ((eq major-mode 'dired-mode)
                     (dired-get-filename nil t))))
           (filename (and f (concat " " (shell-quote-argument ((file-relative-name f)))))))
      (read-shell-command "Terminal command: "
                          (cons filename 0)
                          (cons 'shell-command-history 1)
                          (and filename (list filename))))))
  (with-current-buffer (vterm (concat "*" command "*"))
    (set-process-sentinel vterm--process #'rasen/vterm-start--vterm-kill)
    (vterm-send-string command)
    (vterm-send-return)))

fish

fish is a cool shell, I use it as my default for day-to-day work.

{
  programs.fish.enable = true;
  users.defaultUserShell = pkgs.fish;
}

For home-manager:

{
  programs.fish = {
    enable = true;
    shellAliases = {
      g = "git";
      rm = "rm -r";
      ec = "emacsclient";
    };
    functions = {
      # old stuff
      screencast = ''
        function screencast
            # key-mon --meta --nodecorated --theme=big-letters --key-timeout=0.05 &
            ffmpeg -probesize 3000000000 -f x11grab -framerate 25 -s 3840x3960 -i :0.0 -vcodec libx264 -threads 2 -preset ultrafast -crf 0 ~/tmp/record/record-(date +"%FT%T%:z").mkv
            # killall -r key-mon
        end
      '';
      reencode = ''
        function reencode
            ffmpeg -i file:$argv[1] -c:v libx264 -crf 0 -preset veryslow file:(basename $argv[1] .mkv).crf-0.min.mkv
        end
      '';
    };
  };

  # manage other shells as well
  programs.bash.enable = true;
  programs.zsh.enable = true;
}

For macOS:

{
  programs.fish.enable = true;
  programs.zsh.enable = true;
  # add fish to default shells
  environment.shells = [ pkgs.fish ];
  # set it as default for me
  users.users.rasen.shell = pkgs.fish;
}

Vi key bindings

{
  programs.fish.functions.fish_user_key_bindings = ''
    function fish_user_key_bindings
        fish_vi_key_bindings

        bind -s j up-or-search
        bind -s k down-or-search
        bind -s -M visual j up-line
        bind -s -M visual k down-line

        bind -s '.' repeat-jump
    end
  '';
}

eshell

Eshell is a shell written in Emacs lisp. It offers great integration with Emacs.

(use-package eshell
  :commands (eshell eshell-exec-visual)
  :init
  (defun rasen/nixos-rebuild ()
    (interactive)
    (eshell-exec-visual "sudo" "nixos-rebuild" "switch"))

  :config
  (with-eval-after-load 'em-term
    (add-to-list 'eshell-visual-commands "nethack")
    (setq eshell-destroy-buffer-when-process-dies t)))
(use-package fish-completion
  :when (executable-find "fish")
  :config
  (global-fish-completion-mode))

tmux

(<<nixos-section>>)

{
  environment.systemPackages = [ pkgs.tmux ];
}

For home-manager. (<<home-manager-section>>)

{
  programs.tmux = {
    enable = true;
    keyMode = "vi";
    # Use C-a as prefix
    shortcut = "a";
    # To make vim work properly
    terminal = "screen-256color";

    # start numbering from 1
    baseIndex = 1;
    # Allows for faster key repetition
    escapeTime = 0;
    historyLimit = 10000;

    reverseSplit = true;

    clock24 = true;

    extraConfig = ''
      bind-key S-left swap-window -t -1
      bind-key S-right swap-window -t +1

      bind h select-pane -L
      bind k select-pane -D
      bind j select-pane -U
      bind l select-pane -R

      bind r source-file ~/.tmux.conf \; display-message "Config reloaded..."

      set-window-option -g automatic-rename
    '';
  };
}

Other terminal goodies

{
  home.packages = [ pkgs.dtach ];
}
{
  environment.systemPackages = [
    pkgs.wget
    pkgs.htop
    pkgs.psmisc
    pkgs.zip
    pkgs.unzip
    pkgs.unrar
    pkgs.bind
    pkgs.file
    pkgs.which
    # pkgs.utillinuxCurses
    pkgs.ripgrep
    pkgs.xclip

    pkgs.patchelf

    pkgs.python3
  ];
  # environment.variables.NPM_CONFIG_PREFIX = "$HOME/.npm-global";
  # environment.variables.PATH = "$HOME/.npm-global/bin:$PATH";
}

git

git config

(<<home-manager-section>>)

{
  programs.git = {
    enable = true;
    package = pkgs.gitAndTools.gitFull;

    userName = "Oleksii Shmalko";
    userEmail = "[email protected]";

    # signing = {
    #   key = "EB3066C3";
    #   signByDefault = true;
    # };

    extraConfig = {
      sendemail = {
        smtpencryption = "ssl";
        smtpserver = "smtp.gmail.com";
        smtpuser = "[email protected]";
        smtpserverport = 465;
      };

      color.ui = true;
      core.editor = "vim";
      push.default = "simple";
      pull.rebase = true;
      rebase.autostash = true;
      rerere.enabled = true;
      advice.detachedHead = false;
    };
  };
}

I have LOTS of aliases (<<home-manager-section>>)

{
  programs.git.aliases = {
    cl    = "clone";
    gh-cl = "gh-clone";
    cr    = "cr-fix";
    p     = "push";
    pl    = "pull";
    f     = "fetch";
    fa    = "fetch --all";
    a     = "add";
    ap    = "add -p";
    d     = "diff";
    dl    = "diff HEAD~ HEAD";
    ds    = "diff --staged";
    l     = "log --show-signature";
    l1    = "log -1";
    lp    = "log -p";
    c     = "commit";
    ca    = "commit --amend";
    co    = "checkout";
    cb    = "checkout -b";
    cm    = "checkout origin/master";
    de    = "checkout --detach";
    fco   = "fetch-checkout";
    br    = "branch";
    s     = "status";
    re    = "reset --hard";
    r     = "rebase";
    rc    = "rebase --continue";
    ri    = "rebase -i";
    m     = "merge";
    t     = "tag";
    su    = "submodule update --init --recursive";
    bi    = "bisect";
  };
}

Always push to github with ssh keys instead of login/password. (<<home-manager-section>>)

{
  programs.git.extraConfig = {
    url."[email protected]:".pushInsteadOf = "https://github.com";
  };
}

Also, install git for the rest of the system. (<<nixos-section>>)

{
  environment.systemPackages = [ pkgs.git ];
}

magit

Magit is a cool emacs interface to git. (emacs-lisp)

(use-package magit
  :general
  (:states 'motion
   "g m"    #'magit-status)
  :diminish auto-revert-mode
  :config
  <<magit-config>>
  )

Do not put files into trash can. Delete them for real. (<<magit-config>>)

(setq-default magit-delete-by-moving-to-trash nil)

Integrate with ivy. (<<magit-config>>)

(setq-default magit-completing-read-function 'ivy-completing-read)

Use q to quit transient buffers.

(use-package transient
  :defer t
  :config
  (transient-bind-q-to-quit))

Custom commands

git push HEAD …

Add a magit command to push HEAD into a specified ref. Bound to p h.

(defun rasen/magit-push-head (target args)
  "Push HEAD to a branch read in the minibuffer."
  (interactive
   (list (magit-read-remote-branch "Push HEAD to"
                                   nil nil nil 'confirm)
         (magit-push-arguments)))
  (magit-git-push "HEAD" target args))

(transient-insert-suffix 'magit-push 'magit-push-other
  '(1 "h" "HEAD" rasen/magit-push-head))
git fetch origin/master && git checkout origin/master

(evil-magit)

(defun rasen/magit-fco (remote refspec args)
  "Fetch remote branch and checkout it (detached HEAD)."
  (interactive
   (let ((remote (magit-read-remote-or-url "Fetch from remote or url")))
     (list remote
           (magit-read-refspec "Fetch using refspec" remote)
           (magit-fetch-arguments))))
  (magit-git-fetch remote (cons refspec args))
  ;; FIXME: magit-checkout does not wait for git fetch to finish.
  (magit-checkout "FETCH_HEAD"))

(if (fboundp 'transient-insert-suffix)
    (transient-insert-suffix 'magit-fetch 'magit-fetch-modules
      '(1 "c" "checkout" rasen/magit-fco)))

(defun rasen/magit-fco-master ()
  "Fetch origin/master and checkout it."
  (interactive)
  (magit-git-fetch "origin" "master")
  (magit-checkout "origin/master"))

(evil-collection-define-key evil-collection-magit-state 'magit-mode-map
                       "g m" 'rasen/magit-fco-master)
add a detach head command (git checkout HEAD)

GPG

Show commit signatures in log. (<<magit-config>>)

(setq magit-log-arguments '("--graph" "--decorate" "--show-signature" "-n256"))

git-commit

git-commit is emacs mode for editing commit messages. (emacs-lisp)

(use-package git-commit
  :gfhook 'flyspell-mode

  :general
  (:keymaps 'with-editor-mode-map
   :states 'normal
   "'" #'with-editor-finish)

  :config
  (add-to-list 'evil-insert-state-modes 'with-editor-mode)
  (setq evil-normal-state-modes (delete 'git-commit-mode evil-normal-state-modes)))

diff-hl

diff-hl is an emacs package to highlight uncommitted changes.

(use-package diff-hl
  :after magit
  :config
  (require 'diff-hl-flydiff)
  (add-hook 'magit-post-refresh-hook 'diff-hl-magit-post-refresh)
  (diff-hl-flydiff-mode t)
  (global-diff-hl-mode t))

Man pages

This install a number of default man pages for the linux/posix system. (<<nixos-section>>)

{
  documentation = {
    man.enable = true;
    dev.enable = true;
  };

  environment.systemPackages = [
    pkgs.man-pages
    pkgs.stdman
    pkgs.posix_man_pages
    pkgs.stdmanpages
  ];
}

Emacs

Use spaces for indentation

Do not use tabs for indentation.

(setq-default indent-tabs-mode nil)

Make underscore part of words

Make ‘_’ a part of words so that commands like evil-forward-word-begin work properly.

(add-hook 'prog-mode-hook
          (lambda () (modify-syntax-entry ?_ "w")))

Color compilation mode

(require 'ansi-color)
(add-hook 'compilation-filter-hook
          (defun rasen/colorize-compilation-buffer ()
            (when (eq major-mode 'compilation-mode)
              (ansi-color-apply-on-region compilation-filter-start (point-max)))))

projectile

(use-package projectile
  :general
  (s-leader-def
    "p" #'projectile-command-map)
  ('motion
   ;; That works much better than the default
   "g f"    #'projectile-find-file-dwim
   "U"      #'projectile-find-file
   "<f3>"   #'projectile-test-project
   "<f4>"   #'projectile-compile-project
   "<f5>"   #'projectile-run-project)
  :commands (projectile-project-name)
  :diminish projectile-mode
  :config
  ;; Use the prefix arg if you want to change the compilation command
  (setq-default compilation-read-command nil)

  (setq-default projectile-use-git-grep t)

  ;; projectile-find-file is slow on very large projects.  Enable
  ;; known-files caching for projectile to speed it up.
  ;; (Note: clear cache with `projectile-invalidate-cache', or C-u U)
  ;; (setq-default projectile-enable-caching t)
  ;; set projectile-enable-caching on a per-project basis in .dir-locals.el

  (setq-default projectile-completion-system 'ivy)
  (projectile-mode))
(use-package counsel-projectile
  :after projectile
  :config
  (counsel-projectile-mode))

company

Company mode provides autocomplete features.

(use-package company
  :defer 2
  :general
  (:keymaps 'company-mode-map
   :states 'insert
   "C-n" #'company-complete-common-or-cycle
   "C-p" #'company-select-previous)
  ('company-active-map
   "C-n" #'company-complete-common-or-cycle
   "C-p" #'company-select-previous-or-abort
   "C-e" #'company-complete
   "TAB" #'company-complete-common-or-cycle)

  :diminish company-mode
  :config
  (setq-default company-dabbrev-downcase nil)
  (setq-default company-search-filtering t)
  (setq-default company-global-modes '(not org-mode))
  (global-company-mode))

Company-box frontend works better with variable-pitch fonts.

(use-package company-box
  :disabled t
  :diminish
  :hook (company-mode . company-box-mode))

Hippie expand

Useful for languages that do not support proper completion.

(use-package hippie-exp
  :general
  ('insert
   "C-/"  #'hippie-expand)
  :config
  (setq hippie-expand-try-functions-list
        '(try-expand-dabbrev-visible
          try-expand-dabbrev
          try-expand-dabbrev-all-buffers
          try-complete-file-name-partially
          try-complete-file-name
          try-expand-line
          try-expand-list)))

flycheck

(use-package flycheck
  :config
  ;; not sure I actually use nix-sandbox
  ;; (setq flycheck-command-wrapper-function
  ;;       (lambda (cmd) (apply 'nix-shell-command (nix-current-sandbox) cmd))

  ;;       flycheck-executable-find
  ;;       (lambda (cmd) (nix-executable-find (nix-current-sandbox) cmd)))

  ;; Do not check for elisp header/footer
  (setq-default flycheck-disabled-checkers
                (append flycheck-disabled-checkers
                        '(emacs-lisp-checkdoc)))

  (global-flycheck-mode))

flycheck-inline

Display flycheck error inline.

(use-package flycheck-inline
  :after flycheck
  :config
  (global-flycheck-inline-mode))

electric-pair

Auto-close pairs.

(electric-pair-mode)

Color identifiers

(use-package color-identifiers-mode
  :commands (color-identifiers-mode
             global-color-identifiers-mode)
  :diminish (color-identifiers-mode
             global-color-identifiers-mode))

dtrt-indent

Automatically determine indent style.

(use-package dtrt-indent
  :diminish
  :config
  (dtrt-indent-global-mode))

paren-face

Dim parens.

(use-package paren-face
  :config
  (global-paren-face-mode))

LSP

LSP is Language Server Protocol. Some languages support it usually provide better completion and diagnostics.

(use-package lsp-mode
  :disabled t
  :commands (lsp)
  :hook ((rust-mode haskell-mode) . lsp)
  :config
  (setq lsp-prefer-flymake nil)
  (setq lsp-rust-rls-server-command "rls")
  (setq lsp-rust-analyzer-server-command "rust-analyzer"))

(use-package lsp-ui
  :disabled t
  :commands lsp-ui-mode)

(use-package eglot
  :hook ((rust-mode) . eglot-ensure))

Commenting

(leader-def '(normal visual)  "/" #'comment-line)

yasnippet

(use-package yasnippet
  :defer 5
  :diminish yas-minor-mode
  :config
  (yas-global-mode 1)

  (setq rasen/snippets-directory
        (file-name-as-directory
         (expand-file-name ".emacs.d/snippets" rasen/dotfiles-directory)))

  (make-directory rasen/snippets-directory t)
  (yas-load-directory rasen/snippets-directory)

  ;; yasnippet's wrapping doesn't work well with evil. When you
  ;; trigger a snippet from a visual state, it switches into normal
  ;; state, but cursor moves in such a way, so that you leave the
  ;; snippet, so you're not able to supply other fields ($1, $2,
  ;; etc.).
  ;;
  ;; This function installed as `yas-before-expand-snippet-hook'
  ;; switches into insert state before expanding the snippet, so you
  ;; can supply all the fields.
  ;;
  ;; Note that it is not always desirable because some snippets don't
  ;; have extra fields, so switching to insert state has not sense.
  ;; In order for the switch to kick in, set `rasen/evil-state'
  ;; expand-env to "insert" like this:
  ;;
  ;; # expand-env: ((rasen/evil-state "insert"))
  (defun rasen/yas-before-expand-snippet ()
    (when (not (string-or-null-p snippet))
      (let ((state (car (alist-get 'rasen/evil-state (yas--template-expand-env snippet)))))
        (when (and (equal state "insert") (evil-visual-state-p))
          (let ((beg evil-visual-beginning)
                (end evil-visual-end))
            (evil-insert-state nil)
            ;; restore mark and point
            (if (eq (char-after beg) ?\s)
                ;; skip whitespaces, if present
                (progn
                  (goto-char beg)
                  (forward-whitespace 1)
                  (set-mark (point)))
              (set-mark beg))
            (goto-char end)
            (when (bolp)
              (backward-char)))))))

  (add-hook 'yas-before-expand-snippet-hook #'rasen/yas-before-expand-snippet)

  (add-hook 'term-mode-hook (lambda ()
                              (setq-local yas-dont-activate-functions t))))

Languages

Emacs lisp

(use-package elisp-mode
  :ensure nil ; built-in
  :config
  <<elisp-mode-config>>
  )

Eval last sexp Vim-style.

(evil-define-operator rasen/evil-eval (beg end type)
  "Evaluate region."
  (if (eq type 'block)
      (evil-apply-on-block 'eval-region beg end nil)
    (eval-region beg end)))

(general-def 'motion emacs-lisp-mode-map "SPC e" #'eval-last-sexp)
(general-def 'visual emacs-lisp-mode-map "SPC e" #'rasen/evil-eval)

aggressive indent

Keep lisp code always indented.

(use-package aggressive-indent
  :commands (aggressive-indent-mode aggressive-indent-global-mode)
  :hook
  (clojure-mode . aggressive-indent-mode)
  (clojurescript-mode . aggressive-indent-mode)
  (emacs-lisp-mode . aggressive-indent-mode)
  (lisp-mode . aggressive-indent-mode))

Fix indentation for keywords

Alternate indent function definition.

;; Fix the indentation of keyword lists in Emacs Lisp. See [1] and [2].
;;
;; Before:
;;  (:foo bar
;;        :baz quux)
;;
;; After:
;;  (:foo bar
;;   :bar quux)
;;
;; [1]: https://github.com/Fuco1/.emacs.d/blob/af82072196564fa57726bdbabf97f1d35c43b7f7/site-lisp/redef.el#L12-L94
;; [2]: http://emacs.stackexchange.com/q/10230/12534
(el-patch-defun (el-patch-swap lisp-indent-function rasen/emacs-lisp-indent-function) (indent-point state)
  "This function is the normal value of the variable `lisp-indent-function'.
The function `calculate-lisp-indent' calls this to determine
if the arguments of a Lisp function call should be indented specially.

INDENT-POINT is the position at which the line being indented begins.
Point is located at the point to indent under (for default indentation);
STATE is the `parse-partial-sexp' state for that position.

If the current line is in a call to a Lisp function that has a non-nil
property `lisp-indent-function' (or the deprecated `lisp-indent-hook'),
it specifies how to indent.  The property value can be:

* `defun', meaning indent `defun'-style
  (this is also the case if there is no property and the function
  has a name that begins with \"def\", and three or more arguments);

* an integer N, meaning indent the first N arguments specially
  (like ordinary function arguments), and then indent any further
  arguments like a body;

* a function to call that returns the indentation (or nil).
  `lisp-indent-function' calls this function with the same two arguments
  that it itself received.

This function returns either the indentation to use, or nil if the
Lisp function does not specify a special indentation."
  (el-patch-let (($cond (and (elt state 2)
                             (el-patch-wrap 1 1
                               (or (not (looking-at "\\sw\\|\\s_"))
                                   (looking-at ":")))))
                 ($then (progn
                          (if (not (> (save-excursion (forward-line 1) (point))
                                      calculate-lisp-indent-last-sexp))
                              (progn (goto-char calculate-lisp-indent-last-sexp)
                                     (beginning-of-line)
                                     (parse-partial-sexp (point)
                                                         calculate-lisp-indent-last-sexp 0 t)))
                          ;; Indent under the list or under the first sexp on the same
                          ;; line as calculate-lisp-indent-last-sexp.  Note that first
                          ;; thing on that line has to be complete sexp since we are
                          ;; inside the innermost containing sexp.
                          (backward-prefix-chars)
                          (current-column)))
                 ($else (let ((function (buffer-substring (point)
                                                          (progn (forward-sexp 1) (point))))
                              method)
                          (setq method (or (function-get (intern-soft function)
                                                         'lisp-indent-function)
                                           (get (intern-soft function) 'lisp-indent-hook)))
                          (cond ((or (eq method 'defun)
                                     (and (null method)
                                          (> (length function) 3)
                                          (string-match "\\`def" function)))
                                 (lisp-indent-defform state indent-point))
                                 ((integerp method)
                                 (lisp-indent-specform method state
                                                       indent-point normal-indent))
                                (method
                                 (funcall method indent-point state))))))
    (let ((normal-indent (current-column))
          (el-patch-add
            (orig-point (point))))
      (goto-char (1+ (elt state 1)))
      (parse-partial-sexp (point) calculate-lisp-indent-last-sexp 0 t)
      (el-patch-swap
        (if $cond
            ;; car of form doesn't seem to be a symbol
            $then
          $else)
        (cond
         ;; car of form doesn't seem to be a symbol, or is a keyword
         ($cond $then)
         ((and (save-excursion
                 (goto-char indent-point)
                 (skip-syntax-forward " ")
                 (not (looking-at ":")))
               (save-excursion
                 (goto-char orig-point)
                 (looking-at ":")))
          (save-excursion
            (goto-char (+ 2 (elt state 1)))
            (current-column)))
         (t $else))))))

Apply it for emacs-lisp-mode.

(add-hook 'emacs-lisp-mode-hook
          (lambda () (setq-local lisp-indent-function #'rasen/emacs-lisp-indent-function)))

Nix

Pretty self-explaining.

(use-package nix-mode
  :mode "\\.nix$")

Haskell

(Old un-reviewed stuff.)

(use-package haskell-mode
  :mode "\\.hs$"
  :init
  (setq company-ghc-show-info t)
  (setq flycheck-ghc-stack-use-nix t)
  :config
  (add-hook 'haskell-mode-hook #'interactive-haskell-mode)
  (add-hook 'haskell-mode-hook #'haskell-decl-scan-mode)
  (add-hook 'haskell-mode-hook #'lsp)

  (setq haskell-compile-cabal-build-command "cd %s && stack build")
  (setq haskell-compile-cabal-build-command-alt "cd %s && cabal build --ghc-options=-ferror-spans")

  ;; Use Nix for stack ghci
  (add-to-list 'haskell-process-args-stack-ghci "--fast --nix")
  (add-to-list 'haskell-process-args-stack-ghci "--test")

  ;; Use Nix for default build/test command
  (projectile-register-project-type 'haskell-stack
                                    '("stack.yaml")
                                    :compile "stack build --fast --nix"
                                    :test "stack build --nix --test")

  (general-def haskell-mode-map
    [f8]       #'haskell-navigate-imports
    "C-c C-b"  #'haskell-compile
    "C-c v c"  #'haskell-cabal-visit-file

    ;; haskell-interactive-mode
    "C-x C-d"  nil
    "C-c C-z"  #'haskell-interactive-switch
    "C-c C-l"  #'haskell-process-load-file
    "C-c C-t"  #'haskell-process-do-type
    "C-c C-i"  #'haskell-process-do-info
    "C-c M-."  nil
    "C-c C-d"  nil)

  ;; Disable popups (i.e., report errors in the interactive shell).
  (setq haskell-interactive-popup-errors nil)

  (setq haskell-process-suggest-remove-import-lines t
        haskell-process-auto-import-loaded-modules t)

  (with-eval-after-load 'align
    (add-to-list 'align-rules-list
                 '(haskell-types
                   (regexp . "\\(\\s-+\\)\\(::\\|∷\\)\\s-+")
                   (modes . '(haskell-mode literate-haskell-mode))))
    (add-to-list 'align-rules-list
                 '(haskell-assignment
                   (regexp . "\\(\\s-+\\)=\\s-+")
                   (modes . '(haskell-mode literate-haskell-mode))))
    (add-to-list 'align-rules-list
                 '(haskell-arrows
                   (regexp . "\\(\\s-+\\)\\(->\\|→\\)\\s-+")
                   (modes . '(haskell-mode literate-haskell-mode))))
    (add-to-list 'align-rules-list
                 '(haskell-left-arrows
                   (regexp . "\\(\\s-+\\)\\(<-\\|←\\)\\s-+")
                   (modes . '(haskell-mode literate-haskell-mode))))))
(use-package lsp-haskell
  :after haskell-mode lsp-mode)

Rust

(use-package eldoc
  :commands (eldoc-mode)
  :diminish eldoc-mode)

(use-package rust-mode
  :mode ("\\.rs$" . rust-mode)
  :config
  (add-hook 'rust-mode-hook (lambda () (setq-local fill-column 100)))
  (add-hook 'rust-mode-hook #'rust-enable-format-on-save))

(use-package racer
  :after rust-mode
  :commands racer-mode
  :diminish racer-mode
  :config
  (setq racer-rust-src-path nil) ; Nix manages that
  (add-hook 'rust-mode-hook #'racer-mode)
  (add-hook 'racer-mode-hook #'eldoc-mode))

(use-package flycheck-rust
  :after rust-mode
  :config
  (add-hook 'flycheck-mode-hook #'flycheck-rust-setup))

Go

(use-package go-mode
  :mode ("\\.go$" . go-mode))

C/C++

Doxygen

This const is taken from doxymacs and is subject to GPLv2. I’ve copied it my dotfiles as I don’t need all doxymacs features and setup is non-trivial. (It requires compilation, there is no melpa package.)

(defconst doxymacs-doxygen-keywords
  (list
   (list
    ;; One shot keywords that take no arguments
    (concat "\\([@\\\\]\\(brief\\|li\\|\\(end\\)?code\\|sa"
            "\\|note\\|\\(end\\)?verbatim\\|return\\|arg\\|fn"
            "\\|hideinitializer\\|showinitializer"
            "\\|parblock\\|endparblock"
            ;; FIXME
            ;; How do I get & # < > % to work?
            ;;"\\|\\\\&\\|\\$\\|\\#\\|<\\|>\\|\\%"
            "\\|internal\\|nosubgrouping\\|author\\|date\\|endif"
            "\\|invariant\\|post\\|pre\\|remarks\\|since\\|test\\|version"
            "\\|\\(end\\)?htmlonly\\|\\(end\\)?latexonly\\|f\\$\\|file"
            "\\|\\(end\\)?xmlonly\\|\\(end\\)?manonly\\|property"
            "\\|mainpage\\|name\\|overload\\|typedef\\|deprecated\\|par"
            "\\|addindex\\|line\\|skip\\|skipline\\|until\\|see"
            "\\|endlink\\|callgraph\\|endcond\\|else\\)\\)\\>")
    '(0 font-lock-keyword-face prepend))
   ;; attention, warning, etc. given a different font
   (list
    "\\([@\\\\]\\(attention\\|warning\\|todo\\|bug\\)\\)\\>"
    '(0 font-lock-warning-face prepend))
   ;; keywords that take a variable name as an argument
   (list
    (concat "\\([@\\\\]\\(param\\(?:\\s-*\\[\\(?:in\\|out\\|in,out\\)\\]\\)?"
            "\\|a\\|namespace\\|relates\\(also\\)?"
            "\\|var\\|def\\)\\)\\s-+\\(\\sw+\\)")
    '(1 font-lock-keyword-face prepend)
    '(4 font-lock-variable-name-face prepend))
   ;; keywords that take a type name as an argument
   (list
    (concat "\\([@\\\\]\\(class\\|struct\\|union\\|exception\\|enum"
            "\\|throw\\|interface\\|protocol\\)\\)\\s-+\\(\\(\\sw\\|:\\)+\\)")
    '(1 font-lock-keyword-face prepend)
    '(3 font-lock-type-face prepend))
   ;; keywords that take a function name as an argument
   (list
    "\\([@\\\\]retval\\)\\s-+\\([^ \t\n]+\\)"
    '(1 font-lock-keyword-face prepend)
    '(2 font-lock-function-name-face prepend))
   ;; bold
   (list
    "\\([@\\\\]b\\)\\s-+\\([^ \t\n]+\\)"
    '(1 font-lock-keyword-face prepend)
    '(2 (quote bold) prepend))
   ;; code
   (list
    "\\([@\\\\][cp]\\)\\s-+\\([^ \t\n]+\\)"
    '(1 font-lock-keyword-face prepend)
    '(2 (quote underline) prepend))
   ;; italics/emphasised
   (list
    "\\([@\\\\]e\\(m\\)?\\)\\s-+\\([^ \t\n]+\\)"
    '(1 font-lock-keyword-face prepend)
    '(3 (quote italic) prepend))
   ;; keywords that take a list
   (list
    "\\([@\\\\]ingroup\\)\\s-+\\(\\(\\sw+\\s-*\\)+\\)\\s-*$"
    '(1 font-lock-keyword-face prepend)
    '(2 font-lock-string-face prepend))
   ;; one argument that can contain arbitrary non-whitespace stuff
   (list
    (concat "\\([@\\\\]\\(link\\|copydoc\\|xrefitem"
            "\\|if\\(not\\)?\\|elseif\\)\\)"
            "\\s-+\\([^ \t\n]+\\)")
    '(1 font-lock-keyword-face prepend)
    '(4 font-lock-string-face prepend))
   ;; one optional argument that can contain arbitrary non-whitespace stuff
   (list
    "\\([@\\\\]\\(cond\\|dir\\)\\(\\s-+[^ \t\n]+\\)?\\)"
    '(1 font-lock-keyword-face prepend)
    '(3 font-lock-string-face prepend t))
   ;; one optional argument with no space between
   (list
    "\\([@\\\\]\\(~\\)\\([^ \t\n]+\\)?\\)"
    '(1 font-lock-keyword-face prepend)
    '(3 font-lock-string-face prepend t))
   ;; one argument that has to be a filename
   (list
    (concat "\\([@\\\\]\\(example\\|\\(dont\\)?include\\|includelineno"
            "\\|htmlinclude\\|verbinclude\\)\\)\\s-+"
            "\\(\"?[~:\\/a-zA-Z0-9_. ]+\"?\\)")
    '(1 font-lock-keyword-face prepend)
    '(4 font-lock-string-face prepend))
   ;; dotfile <file> ["caption"]
   (list
    (concat "\\([@\\\\]dotfile\\)\\s-+"
            "\\(\"?[~:\\/a-zA-Z0-9_. ]+\"?\\)\\(\\s-+\"[^\"]+\"\\)?")
    '(1 font-lock-keyword-face prepend)
    '(2 font-lock-string-face prepend)
    '(3 font-lock-string-face prepend t))
   ;; image <format> <file> ["caption"] [<sizeindication>=<size>]
   (list
    "\\([@\\\\]image\\)\\s-+\\(html\\|latex\\)\\s-+\\(\"?[~:\\/a-zA-Z0-9_. ]+\"?\\)\\(\\s-+\"[^\"]+\"\\)?\\(\\s-+\\sw+=[0-9]+\\sw+\\)?"
    '(1 font-lock-keyword-face prepend)
    '(2 font-lock-string-face prepend)
    '(3 font-lock-string-face prepend)
    '(4 font-lock-string-face prepend t)
    '(5 font-lock-string-face prepend t))
   ;; one argument that has to be a word
   (list
    (concat "\\([@\\\\]\\(addtogroup\\|defgroup\\|weakgroup"
            "\\|page\\|anchor\\|ref\\|section\\|subsection\\|subsubsection\\|paragraph"
            "\\)\\)\\s-+\\(\\sw+\\)")
    '(1 font-lock-keyword-face prepend)
    '(3 font-lock-string-face prepend))))

(defconst doxygen-font-lock-keywords
  `((,(lambda (limit)
        (c-font-lock-doc-comments "/\\(\\*[\\*!]\\|/[/!]\\)<?" limit
          doxymacs-doxygen-keywords)))))

(setq c-doc-comment-style '((java-mode . javadoc)
                            (pike-mode . autodoc)
                            (c-mode . doxygen)
                            (c++-mode . doxygen)))

CMake

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

Python

(use-package elpy
  :after (flycheck)
  :config
  ;; Do not show vertical guides in python code
  (setq elpy-modules (delq 'elpy-module-highlight-indentation elpy-modules))

  ;; Do not use flymake (flycheck will kick in instead)
  (setq elpy-modules (delq 'elpy-module-flymake elpy-modules))

  ;; Use python3 (instead of python2)
  (setq flycheck-python-flake8-executable "python3"
        flycheck-python-pycompile-executable "python3"
        flycheck-python-pylint-executable "python3")

  (elpy-enable))
(use-package blacken
  :hook (elpy-mode . blacken-mode))
(use-package py-autopep8
  :disabled t
  :hook (elpy-mode . py-autopep8-enable-on-save)
  :config
  (defun rasen/autopep8-disable-on-save ()
    (interactive)
    (remove-hook 'before-save-hook 'py-autopep8-buffer t)))
(use-package pip-requirements
  :mode "^requirements.txt$")

JavaScript

(use-package js2-mode
  :mode "\\.js$"
  :init
  (add-hook 'js2-mode-hook 'color-identifiers-mode)
  :config

  (defun rasen/use-eslint-from-node-modules ()
    (let* ((root (locate-dominating-file
                  (or (buffer-file-name) default-directory)
                  "node_modules"))
           (eslint (and root
                        (expand-file-name "node_modules/eslint/bin/eslint.js"
                                          root))))
      (when (and eslint (file-executable-p eslint))
        (setq-local flycheck-javascript-eslint-executable eslint))))
  (add-hook 'flycheck-mode-hook #'rasen/use-eslint-from-node-modules)

  (add-hook 'js2-mode-hook
            (lambda ()
              (flycheck-select-checker 'javascript-eslint)))

  (setq-default flycheck-disabled-checkers
                (append flycheck-disabled-checkers
                        '(javascript-jshint)))
  (setq-default flycheck-enabled-checkers
                (append flycheck-enabled-checkers
                        '(javascript-eslint)))

  (flycheck-add-mode 'javascript-eslint 'js2-mode)

  (setq-default js2-strict-trailing-comma-warning nil))

(use-package rjsx-mode
  :mode "\\.js$"
  :config
  (setq-default js-indent-level 2))

Typescript

(use-package typescript-mode
  :commands (typescript-mode)
  :gfhook
  #'rasen/setup-tide-mode
  #'abbrev-mode
  :init
  (el-patch-feature typescript-mode)
  (add-hook 'web-mode-hook
            (defun rasen/enable-typescript ()
              (when (member (file-name-extension buffer-file-name)
                            '("ts" "tsx" "js" "jsx"))
                (typescript-mode))))
  (add-hook 'rjsx-mode-hook #'rasen/enable-typescript)
  :general
  ('insert
   'typescript-mode-map
   "M-j"   #'c-indent-new-comment-line
   "C-M-j" #'c-indent-new-comment-line)
  :config
  (setq-default typescript-indent-level 2)

  ;; Add more jsdoc tags
  (el-patch-defconst typescript-jsdoc-empty-tag-regexp
    (concat typescript-jsdoc-before-tag-regexp
            "\\(@"
            (regexp-opt
             '("abstract"
               "addon"
               "async"
               "author"
               "class"
               "classdesc"
               "const"
               "constant"
               "constructor"
               "constructs"
               "copyright"
               "default"
               "defaultvalue"
               "deprecated"
               "desc"
               "description"
               "event"
               "example"
               "exec"
               "export"
               "exports"
               "file"
               "fileoverview"
               "final"
               "func"
               "function"
               "generator"
               "global"
               "hidden"
               "hideconstructor"
               "ignore"
               "implicitcast"
               "inheritdoc"
               "inner"
               "instance"
               "interface"
               "license"
               "method"
               "mixin"
               "noalias"
               "noshadow"
               "notypecheck"
               "override"
               "overview"
               "owner"
               "package"
               "preserve"
               "preservetry"
               "private"
               "protected"
               "public"
               "readonly"
               "static"
               "summary"
               "supported"
               "todo"
               "tutorial"
               "virtual"
               (el-patch-add
                 "remarks"
                 "alpha"
                 "beta"
                 "defaultValue"
                 "eventProperty"
                 "example"
                 "experimental"
                 "inheritDoc"
                 "internal"
                 "label"
                 "link"
                 "packageDocumentation"
                 "privateRemarks"
                 "sealed"
                 "typeParam")))
            "\\)\\s-*")
    "Matches empty jsdoc tags."))
(use-package tide
  :commands (tide-setup
             tide-hl-identifier-mode
             tide-format-before-save)
  :hook
  (typescript-mode . rasen/setup-tide-mode)
  :init
  (defun rasen/setup-tide-mode ()
    (interactive)
    (with-demoted-errors "tide-setup: %S"
      (tide-setup))
    (flycheck-mode +1)
    (setq flycheck-check-syntax-automatically '(save mode-enabled))
    (eldoc-mode +1)
    (tide-hl-identifier-mode +1)
    (company-mode +1))
  :config
  ;; K is used for avy
  (general-def 'normal 'tide-mode-map
    "K" nil))
(use-package prettier-js
  :commands (prettier-js prettier-js-mode)
  :hook
  (typescript-mode . prettier-js-mode))
(use-package flycheck-jest
  :after flycheck)

Svelte

(use-package svelte-mode
  :mode "\\.svelte$"
  :gfhook #'prettier-js-mode
  :config
  (setq svelte-basic-offset 2))

Vue

(use-package vue-mode
  :mode "\\.vue$")

Web-mode

(use-package web-mode
  :commands (web-mode)
  :init
  (add-to-list 'auto-mode-alist '("\\.blade.php\\'" . web-mode))
  (add-to-list 'auto-mode-alist '("\\.ts\\'" . web-mode))
  (add-to-list 'auto-mode-alist '("\\.tsx\\'" . web-mode))
  :config
  (setq web-mode-engines-alist
        '(("php"    . "\\.phtml\\'")
          ("blade"  . "\\.blade\\."))))

Clojure(Script)

(use-package clojure-mode
  :mode "\\.\\(clj\\|cljs\\)$")

(use-package cider
  :after clojure-mode
  :config
  (evil-set-initial-state 'cider-stacktrace-mode 'emacs))

Groovy

(use-package groovy-mode
  :mode "\\.\\(groovy\\|gradle\\)$")

Kotlin

(use-package kotlin-mode
  :mode "\\.kt'")

Forth

(use-package forth-mode
  :config
  (defun rasen/disable-electric-pair ()
    (interactive)
    (electric-pair-local-mode -1))
  (add-hook 'forth-mode-hook #'rasen/disable-electric-pair))

Lua

(use-package lua-mode
  :mode ("\\.lua$" . lua-mode)
  :config
  (setq lua-indent-level 4))

Ledger / Hledger

(use-package ledger-mode
  :mode "\\.journal$"
  :config
  (setq ledger-binary-path "hledger")
  (add-hook 'ledger-mode-hook 'orgstruct-mode))

Markdown

(use-package markdown-mode
  :mode ("\\.\\(markdown\\|mdown\\|md\\)$" . markdown-mode)
  :commands gfm-view-mode
  :init
  (add-hook 'markdown-mode-hook 'visual-line-mode)
  (add-hook 'markdown-mode-hook 'flyspell-mode)
  :config
  (defun rasen/insert-timestamp ()
    "Insert current timestamp in ISO 8601 format"
    (interactive)
    (insert (format-time-string "%FT%R%z")))

  (general-def 'normal markdown-mode-map
    "'"      #'markdown-edit-code-block
    "C-c '"  (rasen/hard-way "'"))
  (general-def 'normal edit-indirect-mode-map
    "'"      #'edit-indirect-commit
    "C-c '"  (rasen/hard-way "'"))
  (general-def 'insert markdown-mode-map
    "C-c ,"  #'rasen/insert-timestamp)

  (setq markdown-fontify-code-blocks-natively t))

Package edit-indirect needed to edit code blocks.

(use-package edit-indirect
  :after markdown-mode)

Markdown cliplink

(Uses org-cliplink.)

(defun rasen/md-link-transformer (url title)
  (if title
      (format "[%s](%s)"
              (org-cliplink-elide-string
               (org-cliplink-escape-html4
                (org-cliplink-title-for-url url title))
               org-cliplink-max-length)
              url)
    (format "<%s>" url)))

(defun rasen/md-cliplink ()
  "Takes a URL from the clipboard and inserts an markdown-mode link
with the title of a page found by the URL into the current
buffer"
  (interactive)
  (org-cliplink-insert-transformed-title (org-cliplink-clipboard-content)
                                         #'rasen/md-link-transformer))

WriteGood

See http://bnbeckwith.com/code/writegood-mode.html

(use-package writegood-mode
  :hook (markdown-mode . writegood-mode))

JSON

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

YAML

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

Jinja2

(use-package jinja2-mode
  :mode "\\.j2$")

Docker

(<<nixos-section>>)

{
  virtualisation.docker.enable = true;
}

(emacs-lisp)

(use-package dockerfile-mode
  :mode "Dockerfile"
  :config
  (setq dockerfile-mode-command "env SUDO_ASKPASS=/usr/bin/ssh-askpass sudo -A docker"))

restclient

(use-package restclient
  :mode "\\.http$")

terraform

(use-package terraform-mode
  :mode "\\.tf$")

graphviz

(use-package graphviz-dot-mode
  :mode "\\.dot$"
  :config
  (setq graphviz-dot-view-command "dotty %s"))

protobuf

(use-package protobuf-mode
  :mode "\\.proto$")

SQL

(use-package sql
  :commands (sql-mode
             sql-connect
             sql-oracle
             sql-sybase
             sql-informix
             sql-sqlite
             sql-mysql
             sql-solid
             sql-ingres
             sql-ms
             sql-postgres
             sql-interbase
             sql-db2
             sql-linter
             sql-vertica)
  :config
  (add-hook 'sql-mode-hook (lambda () (toggle-truncate-lines t))))

PlantUML

Install plantuml.

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

Emacs configuration.

(use-package plantuml-mode
  :config
  :disabled t
  (setq plantuml-executable-path (executable-find "plantuml"))
  (setq plantuml-jar-path (expand-file-name "../lib/plantuml.jar" (file-name-directory (file-truename plantuml-executable-path))))
  (setq org-plantuml-jar-path plantuml-jar-path)

  (setq plantuml-default-exec-mode 'executable)
  (setq plantuml-exec-mode 'executable)

  (setenv "PLANTUML_LIMIT_SIZE" "8192")

  (add-to-list 'org-src-lang-modes '("plantuml" . plantuml))
  (org-babel-do-load-languages 'org-babel-load-languages '((plantuml . t))))

Zig

(use-package zig-mode
  :mode "\\.zig$"
  :config

  (el-patch-defun zig--run-cmd (cmd &optional source &rest args)
    "Use compile command to execute a zig CMD with ARGS if given.
If given a SOURCE, execute the CMD on it."
    (let ((cmd-args
           (if source
               (mapconcat 'shell-quote-argument (cons source args) " ")
             args)))
      (compile (concat zig-zig-bin " " cmd (el-patch-add " -target native-macos.11") " " cmd-args)))))

Look and Feel

Remove the clutter

Hide menu, toolbar, scrollbar. (Goes to early-init to avoid flash of unstyled emacs.)

(tool-bar-mode -1)
(menu-bar-mode -1)
(scroll-bar-mode -1)

Do not show startup screen. (emacs-lisp)

(setq inhibit-startup-screen t)

Do not blink cursor. (emacs-lisp)

(blink-cursor-mode 0)

Draw block cursor as wide as the glyph under it. For example, if a block cursor is over a tab, it will be drawn as wide as that tab on the display.

(setq-default x-stretch-cursor t)

Hightlight parentheses, show current column.

(show-paren-mode 1)
(column-number-mode 1)

beacon-mode

Add a little bit of highlighting for the cursor when buffer scrolls or cursor jumps, so I don’t lose it.

(use-package beacon
  :diminish beacon-mode
  :config

  ;; do not blink where beacon-mode plays badly
  (defun rasen/dont-blink-predicate ()
    (member major-mode '(notmuch-search-mode)))
  (add-hook 'beacon-dont-blink-predicates #'rasen/dont-blink-predicate)

  (beacon-mode 1))

Fonts

I’m not a font guru, so I just stuffed a bunch of random fonts in here.

{
  fonts = {
    fontconfig.enable = true;
    fontDir.enable = true;
    enableGhostscriptFonts = false;

    fonts = with pkgs; [
      pkgs.inconsolata
      pkgs.dejavu_fonts
      pkgs.source-code-pro
      pkgs.ubuntu_font_family
      pkgs.unifont
      pkgs.powerline-fonts
      pkgs.terminus_font
    ];
  };
}

For home-manager.

{
  fonts.fontconfig.enable = true;
  home.packages = [
    pkgs.inconsolata
    pkgs.dejavu_fonts
    pkgs.source-code-pro
    pkgs.ubuntu_font_family
    pkgs.unifont
    pkgs.powerline-fonts
    pkgs.terminus_font
  ];
}

Configure fonts in Emacs. (.emacs.d/early-init.el)

(defun rasen/font-exists-p (font)
  "Check if the FONT exists."
  (and (display-graphic-p) (not (null (x-list-fonts font)))))

(defun rasen/set-my-fonts ()
  (cond
   ((and (eq system-type 'darwin)
         (rasen/font-exists-p "Input"))
    (set-face-attribute 'fixed-pitch nil :family "Input" :height 140)
    (set-face-attribute 'default nil :family "Input" :height 140))

   ((rasen/font-exists-p "Input") ; check for custom four-family font first
    (set-face-attribute 'fixed-pitch nil :family "Input" :height 70)
    (set-face-attribute 'default nil :family "Input" :height 70))
   ((rasen/font-exists-p "Input Mono")
    (set-face-attribute 'fixed-pitch nil :family "Input Mono" :height 65)
    (set-face-attribute 'default nil :family "Input Mono" :height 65))
   ((rasen/font-exists-p "Fira Code Retina")
    (set-face-attribute 'fixed-pitch nil :family "Fira Code Retina" :height 65)
    (set-face-attribute 'default nil :family "Fira Code Retina" :height 65))
   ((rasen/font-exists-p "Terminess Powerline")
    (set-face-attribute 'fixed-pitch nil :family "Terminess Powerline" :height 160)
    (set-face-attribute 'default nil :family "Terminess Powerline" :height 160))
   ((rasen/font-exists-p "Terminus")
    (set-face-attribute 'fixed-pitch nil :family "Terminus" :height 160)
    (set-face-attribute 'default nil :family "Terminus" :height 160)))

  (cond
   ((and (eq system-type 'darwin)
         (rasen/font-exists-p "Linux Libertine O"))
    (set-face-attribute 'variable-pitch nil :family "Linux Libertine O" :height 180))

   ((rasen/font-exists-p "Linux Libertine O")
    (set-face-attribute 'variable-pitch nil :family "Linux Libertine O" :height 90))
   ((rasen/font-exists-p "Vollkorn")
    (set-face-attribute 'variable-pitch nil :family "Vollkorn" :height 80))
   ((rasen/font-exists-p "DejaVu Sans")
    (set-face-attribute 'variable-pitch nil :family "DejaVu Sans"))))

(rasen/set-my-fonts)

Apply my font settings when new frame is created (useful when emacs is started in daemon mode). (.emacs.d/early-init.el)

(defun rasen/font-hook (frame)
  (select-frame frame)
  (rasen/set-my-fonts))

(add-hook 'after-make-frame-functions #'rasen/font-hook)

Custom Input font

I like the following settings more than defaults. I also need a custom four-style family because Emacs confuses regular/medium weights otherwise.

images/input-mono.png

(<<flake-packages>>)

{
  # note it's a new attribute and does not override old one
  input-mono = (pkgs.input-fonts.overrideAttrs (old: {
    pname = "input-mono";
    src = pkgs.fetchzip {
      name = "input-mono-${old.version}.zip";
      extension = ".zip";
      url = "https://input.djr.com/build/?fontSelection=fourStyleFamily&regular=InputMonoNarrow-Regular&italic=InputMonoNarrow-Italic&bold=InputMonoNarrow-Bold&boldItalic=InputMonoNarrow-BoldItalic&a=0&g=0&i=topserif&l=serifs_round&zero=0&asterisk=height&braces=straight&preset=default&line-height=1.2&accept=I+do&email=";
      sha256 = "sha256-vYwbel6yDrhKHv+rZGID5NPUhXw17mA18uUv8r+IhYM=";

      stripRoot = false;

      postFetch = ''
        # Reset the timestamp to release date for determinism.
        PATH=${pkgs.lib.makeBinPath [ pkgs.python3.pkgs.fonttools ]}:$PATH
        for ttf_file in $out/Input_Fonts/*/*/*.ttf; do
          ttx_file=$(dirname "$ttf_file")/$(basename "$ttf_file" .ttf).ttx
          ttx "$ttf_file"
          rm "$ttf_file"
          touch -m -t 201506240000 "$ttx_file"
          ttx --recalc-timestamp "$ttx_file"
          rm "$ttx_file"
        done
      '';
    };
  }));
}

Variable-pitch fonts in org-mode

Use variable-pitch fonts in org-mode. (<<org-config>>)

(push "~/.emacs.d/site-lisp" load-path)
(require 'org-variable-pitch)
(setq org-variable-pitch-fixed-font "Input")
(set-face-attribute 'org-variable-pitch-fixed-face nil
                    :height (if (eq system-type 'darwin) 140 65)
                    :weight 'regular)
(diminish 'org-variable-pitch-minor-mode)
(diminish 'buffer-face-mode)

;; Because asterisk in variable-pitch font is way too high, fontify
;; leading asterisks with fixed pitch face instead.
(setq org-variable-pitch-fontify-headline-prefix t)

;; remove org-table because valign-mode handles that
(delete 'org-table org-variable-pitch-fixed-faces)

(add-hook 'org-mode-hook #'org-variable-pitch-minor-mode)

Patch adaptive-wrap, so it work better with org-variable-pitch-mode (i.e., make adaptive prefix fixed-pitch).

(el-patch-defun adaptive-wrap-prefix-function (beg end)
  "Indent the region between BEG and END with adaptive filling."
  ;; Any change at the beginning of a line might change its wrap prefix, which
  ;; affects the whole line.  So we need to "round-up" `end' to the nearest end
  ;; of line.  We do the same with `beg' although it's probably not needed.
  (goto-char end)
  (unless (bolp) (forward-line 1))
  (setq end (point))
  (goto-char beg)
  (forward-line 0)
  (setq beg (point))
  (while (< (point) end)
    (let ((lbp (point)))
      (put-text-property
       (point) (progn (search-forward "\n" end 'move) (point))
       'wrap-prefix
       (let ((pfx (adaptive-wrap-fill-context-prefix
                   lbp (point))))
         ;; Remove any `wrap-prefix' property that
         ;; might have been added earlier.
         ;; Otherwise, we end up with a string
         ;; containing a `wrap-prefix' string
         ;; containing a `wrap-prefix' string ...
         (remove-text-properties
          0 (length pfx) '(wrap-prefix) pfx)
         (let ((dp (get-text-property 0 'display pfx)))
           (when (and dp (eq dp (get-text-property (1- lbp) 'display)))
             ;; There's a `display' property which covers not just the
             ;; prefix but also the previous newline.  So it's not just making
             ;; the prefix more pretty and could interfere or even defeat our
             ;; efforts (e.g. it comes from `visual-fill-mode').
             (remove-text-properties
              0 (length pfx) '(display) pfx)))

         (el-patch-add
           (add-face-text-property 0 (length pfx) 'org-variable-pitch-fixed-face nil pfx))

         pfx))))
  `(jit-lock-bounds ,beg . ,end))

Align org-mode tables with variable-pitch fonts

(use-package valign
  :hook
  (org-mode . valign-mode)
  :config
  (setq valign-fancy-bar t))

Hi-DPI

These are for omicron-only.

{
  xresources.properties = {
    "Xft.dpi" = 276;
    "Xcursor.size" = 64;
  };
}
{
  console.packages = [
    pkgs.terminus_font
  ];
  console.font = "ter-132n";
}

Load console configuration early at initrd. This ensures the proper font (sizes) are loaded when asking for disk encryption passphrase.

{
  console.earlySetup = true;
}
{
  services.xserver.dpi = 276;
}

Color theme

I use modus-operandi theme. (Goes to early-init to avoid flash of unstyled emacs.)

(require 'modus-themes)
(require 'modus-operandi-theme)

(setq modus-themes-italic-constructs t)
(setq modus-themes-bold-constructs t)

;; Use proportional fonts only when I explicitly configure them.
(setq modus-themes-no-mixed-fonts t)

(setq modus-themes-links '(faint background no-underline))
(setq modus-themes-org-agenda
      '((header-date . (workaholic bold-all))
        (scheduled . uniform)))

(load-theme 'modus-operandi t)

(modus-themes-with-colors
  (let ((custom--inhibit-theme-enable nil))
    (custom-theme-set-faces
     'modus-operandi

     `(rasen/org-project-face ((t :weight bold :background ,bg-dim)))

     `(rasen/agenda-date-header ((t :weight bold :foreground ,fg-dim)))

     `(fringe ((,c (:background ,bg-main :foreground ,fg-main))))

     ;; custom colors for vterm
     `(vterm-color-default ((,c (:background ,bg-main :foreground ,fg-main))))
     `(vterm-color-black ((,c (:background "#000" :foreground "#000"))))
     `(vterm-color-white ((,c (:background "#fff" :foreground "#fff"))))
     `(vterm-color-red ((,c (:background ,bg-red-intense :foreground ,red))))
     `(vterm-color-green ((,c (:background ,bg-green-intense :foreground ,green))))
     `(vterm-color-yellow ((,c (:background ,bg-yellow-intense :foreground ,yellow))))
     `(vterm-color-blue ((,c (:background ,bg-blue-intense :foreground ,blue))))
     `(vterm-color-magenta ((,c (:background ,bg-magenta-intense :foreground ,magenta))))
     `(vterm-color-cyan ((,c (:background ,bg-cyan-intense :foreground ,cyan))))

     ;; do not bold matches (this makes text jiggle with variable-pitch fonts)
     `(isearch ((,c (:inherit modus-themes-search-success))))
     `(query-replace ((,c :inherit modus-theme-intense-yellow)))

     ;; Make TODOs bold
     `(org-todo ((,c :foreground ,magenta-warmer
                     :weight bold)))
     ;; `(org-table ((,c (:foreground ,fg-special-cold :inherit nil))))

     ;; Make tags stand out
     `(org-tag ((,c :foreground ,fg-main :background ,bg-yellow-intense)))

     ;; dim done items
     `(org-done ((,c :foreground ,fg-alt)))
     `(org-headline-done ((,c :foreground ,fg-alt)))
     `(org-agenda-calendar-event ((,c))) ;; plain black is fine
     `(org-agenda-done ((,c :foreground ,fg-alt)))
     ;; remove special background from archived items
     `(org-archived ((,c (:foreground ,fg-alt))))

     `(org-link ((,c :foreground ,blue-warmer :background ,bg-dim :underline nil :inherit link)))
     `(rasen/org-id-link ((,c :foreground ,green-warmer :inherit org-link)))
     ))

  (setq org-todo-keyword-faces
        `(("TODO"     . (:foreground ,blue-intense :inherit fixed-pitch))
          ("NEXT"     . (:foreground ,red-intense  :inherit fixed-pitch))
          ("BUILD"    . (:foreground ,red-intense  :inherit fixed-pitch))
          ("WAIT"     . (:foreground ,magenta-warmer  :inherit fixed-pitch))
          ("DONE"     . (:foreground ,fg-dim       :inherit fixed-pitch))
          ("CANCELED" . (:foreground ,fg-dim       :inherit fixed-pitch)))))

Emacs modeline

(use-package doom-modeline
  :config
  (setq doom-modeline-icon nil)
  (doom-modeline-mode 1))

Display time and battery level in modeline.

(use-package time
  ;; builtin
  :config
  (setq display-time-default-load-average nil)
  (setq display-time-24hr-format t)
  (display-time-mode 1))

(use-package battery
  ;; builtin
  :config
  (display-battery-mode 1))

Misc

{
  home.file = {
    ".nethackrc".source = ./.nethackrc;
  };

  programs.fish.shellInit = ''
    set -x PATH ${./bin} $PATH
  '';
}

quick access

A key binding to quickly open my configuration file.

(defun rasen/find-config ()
  (interactive)
  (find-file (expand-file-name "README.org" rasen/dotfiles-directory)))

(general-def
  "<f12>" #'rasen/find-config)

(rasen/exwm-input-set-key (kbd "<s-f12>") #'rasen/find-config)

Same for my planning file.

(defun rasen/find-plan ()
  (interactive)
  (find-file (expand-file-name "plan.org" org-directory)))

(general-def
  "<f10>" #'rasen/find-plan)

(rasen/exwm-input-set-key (kbd "<s-f10>") #'rasen/find-plan)

And for agenda.

(defun rasen/find-agenda-overview ()
  (interactive)
  (org-agenda nil "o"))

(general-def
  "<f9>" #'rasen/find-agenda-overview)

(rasen/exwm-input-set-key (kbd "<s-f9>") #'rasen/find-agenda-overview)

Configure PATH

Set PATH env variable from exec-path. This is required for shell-command to find executables available in exec-path. (Most notably, org-mode latex preview fails without this.)

(use-package exec-path-from-shell
  :config
  (let ((old-path exec-path))
    (push "SSH_AUTH_SOCK" exec-path-from-shell-variables)
    (exec-path-from-shell-initialize)
    ;; append some old paths (required for )
    (setq exec-path (delete-dups (append exec-path old-path)))
    ;; set new value to PATH
    (setenv "PATH" (string-join exec-path ":"))))

break mode

(require 'break)
(general-def break-rest-mode-map
  "l" #'rasen/lock-screen)

(setq break-work-length 45
      break-rest-length 15)

;; (break-mode)

Watch IdleHint from logind to pause or resume break-mode.

(when (featurep 'dbusbind)
  (require 'dbus)

  (defvar rasen/idlehint-changed-hook nil)
  (defvar rasen/idlehint-object nil)

  (defun rasen/idlehint-changed-handler (interface changed invalidated)
    (if-let ((idlehintprop (alist-get "IdleHint" changed nil nil 'equal)))
        (run-hook-with-args 'rasen/idlehint-changed-hook (caar idlehintprop))))
  (defun rasen/idlehint-watch ()
    (interactive)
    (rasen/idlehint-unwatch)
    (dbus-register-signal
     :system
     nil
     "/org/freedesktop/login1/user/_1000"
     "org.freedesktop.DBus.Properties"
     "PropertiesChanged"
     #'rasen/idlehint-changed-handler))
  (defun rasen/idlehint-unwatch ()
    (interactive)
    (when rasen/idlehint-object
      (dbus-unregister-object rasen/idlehint-object)
      (setq rasen/idlehint-object nil)))
  (rasen/idlehint-watch)

  (defun rasen/idlehinit-to-break (idle)
    (message "(rasen/idlehint-to-break %s)" idle)
    (if idle
        (break-pause)
      (break-resume)))
  (add-hook 'rasen/idlehint-changed-hook #'rasen/idlehinit-to-break))