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
- Introduction
- Top-level
- NixOS
- Emacs
- Install Emacs
- Bootstrap Emacs config
- Patch ob-tangle
- GC hacks
- use-package
- package
- General (package)
- Don’t clutter system
- Helpers
- ivy
- smex
- counsel
- avy
- imenu / imenu-list
- wgrep
- whitespace
- whitespace-cleanup
- which-key
- Google translate
- tab-bar-mode
- Highlight current line
- Scrolling
- visual-fill-column
- Misc
- Environment
- Input
- Network
- Services
- Hardware
- Browsers
- Evil-mode
- Org-mode
- General
- Todo
- Highlight projects
- Clocking
- Capture
- Capturing images
- datetree
- cliplink
- Refile
- Archive
- Agenda
- Babel
- Latex preview
- Image preview
- Export
- Crypt
- org-list
- org-checklist
- Habits
- adaptive-wrap
- org-id
- org-roam
- org-roam-ui
- org-ref
- org-roam-bibtex
- toc-org
- org-fc
- org-fc review todos
- toggle markup/view
- Evilify org-mode
- Timestamps
- Fix focus steal when inserting “.” in date field
- Mail setup
- Applications
- Development
- Look and Feel
- Misc
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:
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 "-" "-"
nil ; replace all
nil ; not regex
nil ; replace on word boundaries
)
(goto-char (point-min))
(perform-replace "_" "_"
nil ; replace all
nil ; not regex
nil ; replace on word boundaries
)
(goto-char (point-min))
(perform-replace "{" "{"
nil ; replace all
nil ; not regex
nil ; replace on word boundaries
)
(goto-char (point-min))
(perform-replace "}" "}"
nil ; replace all
nil ; not regex
nil ; replace on word boundaries
)
(goto-char (point-min))
(perform-replace "[" "["
nil ; replace all
nil ; not regex
nil ; replace on word boundaries
)
(goto-char (point-min))
(perform-replace "]" "]"
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.
(<<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®ular=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))