#+PROPERTY: header-args :tangle yes :noweb yes :results silent
#+STARTUP: overview
* Introduction
Hi there! That's my dotfiles.
This repository contains configuration for several hosts:
- omicron — my main laptop which runs NixOS
- pie — my home server RPi running NixOS
- bayraktar — work MacBook Pro managed with nix-darwin
- oleksiishmalko — another work MacBook Pro managed with nix-darwin
Most of config files are generated by [[http://orgmode.org/worg/org-contrib/babel/][org-babel]] from org files in this repository (yes, including this very same ~README.org~). That's [[https://en.wikipedia.org/wiki/Literate_programming][literate programming]] applied to dotfiles.
This file contains Emacs configuration, NixOS and home-manager configuration for omicron, and NixOS configuration for pie.
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.
#+begin_src sh :tangle no
emacs -Q README.org --batch -f org-babel-tangle
#+end_src
#+RESULTS:
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/=
* Top-level
** README.org preamble
#+begin_src emacs-lisp
;;; -*- lexical-binding: t; -*-
#+end_src
** Flake
This repository is nix flakes–compatible.
The following goes to ~flake.nix~ file.
#+begin_src nix :tangle flake.nix :padline no
#
# 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 = "nixos-26.05";
};
<<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.attrsets.recursiveUpdate {};
in mergeSections [
<<flake-outputs-nixos>>
<<flake-outputs-home-manager>>
<<flake-outputs-packages>>
<<flake-outputs-overlays>>
<<flake-outputs-nix-darwin>>
];
nixConfig = {
extra-substituters = [
<<flake-extra-substituters>>
];
extra-trusted-public-keys = [
<<flake-extra-trusted-public-keys>>
];
};
}
#+end_src
Nix flakes are still an experimental feature, so you need the following in NixOS configuration to enable it.
#+begin_src nix :noweb-ref nixos-section
{
nix = {
extraOptions = ''
experimental-features = nix-command flakes
'';
};
}
#+end_src
For nix-darwin systems:
#+begin_src nix :noweb-ref darwin-section
{
nix = {
extraOptions = ''
experimental-features = nix-command flakes
'';
};
}
#+end_src
For non-NixOS system, put the following into =~/.config/nix/nix.conf=.
#+begin_src conf :tangle no
experimental-features = nix-command flakes
#+end_src
** Unstable packages
For packages that are not available in latest stable, expose the unstable channel as ~pkgs.unstable~.
Add input:
#+begin_src nix :noweb-ref flake-inputs
nixpkgs-unstable = {
type = "github";
owner = "NixOS";
repo = "nixpkgs";
ref = "nixpkgs-unstable";
};
#+end_src
Add overlay:
#+begin_src nix :noweb-ref flake-overlays
(final: prev: {
unstable = import inputs.nixpkgs-unstable {
inherit system;
overlays = self.overlays.${system};
config = { allowUnfree = true; };
};
})
#+end_src
** NixOS
Expose NixOS configurations.
#+begin_src nix :noweb-ref flake-outputs-nixos
(let
nixosHosts = {
omicron = { system = "x86_64-linux"; config = ./nixos-config.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;
})
#+end_src
** Home manager
Add home-manager to flake inputs.
#+begin_src nix :noweb-ref flake-inputs
home-manager = {
type = "github";
owner = "rycee";
repo = "home-manager";
ref = "release-26.05";
inputs.nixpkgs.follows = "nixpkgs";
};
#+end_src
Expose home-manager configurations.
#+begin_src nix :noweb-ref flake-outputs-home-manager
(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 = { name, lib, pkgs, config, ... }: {
imports = [
<<home-manager-section>>
];
home.stateVersion = "21.05";
};
homeManagerConfigurations = genHosts homeManagerHosts mkHomeManagerConfiguration;
})
#+end_src
#+begin_src nix :noweb-ref home-manager-section
{
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.";
};
}
#+end_src
Integrate home-manager module into NixOS.
#+begin_src nix :noweb-ref nixos-section
{
imports = [inputs.home-manager.nixosModules.home-manager];
home-manager = {
useUserPackages = true;
useGlobalPkgs = true;
users.rasen = inputs.self.lib.home-manager-common;
};
}
#+end_src
Integrate home-manager into nix-darwin.
#+begin_src nix :noweb-ref darwin-section
{
imports = [inputs.home-manager.darwinModules.home-manager];
home-manager = {
useUserPackages = false;
useGlobalPkgs = true;
users."${config.system.primaryUser}" = { ... }: {
imports = [
inputs.self.lib.home-manager-common
{ hostname = config.networking.hostName; }
];
};
# users.rasen = inputs.self.lib.home-manager-common;
backupFileExtension = "bak";
};
}
#+end_src
** nix-darwin
Add nix-darwin to flake inputs.
#+begin_src nix :noweb-ref flake-inputs
darwin = {
url = "github:lnl7/nix-darwin/nix-darwin-26.05";
inputs.nixpkgs.follows = "nixpkgs";
};
#+end_src
Add helper function to differentiate linux-only config:
#+begin_src nix :noweb-ref flake-overlays
(final: prev: {
lib = prev.lib // {
linux-only = prev.lib.mkIf final.stdenv.isLinux;
};
})
#+end_src
#+begin_src nix :noweb-ref flake-outputs-nix-darwin
(let
darwinHosts = {
<<darwin-host>>
};
mkDarwinConfiguration = { name, system, systemStateVersion, homeManagerStateVersion, primaryUser ? "rasen", modules ? [] }:
inputs.darwin.lib.darwinSystem {
inherit system;
modules = modules ++ [
{
networking.hostName = name;
system.primaryUser = primaryUser;
system.stateVersion = systemStateVersion;
home-manager.sharedModules = [
({lib, ...}: {
home.stateVersion = lib.mkForce homeManagerStateVersion;
})
];
}
self.darwin-common
];
};
in {
darwin-common = { lib, pkgs, config, ... }: {
imports = [
<<darwin-section>>
];
};
darwinConfigurations = genHosts darwinHosts mkDarwinConfiguration;
})
#+end_src
#+begin_src nix :noweb-ref darwin-section
{
nixpkgs.config = {
allowUnfree = true;
};
nixpkgs.overlays = self.overlays.aarch64-darwin;
}
#+end_src
Link installed apps to ~~/Applications/Home Manager Apps~:
#+begin_src nix :noweb-ref home-manager-section
{
targets.darwin.linkApps.enable = pkgs.stdenv.hostPlatform.isDarwin;
targets.darwin.copyApps.enable = false;
}
#+end_src
** Packages
Generate packages set for each supported system.
#+begin_src nix :noweb-ref flake-outputs-packages
(let
mkPackages = system:
let
pkgs = pkgsBySystem.${system};
in
mergeSections [
<<flake-packages>>
];
in {
packages = genAttrs systems mkPackages;
})
#+end_src
** Overlays
Generate overlays for all supported systems.
#+begin_src nix :noweb-ref flake-outputs-overlays
(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;
})
#+end_src
~<<flake-overlays>>~ are defined elsewhere.
* NixOS
** General
I'm a [[http://nixos.org/][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:
#+begin_src nix :tangle nixos-config.nix :noweb no-export
#
# This file is auto-generated from "README.org"
#
{ name, config, pkgs, lib, inputs, ... }:
let
machine-config = lib.getAttr name {
omicron = [
<<machine-omicron>>
];
};
in
{
imports = [
{
# The NixOS release to be compatible with for stateful data such as databases.
# TODO: make it per-machine
system.stateVersion = "21.05";
}
<<nixos-section>>
] ++ machine-config;
}
#+end_src
This =<<nixos-section>>= is replaced by other parts of this doc.
** Re-expose nixpkgs
For NixOS:
#+begin_src nix :noweb-ref nixos-section
{
# 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;
nixpkgs-unstable.flake = inputs.nixpkgs-unstable;
};
}
#+end_src
Same configuration for nix-darwin systems.
#+begin_src nix :noweb-ref darwin-section
{
# 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;
nixpkgs-unstable.flake = inputs.nixpkgs-unstable;
};
}
#+end_src
Same but for Home Manager–managed host.
#+begin_src nix :noweb-ref home-manager-section
{
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);
}
{
from = { id = "nixpkgs-unstable"; type = "indirect"; };
to = ({
type = "path";
path = inputs.nixpkgs-unstable.outPath;
} // lib.filterAttrs
(n: v: n == "lastModified" || n == "rev" || n == "revCount" || n == "narHash")
inputs.nixpkgs);
}
];
};
}
#+end_src
** Sandbox
Build all Nix packages in sandbox:
#+begin_src nix :noweb-ref nixos-section
{
nix.settings.sandbox = true;
}
#+end_src
** Users
I'm the only user of the system (nixos-section):
#+begin_src nix :noweb-ref nixos-section
{
users.extraUsers.rasen = {
isNormalUser = true;
uid = 1000;
extraGroups = [ "users" "wheel" ];
initialPassword = "HelloWorld";
};
nix.settings.trusted-users = ["rasen"];
}
#+end_src
=initialPassword= is used only first time when user is created. It must be changed as soon as possible with =passwd=.
* macOS
** Users
#+begin_src nix :noweb-ref darwin-section
{
users.users.${config.system.primaryUser} = {
description = "Oleksii Shmalko";
home = "/Users/${config.system.primaryUser}";
};
}
#+end_src
** Some defaults
#+begin_src nix :noweb-ref darwin-section
{
system.defaults = {
NSGlobalDomain = {
# auto switch light/dark mode
AppleInterfaceStyleSwitchesAutomatically = true;
# disable automatic capitalization
NSAutomaticCapitalizationEnabled = false;
# Use F1, F2, etc keys by default (without needing to press fn)
"com.apple.keyboard.fnState" = true;
# disable beeps
"com.apple.sound.beep.volume" = 0.0;
# Disable natural scrolling
"com.apple.swipescrolldirection" = false;
};
dock = {
# Auto-hide dock
autohide = true;
};
finder = {
# Show extensions in finder
AppleShowAllExtensions = true;
# Prefer list view
FXPreferredViewStyle = "Nlsv";
# Open home directory by default
NewWindowTarget = "Home";
# Show pathbar
ShowPathbar = true;
};
# Disable default fn keyboard layout switch. I have my own with Karabiner
hitoolbox.AppleFnUsageType = "Do Nothing";
# Show seconds in clock
menuExtraClock.ShowSeconds = true;
};
system.keyboard = {
enableKeyMapping = true;
# Move tilde back on non-US keyboards
nonUS.remapTilde = true;
# Remap caps lock to control
remapCapsLockToControl = true;
};
}
#+end_src
** xbar integration function
#+begin_src emacs-lisp
(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))))))
#+end_src
org-clock.1s.sh
#+begin_src sh
#!/usr/bin/env bash
~/.nix-profile/bin/emacs --batch --eval "(progn (require 'server) (princ (server-eval-at \"server\" '(rasen/xbar \"$0\"${1:+" '$1"}))))"
#+end_src
** Yabai integration
#+begin_src nix :noweb-ref darwin-section
{
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
'';
};
}
#+end_src
Skhd integration to move between windows.
https://gist.github.com/ethan-leba/760054f36a2f7c144c6b06ab6458fae6
#+begin_src nix :noweb-ref darwin-section
{
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 - e ~
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 - n : yabai -m window --ratio rel:-0.05
# default < lcmd + ctrl - o : yabai -m window --ratio rel:+0.05
default < lcmd + shift - n : yabai -m window --swap west
default < lcmd + shift - u : yabai -m window --swap north
default < lcmd + shift - e : yabai -m window --swap south
default < lcmd + shift - o : yabai -m window --swap east
default < lcmd - n [
,* : yabai -m window --focus west
"Emacs" ~
]
default < lcmd - u [
,* : yabai -m window --focus north
"Emacs" ~
]
default < lcmd - e [
,* : yabai -m window --focus south
"Emacs" ~
]
default < lcmd - o [
,* : yabai -m window --focus east
"Emacs" ~
]
'';
};
}
#+end_src
Emacs integration for yabai:
#+begin_src emacs-lisp
(defun yabai-move-on-error (direction move-fn)
"Execute `move-fn'. If that function errors, move yabai focus in the specified direction."
(interactive)
(condition-case nil
(funcall move-fn)
(user-error (rasen/yabai-move direction))))
(defun rasen/yabai-move (direction)
"Move yabai focus in the specified `direction'."
(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))
#+end_src
** Karabiner configuration
Configure Karabiner-Elements for advanced keyboard customization.
#+begin_src nix :noweb-ref home-manager-section
{
xdg.configFile."karabiner/karabiner.json".text = builtins.toJSON {
global.show_in_menu_bar = false;
profiles = [
{
name = "Default profile";
selected = true;
virtual_hid_keyboard = {
country_code = 0;
keyboard_type_v2 = "ansi";
};
complex_modifications.rules = [
{
description = "S-<return> to Terminal";
manipulators = [
{
type = "basic";
conditions = [
{
type = "frontmost_application_unless";
bundle_identifiers = ["org\\.gnu\\.Emacs"];
file_paths = ["/emacs$"];
}
];
from = {
key_code = "return_or_enter";
modifiers.mandatory = ["left_gui"];
};
to = [{ shell_command = "cd ~; /Users/rasen/.nix-profile/bin/emacsclient -c -e '(progn (vterm) (set-window-dedicated-p nil t))'"; }];
}
];
}
{
description = "remap s-C-j/l to S-C-left/right for space switching";
manipulators = [
{
type = "basic";
from = {
key_code = "j";
modifiers.mandatory = ["left_gui" "left_control"];
};
to = {
key_code = "left_arrow";
modifiers = ["left_gui" "left_control"];
};
}
{
type = "basic";
from = {
key_code = "l";
modifiers.mandatory = ["left_gui" "left_control"];
};
to = {
key_code = "right_arrow";
modifiers = ["left_gui" "left_control"];
};
}
];
}
{
description = "ctrl to switch keyboard layout";
manipulators = [
{
type = "basic";
conditions = [{ type = "input_source_unless"; input_sources = [{ language = "en"; }]; }];
from.key_code = "left_control";
to = [{ select_input_source.input_source_id = "org.sil.ukelele.keyboardlayout.workman.workman"; }];
}
{
type = "basic";
conditions = [{ type = "input_source_if"; input_sources = [{ language = "en"; }]; }];
from.key_code = "left_control";
to = [{ select_input_source.language = "^uk$"; }];
}
];
}
{
description = "fn key to switch keyboard layout";
manipulators = [
{
type = "basic";
conditions = [{ type = "input_source_unless"; input_sources = [{ language = "en"; }]; }];
from.apple_vendor_top_case_key_code = "keyboard_fn";
to = [{ apple_vendor_keyboard_key_code = "function"; }];
to_if_alone = [{ select_input_source.input_source_id = "org.sil.ukelele.keyboardlayout.workman.workman"; }];
parameters.basic.to_if_alone_timeout_milliseconds = 200;
}
{
type = "basic";
conditions = [{ type = "input_source_if"; input_sources = [{ language = "en"; }]; }];
from.apple_vendor_top_case_key_code = "keyboard_fn";
to = [{ apple_vendor_keyboard_key_code = "function"; }];
to_if_alone = [{ select_input_source.language = "^uk$"; }];
parameters.basic.to_if_alone_timeout_milliseconds = 200;
}
];
}
{
description = "Post escape if caps_lock is tapped";
manipulators = [
{
type = "basic";
from = {
key_code = "caps_lock";
modifiers.optional = ["any"];
};
parameters = {
basic = {
to_if_alone_timeout_milliseconds = 200;
to_if_held_down_threshold_milliseconds = 200;
};
};
to = [{ key_code = "left_control"; }];
to_if_alone = [{ key_code = "escape"; }];
to_if_held_down = [{ key_code = "left_control"; }];
}
];
}
];
}
];
};
}
#+end_src
** defaults bindings
Override keybindings in settings:
#+begin_src nix :noweb-ref darwin-section
{
system.defaults.CustomUserPreferences = {
"com.apple.symbolichotkeys" = {
AppleSymbolicHotKeys = let
disabled = { enabled = false; };
modifier = enabled: code: if enabled then code else 0;
enabled =
{ shift ? false
, control ? false
, option ? false
, command ? false
, function ? false
# Keycode from
# /System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h
, key
# ASCII code of the key
, ascii ? 65535
}:
let
modifiers = lib.foldl' builtins.bitOr 0 [
(modifier shift (lib.fromHexString "0x020000"))
(modifier control (lib.fromHexString "0x040000"))
(modifier option (lib.fromHexString "0x080000"))
(modifier command (lib.fromHexString "0x100000"))
(modifier function (lib.fromHexString "0x800000"))
];
in {
enabled = true;
value = {
parameters = [ascii key modifiers];
type = "standard";
};
};
in {
# Move left a space
"79" = enabled {
command = true;
control = true;
key = 123; # left arrow
};
# Move right a space
"81" = enabled {
command = true;
control = true;
key = 124; # right arrow
};
# quick note
"190" = disabled;
# Cmd-P -> Show Launchpad
"160" = enabled {
command = true;
key = 31; # o
ascii = 112; # p
};
# Show spotlight search
"64" = disabled;
};
};
};
system.activationScripts.activateHotkeys = {
enable = true;
text = ''
/System/Library/PrivateFrameworks/SystemAdministration.framework/Resources/activateSettings -u
'';
};
}
#+end_src
** Linux builder
#+begin_src nix :noweb-ref darwin-section
{
nix.settings.trusted-users = [ "@admin" ];
nix.linux-builder = {
enable = true;
config.virtualisation.cores = 8;
config.virtualisation.darwin-builder = {
diskSize = 40 * 1024; # bump from 20Gb
memorySize = 8 * 1024;
};
};
launchd.daemons.linux-builder = {
serviceConfig = {
StandardOutPath = "/var/log/linux-builder.log";
StandardErrorPath = "/var/log/linux-builder.log";
};
};
}
#+end_src
* Machines
** omicron
This is my small Dell XPS 13 running NixOS.
#+begin_src nix :noweb-ref machine-omicron
{
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.graphics = {
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;
}
#+end_src
~inputs.nixos-hardware~ comes from the following flake input.
#+begin_src nix :noweb-ref flake-inputs
nixos-hardware = {
type = "github";
owner = "NixOS";
repo = "nixos-hardware";
};
#+end_src
LVM on LUKS setup for disk encryption.
#+begin_src nix :noweb-ref machine-omicron
{
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"; }
];
}
#+end_src
Clickpad:
#+begin_src nix :noweb-ref machine-omicron
{
services.libinput = {
enable = true;
touchpad.accelSpeed = "0.7";
};
}
#+end_src
Fix screen tearing ([[https://wiki.archlinux.org/index.php/intel_graphics#Tearing][ArchWiki]]):
#+begin_src nix :noweb-ref machine-omicron
{
services.xserver.config = ''
Section "Device"
Identifier "Intel Graphics"
Driver "intel"
Option "TearFree" "true"
Option "TripleBuffer" "true"
EndSection
'';
}
#+end_src
** bayraktar
bayraktar is macbook pro managed with nix-darwin.
#+begin_src nix :noweb-ref darwin-host
bayraktar = {
system = "aarch64-darwin";
systemStateVersion = 4;
homeManagerStateVersion = "21.05";
modules = [
({ lib, ... }: {
home-manager.users."rasen" = {
programs.git.settings.user.email = lib.mkForce "oleksii@fluxon.com";
};
})
];
};
#+end_src
*** Work email
#+begin_src nix :noweb-ref home-manager-section
{
accounts.email.accounts = lib.mkIf (config.hostname == "bayraktar") (lib.mkForce {
fluxon = {
realName = "Oleksii Shmalko";
address = "oleksii@fluxon.com";
flavor = "gmail.com";
primary = true;
passwordCommand = "pass fluxon/google.com/as@fluxon.com/email";
maildir.path = "fluxon";
msmtp.enable = true;
notmuch.enable = true;
mbsync.enable = true;
mbsync.create = "maildir";
};
});
programs.mbsync.extraConfig = lib.mkForce "";
programs.notmuch.extraConfig = {
index."header.Sender" = "Sender";
};
}
#+end_src
** oleksiishmalko
Another work macbook pro.
#+begin_src nix :noweb-ref darwin-host
oleksiishmalko = {
system = "aarch64-darwin";
primaryUser = "oleksii.shmalko";
systemStateVersion = 6;
homeManagerStateVersion = "25.05";
modules = [
({ lib, ... }: {
home-manager.users."oleksii.shmalko" = {
programs.git.settings.user.email = lib.mkForce "oleksii.shmalko@datadoghq.com";
programs.git.settings.github.user = lib.mkForce "dd-oleksii";
};
})
];
};
#+end_src
** pie
Pie is a Raspberry Pi machine.
Build:
#+begin_src sh
nix run nixpkgs#nixos-rebuild -- build --flake path:.#pie
#+end_src
Deploy:
#+begin_src sh
nix run nixpkgs#nixos-rebuild -- switch --flake path:.#pie --target-host pie.local --use-remote-sudo --fast
#+end_src
Add nixos-raspberrypi input:
#+begin_src nix :noweb-ref flake-inputs
nixos-raspberrypi.url = "github:nvmd/nixos-raspberrypi/main";
#+end_src
Add configuration:
#+begin_src nix :noweb-ref flake-outputs-nixos
{
nixosConfigurations = {
pie = inputs.nixos-raspberrypi.lib.nixosSystem {
specialArgs = {
inherit inputs;
inherit (inputs) nixos-raspberrypi;
# Semi-secret: don't want to publish these values but it'll
# end up in nix store anyway.
sensitive = import ./secrets/sensitive.nix;
};
system = "aarch64-linux";
modules = [
inputs.agenix.nixosModules.default
inputs.disko.nixosModules.disko
inputs.nixos-raspberrypi.nixosModules.raspberry-pi-3.base
({ pkgs, lib, config, inputs, sensitive, ... }: {
imports = [
<<pie>>
];
})
];
};
};
}
#+end_src
*** General
General stuff first.
#+begin_src nix :noweb-ref pie
{
networking.hostName = "pie";
system.stateVersion = "25.11";
environment.systemPackages = [pkgs.libraspberrypi];
}
#+end_src
*** Flake support
#+begin_src nix :noweb-ref pie
{
nix = {
settings.trusted-users = ["@wheel"];
extraOptions = ''
experimental-features = nix-command flakes
'';
};
}
#+end_src
*** Bootloader
Configure bootloader via nixos-raspberrypi.
#+begin_src nix :noweb-ref pie
{
boot.loader.raspberry-pi.bootloader = "kernel";
}
#+end_src
*** Remote access
#+begin_src nix :noweb-ref pie
{
services.openssh = {
enable = true;
settings = {
PasswordAuthentication = false;
PermitRootLogin = "no";
};
};
users.mutableUsers = false;
users.users.rasen = {
isNormalUser = true;
home = "/home/rasen";
createHome = true;
openssh.authorizedKeys.keys = [
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDHH15uiQw3jBbrdlcRb8wOr8KVltuwbHP/JOFAzXFO1l/4QxnKs6Nno939ugULM7Lu0Vx5g6FreuCOa2NMWk5rcjIwOzjrZnHZ7aoAVnE7H9scuz8NGnrWdc1Oq0hmcDxdZrdKdB6CPG/diGWNZy77nLvz5JcX1kPLZENPeApCERwR5SvLecA4Es5JORHz9ssEcf8I7VFpAebfQYDu+VZZvEu03P2+5SXv8+5zjiuxM7qxzqRmv0U8eftii9xgVNC7FaoRBhhM7yKkpbnqX7IeSU3WeVcw4+d1d8b9wD/sFOyGc1xAcvafLaGdgeCQGU729DupRRJokpw6bBRQGH29 rasen@omicron"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIulco7Hi0uJ90GLwuDvgG/Xlv/i2R4ySa5+/dOYygsr rasen@bayraktar"
];
extraGroups = [ "wheel" ];
};
security.sudo.wheelNeedsPassword = false;
services.avahi = {
enable = true;
nssmdns4 = true;
publish = {
enable = true;
addresses = true;
userServices = true;
};
};
}
#+end_src
*** Netdata
#+begin_src nix :noweb-ref pie
{
nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [
"netdata"
];
services.netdata = {
enable = true;
package = pkgs.netdata.override {
withCloudUi = true;
};
};
networking.firewall.allowedTCPPorts = [ 19999 ];
}
#+end_src
*** Gitolite
#+begin_src nix :noweb-ref pie
{
services.gitolite = {
enable = true;
enableGitAnnex = true;
user = "git";
adminPubkey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIulco7Hi0uJ90GLwuDvgG/Xlv/i2R4ySa5+/dOYygsr rasen@bayraktar";
dataDir = "/media/data/gitolite";
};
}
#+end_src
*** Syncthing
#+begin_src nix :noweb-ref pie
{
services.syncthing = {
enable = true;
dataDir = "/media/data/syncthing";
openDefaultPorts = true;
settings = {
devices = {
bayraktar = {
id = sensitive.syncthing.bayraktar;
autoAcceptFolders = true;
};
bugdroid = {
id = sensitive.syncthing.bugdroid;
autoAcceptFolders = true;
};
lapdog = {
id = sensitive.syncthing.lapdog;
autoAcceptFolders = true;
};
omicron = {
id = sensitive.syncthing.omicron;
autoAcceptFolders = true;
};
};
};
};
}
#+end_src
*** Tools
#+begin_src nix :noweb-ref pie
{
programs.mosh.enable = true;
environment.systemPackages = [
pkgs.vim
pkgs.psmisc
];
}
#+end_src
*** Disk configuration
#+begin_src nix :noweb-ref pie
{
# This was manually transformed from the sd installer image.
disko.devices.disk.sd = {
type = "disk";
device = "/dev/mmcblk0";
content = {
type = "gpt";
partitions = {
firmware = {
name = "FIRMWARE";
size = "1024M";
type = "EF02";
content = {
type = "filesystem";
device = "/dev/mmcblk0p1";
format = "vfat";
mountpoint = "/boot/firmware";
mountOptions = [
"noatime"
"noauto"
"x-systemd.automount"
"x-systemd.idle-timeout=1min"
];
};
};
luks = {
size = "100%";
content = {
type = "luks";
device = "/dev/mmcblk0p3";
name = "root";
extraFormatArgs = [
# RPi does not have hardware-accelerated AES, so chacha
# + adiantum provide better performance
"--cipher=xchacha12,aes-adiantum-plain64"
# adiantum needs larger sector size to be efficient
"--sector-size=4096"
];
askPassword = true;
content = {
type = "btrfs";
extraArgs = ["-f"]; # override existing partition
postCreateHook = let
thisBtrfs = config.disko.devices.disk.sd.content.partitions.root.content.content;
device = thisBtrfs.device;
subvolumes = thisBtrfs.subvolumes;
makeBlankSnapshot = btrfsMntPoint: subvol: let
subvolAbsPath = lib.strings.normalizePath "${btrfsMntPoint}/${subvol.name}";
dst = "${subvolAbsPath}-blank";
# NOTE: this one-liner has the same functionality (inspired by zfs hook)
# btrfs subvolume list -s mnt/rootfs | grep -E ' rootfs-blank$' || btrfs subvolume snapshot -r mnt/rootfs mnt/rootfs-blank
in ''
if ! btrfs subvolume show "${dst}" > /dev/null 2>&1; then
btrfs subvolume snapshot -r "${subvolAbsPath}" "${dst}"
fi
'';
# Mount top-level subvolume (/) with "subvol=/", without it
# the default subvolume will be mounted. They're the same in
# this case, though. So "subvol=/" isn't really necessary
in ''
MNTPOINT=$(mktemp -d)
mount ${device} "$MNTPOINT" -o subvol=/
trap 'umount $MNTPOINT; rm -rf $MNTPOINT' EXIT
${makeBlankSnapshot "$MNTPOINT" subvolumes."@root"}
'';
subvolumes = {
"@root" = {
mountpoint = "/";
mountOptions = ["compress=zstd" "noatime"];
};
"@nix" = {
mountpoint = "/nix";
mountOptions = ["compress=zstd" "noatime"];
};
"@persist" = {
mountpoint = "/persist";
mountOptions = ["compress=zstd" "noatime"];
};
"@log" = {
mountpoint = "/var/log";
mountOptions = ["compress=zstd" "noatime"];
};
};
};
};
};
};
};
};
disko.devices.disk.main = {
type = "disk";
device = "/dev/sda";
content = {
type = "gpt";
partitions = {
luks = {
size = "100%";
content = {
type = "luks";
name = "main";
initrdUnlock = false;
extraFormatArgs = [
# RPi does not have hardware-accelerated AES, so chacha
# + adiantum provide better performance
"--cipher=xchacha12,aes-adiantum-plain64"
# adiantum needs larger sector size to be efficient
"--sector-size=4096"
];
askPassword = true;
settings.allowDiscards = true;
content = {
type = "btrfs";
extraArgs = ["-f"]; # override existing partition
subvolumes = {
"@data" = {
mountpoint = "/media/data";
mountOptions = [
"ssd"
"compress=zstd"
"noatime"
"noauto" # do not mount on boot
"nofail"
"x-systemd.automount" # mount when needed
"x-systemd.device-timeout=1ms" # don’t wait for device to appear if it’s not there
"x-systemd.idle-timeout=5m" # unmount after 5 mins of inactivity
];
};
"@tmp" = {
mountpoint = "/media/tmp";
mountOptions = [
"ssd"
"compress=zstd"
"noatime"
"noauto" # do not mount on boot
"nofail"
"x-systemd.automount" # mount when needed
"x-systemd.device-timeout=1ms" # don’t wait for device to appear if it’s not there
"x-systemd.idle-timeout=5m" # unmount after 5 mins of inactivity
];
};
};
};
};
};
};
};
};
environment.etc.crypttab = {
mode = "0600";
text = ''
main /dev/disk/by-partlabel/disk-main-luks none luks,discard,noauto,nofail,x-systemd.device-timeout=1ms
'';
};
fileSystems."/".neededForBoot = true;
fileSystems."/nix".neededForBoot = true;
fileSystems."/persist".neededForBoot = true;
fileSystems."/var/log".neededForBoot = true;
swapDevices = [{
device = "/media/tmp/swapfile";
size = 16*1024;
}];
boot.kernel.sysctl = {
"vm.swappiness" = 160;
};
# TODO: dedup https://wiki.nixos.org/wiki/Btrfs#Deduplication
services.btrfs.autoScrub.enable = true;
}
#+end_src
*** Backups
#+begin_src nix :noweb-ref pie
{
services.snapper.configs = {
# TODO: make root ephemeral and snapshot /persist instead
root = {
SUBVOLUME = "/";
TIMELINE_CREATE = true;
TIMELINE_CLEANUP = true;
};
data = {
SUBVOLUME = "/media/data/";
NUMBER_CLEANUP = true;
NUMBER_LIMIT = 10;
NUMBER_LIMIT_IMPORTANT = 10;
TIMELINE_CREATE = true;
TIMELINE_CLEANUP = true;
TIMELINE_LIMIT_MONTHLY = 6;
TIMELINE_LIMIT_YEARLY = 2;
};
};
age.secrets.restic-password.file = ./secrets/pie-restic-password.age;
age.secrets.restic-sa-key.file = ./secrets/pie-restic-sa-key.json.age;
services.restic.backups.data = {
paths = ["/media/data/"];
exclude = ["/media/data/.snapshots/"];
repository = sensitive.backups.repository;
passwordFile = config.age.secrets.restic-password.path;
environmentFile = toString (pkgs.writeText "restic-env" ''
GOOGLE_PROJECT_ID=${sensitive.backups.googleProjectId}
GOOGLE_APPLICATION_CREDENTIALS=${config.age.secrets.restic-sa-key.path}
RESTIC_KEY_HINT=${sensitive.backups.resticKeyHint}
RESTIC_READ_CONCURRENCY=4
RESTIC_CACHE_DIR=/media/tmp/restic/cache
TMPDIR=/media/tmp/restic/tmp
'');
progressFps = 0.1; # every 10 seconds
extraBackupArgs = [
"--skip-if-unchanged"
"--one-file-system"
];
timerConfig.OnCalendar = "hourly";
pruneOpts = [
"--keep-hourly 24"
"--keep-daily 7"
"--keep-weekly 4"
"--keep-monthly 12"
"--keep-yearly 80"
];
runCheck = true;
checkOpts = [
"--read-data-subset=50M"
];
# Remount snapshot in place of /media/data. We’re doing this with
# PrivateMounts, so it does not affect the main system.
backupPrepareCommand = ''
set -euxo pipefail
umount /media/data
SNAPSHOT_NUMBER=$(snapper -c data create --description 'restic backup' --read-only --cleanup-algorithm number --print-number)
mount /dev/mapper/main /media/data -o ro,subvol=@data/.snapshots/$SNAPSHOT_NUMBER/snapshot
'';
backupCleanupCommand = ''
umount /media/data
snapper -c data cleanup number
'';
};
systemd.services.restic-backups-data.path = [pkgs.util-linux pkgs.snapper];
systemd.services.restic-backups-data.serviceConfig = {
PrivateMounts = true;
Nice = 19;
IOSchedulingPriority = 7; # lowest
};
}
#+end_src
*** Binary cache
Substituters:
#+begin_src nix :noweb-ref flake-extra-substituters
"https://nixos-raspberrypi.cachix.org"
#+end_src
Trusted public keys:
#+begin_src nix :noweb-ref flake-extra-trusted-public-keys
"nixos-raspberrypi.cachix.org-1:4iMO9LXa8BqhU+Rpg6LQKiGa2lsNh/j2oiYLNOQ5sPI="
#+end_src
*** agenix
Flake inputs:
#+begin_src nix :noweb-ref flake-inputs
agenix = {
url = "github:ryantm/agenix";
inputs.nixpkgs.follows = "nixpkgs";
inputs.home-manager.follows = "home-manager";
inputs.darwin.follows = "darwin";
};
#+end_src
Secrets (tangled to secrets.nix):
#+begin_src nix :tangle ./secrets.nix :noweb no-export
# Auto-generated from README.org
let
systems = {
pie = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII6VmzumaVf0ELAIzh2Zmq2sjEv2h99Co6lXB5BaN8/y";
};
users = {
yubikey1 = "age1yubikey1qvqpv4f2kvwxzdf4rm349za00jayxhk23ads42l92sjywlcwce20yhm6pm2";
bayraktar = "age1kmlska3klt5ca98chxm0xu2q2rc5x2c3agkut6303ajg35rreqes7qg3yd";
};
in {
"secrets/pie-restic-sa-key.json.age".publicKeys = [
systems.pie
];
"secrets/pie-restic--password.age".publicKeys = [
systems.pie
];
}
#+end_src
*** disko
Flake inputs:
#+begin_src nix :noweb-ref flake-inputs
disko = {
url = "github:nix-community/disko/latest";
inputs.nixpkgs.follows = "nixpkgs";
};
#+end_src
*** impermanence
Flake inputs:
#+begin_src nix :noweb-ref flake-inputs
impermanence = {
url = "github:nix-community/impermanence";
inputs.nixpkgs.follows = "";
inputs.home-manager.follows = "";
};
#+end_src
*** Remote unlock
#+begin_src nix :noweb-ref pie
{
boot.initrd = {
enable = true;
availableKernelModules = [ "r8169" ];
luks.forceLuksSupportInInitrd = true;
luks.cryptoModules = lib.mkDefault (lib.mkAfter [
"aes"
"aes_generic"
"blowfish"
"twofish"
"serpent"
"cbc"
"xts"
"lrw"
"sha1"
"sha256"
"sha512"
"af_alg"
"algif_skcipher"
"cryptd"
"input_leds"
# extra:
"xchacha20"
"xchacha12" "adiantum" "nhpoly1305"
]);
network = {
enable = true;
udhcpc.enable = true;
flushBeforeStage2 = true;
ssh = {
enable = true;
port = 2222;
hostKeys = [./secrets/ssh_host_pie_ed25519_key];
# hostKeys = ["/boot/initrd/sshhost_ed25519_key"];
# ignoreEmptyHostKeys = true; # we’re going to deliver it out of bound
# extraConfig = ''
# HostKey /boot/ssh_host_ed25519_key
# '';
authorizedKeys = [
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDHH15uiQw3jBbrdlcRb8wOr8KVltuwbHP/JOFAzXFO1l/4QxnKs6Nno939ugULM7Lu0Vx5g6FreuCOa2NMWk5rcjIwOzjrZnHZ7aoAVnE7H9scuz8NGnrWdc1Oq0hmcDxdZrdKdB6CPG/diGWNZy77nLvz5JcX1kPLZENPeApCERwR5SvLecA4Es5JORHz9ssEcf8I7VFpAebfQYDu+VZZvEu03P2+5SXv8+5zjiuxM7qxzqRmv0U8eftii9xgVNC7FaoRBhhM7yKkpbnqX7IeSU3WeVcw4+d1d8b9wD/sFOyGc1xAcvafLaGdgeCQGU729DupRRJokpw6bBRQGH29 rasen@omicron"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIulco7Hi0uJ90GLwuDvgG/Xlv/i2R4ySa5+/dOYygsr rasen@bayraktar"
];
};
postCommands = ''
echo "cryptsetup-askpass && exit 0" >> /root/.profile
'';
};
};
}
#+end_src
* Emacs
** Install Emacs
I use emacs from [[https://github.com/nix-community/emacs-overlay][emacs-overlay]].
#+begin_src nix :noweb-ref flake-inputs
emacs-overlay = {
type = "github";
owner = "nix-community";
repo = "emacs-overlay";
inputs.nixpkgs.follows = "";
inputs.nixpkgs-stable.follows = "";
};
#+end_src
Use overlay (~<<flake-overlays>>~).
#+begin_src nix :noweb-ref flake-overlays
inputs.emacs-overlay.overlay
#+end_src
Expose Emacs with my packages as a top-level package (~<<flake-packages>>~).
#+begin_src nix :noweb-ref flake-packages
(let
homebrew-emacs-plus = "https://raw.githubusercontent.com/d12frosted/homebrew-emacs-plus/8da4d9bee657e392aab493b0281b91edd9d9196e";
emacs-base =
if pkgs.stdenv.isDarwin
then pkgs.unstable.emacs31.overrideAttrs (old: {
patches =
(old.patches or [])
++ [
(pkgs.fetchpatch {
url = "${homebrew-emacs-plus}/patches/emacs-31/fix-ns-x-colors.patch";
sha256 = "sha256-oe3DFgEXwp0cZJl+ufWqTonaeWSliikTRsVDNbcy4Yw=";
})
# Make emacs aware of OS-level light/dark mode
(pkgs.fetchpatch {
url = "${homebrew-emacs-plus}/patches/emacs-31/system-appearance.patch";
sha256 = "sha256-4+2U+4+2tpuaThNJfZOjy1JPnneGcsoge9r+WpgNDko=";
})
# Enable rounded window with no decoration
(pkgs.fetchpatch {
url = "${homebrew-emacs-plus}/patches/emacs-31/round-undecorated-frame.patch";
sha256 = "sha256-KCMEvJzN1OkwFYoMLpZghvdeoO1Ckcxk3Mo19YAf850=";
})
(pkgs.fetchpatch {
url = "${homebrew-emacs-plus}/patches/emacs-31/fix-ns-scroll-crash.patch";
sha256 = "sha256-syC9un5Vy1+bmBWIc+TEwTCM/nfPIxd4IhWYdEfP4qE=";
})
];
})
else pkgs.unstable.emacs31.override {
withX = true;
toolkit = "lucid";
withGTK3 = false;
};
emacs-packages = (epkgs:
(with epkgs; [
<<emacs-packages>>
]) ++
(with epkgs.melpaPackages; [
activity-watch-mode
aggressive-indent
atomic-chrome
avy
bash-completion
clojure-mode
cmake-mode
dart-mode
diminish
dockerfile-mode
doom-modeline
edit-indirect
el-patch
exec-path-from-shell
expand-region
fish-completion
fish-mode
flycheck
forge
forth-mode
just-mode
justl
go-mode
gptel
graphviz-dot-mode
haskell-mode
jinja2-mode
json-mode
ledger-mode
lua-mode
markdown-mode
nix-mode
notmuch
ol-notmuch
org-cliplink
org-download
org-ref
org-roam
org-roam-bibtex
org-super-agenda
pass
pip-requirements
plantuml-mode
protobuf-mode
racket-mode
restclient
rust-mode
svelte-mode
swift-mode
terraform-mode
visual-fill-column
vue-mode
wgrep
whitespace-cleanup-mode
yaml-mode
yasnippet
zig-mode
]) ++
[
epkgs.elpaPackages.org
epkgs.nongnuPackages.org-contrib
epkgs.elpaPackages.adaptive-wrap
# not available in melpa
epkgs.elpaPackages.exwm
epkgs.elpaPackages.valign
epkgs.elpaPackages.llm
(epkgs.trivialBuild rec {
pname = "org-fc";
version = "20201121";
src = pkgs.fetchFromGitHub {
owner = "l3kn";
repo = "org-fc";
rev = "cc191458a991138bdba53328690a569b8b563502";
sha256 = "sha256-wzMSgS4iZfpKOICqQQuQYNPb2h7i4tTWsMs7mVmgBt8=";
};
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.imagemagick
pkgs.shellcheck
pkgs.direnv
(pkgs.hunspell.withDicts (dicts: with dicts; [en_US-large uk_UA]))
# 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;
};
})
#+end_src
Install Emacs with Home manager (~<<home-manager-section>>~)
#+begin_src nix :noweb-ref 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;
services.emacs = {
enable = true;
client.enable = true;
defaultEditor = true;
package = config.programs.emacs.finalPackage;
};
# fonts used by emacs
home.packages = [
pkgs.ibm-plex
pkgs.monaspace
pkgs.dejavu_fonts
pkgs.libertine
pkgs.roboto-mono
pkgs.mononoki
pkgs.iosevka
];
}
#+end_src
** 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 [[https://orgmode.org/worg/org-contrib/babel/intro.html#emacs-initialization][org-babel documentation]] for more info.
The following snippet is an adaptation of that idea and goes to my ~.emacs.d/init.el~.
#+begin_src emacs-lisp :tangle .emacs.d/init.el
;;; -*- lexical-binding: t; -*-
;;;
;;; 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)
(require 'el-patch)
(org-babel-load-file (expand-file-name "README.org" rasen/dotfiles-directory))
#+end_src
Another important file that needs to be tangled is [[./.emacs.d/early-init.el]]. For now, just add a header to it.
#+begin_src emacs-lisp :tangle .emacs.d/early-init.el
;;; -*- lexical-binding: t; -*-
;;;
;;; This file is auto-generated from "README.org"
;;;
#+end_src
** GC hacks
Suppress GC in early init and restore it after init is complete. (~.emacs.d/early-init.el~)
#+begin_src emacs-lisp :tangle .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)))
#+end_src
** use-package
[[https://github.com/jwiegley/use-package][use-package]] is a cool emacs library that helps managing emacs configuration making it simpler and more structured. (emacs-lisp)
#+begin_src 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)
#+end_src
** package
All emacs packages are installed with Nix. (See <<install-emacs>>.)
Disable usage of emacs internal archives. (~.emacs.d/early-init.el~)
#+begin_src emacs-lisp :tangle .emacs.d/early-init.el
(require 'package)
(setq package-archives nil)
(setq package-enable-at-startup nil)
#+end_src
** Hard way
Hard way: prohibit usage of keybindings I have more efficient bindings for.
#+begin_src emacs-lisp
(defmacro rasen/hard-way (key)
"Prohibit usage of a keybinding and redirect to use `key' instead."
`(lambda () (interactive) (error "Don't use this key! Use %s instead" ,key)))
#+end_src
** Don't clutter system
Save custom configuration in the =~/.emacs.d/custom.el= file so emacs does not clutter =init.el=.
#+begin_src emacs-lisp
(setq custom-file (expand-file-name "custom.el" user-emacs-directory))
(load custom-file t)
#+end_src
Don't clutter the current directory with backups. Save them in a separate directory.
#+begin_src emacs-lisp
(setq backup-directory-alist '(("." . "~/.emacs.d/backups")))
#+end_src
Don't clutter the current directory with auto-save files.
#+begin_src emacs-lisp
(setq auto-save-file-name-transforms '((".*" "~/.emacs.d/backups/" t)))
#+end_src
Do not create lockfiles either. (I am the only user in the system and only use emacs through daemon, so that should be ok.)
#+begin_src emacs-lisp
(setq create-lockfiles nil)
#+end_src
Store native compilation cache inside =~/.emacs.d/var/eln-cache=. This goes to early init:
#+begin_src emacs-lisp :tangle .emacs.d/early-init.el
(when (and (fboundp 'startup-redirect-eln-cache)
(fboundp 'native-comp-available-p)
(native-comp-available-p))
(startup-redirect-eln-cache
(convert-standard-filename
(expand-file-name "var/eln-cache/" user-emacs-directory))))
#+end_src
** Helpers
Emacs lisp helper functions.
Timestamp-ids are used to uniquely identify things.
#+begin_src emacs-lisp
(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)))
#+end_src
Insert current date in =yyyy-mm-dd= format. Useful when creating dated notes or dumb commits.
#+begin_src emacs-lisp
(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))))
;; (define-key git-commit-mode-map (kbd "C-c .") #'rasen/insert-date)
#+end_src
#+begin_src emacs-lisp
(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)))
#+end_src
#+begin_src emacs-lisp
(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)))
#+end_src
Shamelessly stolen from https://github.com/purcell/emacs.d.
#+begin_src emacs-lisp
(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)))
#+end_src
#+begin_src emacs-lisp
(defun add-to-path (str)
"Add an STR to the PATH environment variable."
(setenv "PATH" (concat str ":" (getenv "PATH"))))
#+end_src
** avy
Jump anywhere with a few keystrokes in tree-like way.
#+begin_src emacs-lisp
(use-package avy
:custom
;; easy workman keys (excluding pinky)
(avy-keys '(?s ?h ?t ?n ?e ?o ?d ?r ?u ?p)))
#+end_src
** wgrep
Edit grep buffers and apply changes to the files.
#+begin_src emacs-lisp
(use-package wgrep)
#+end_src
** whitespace
A good mode to highlight whitespace issues (leading/trailing spaces/newlines) and too long lines.
#+begin_src emacs-lisp
(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)))
#+end_src
** whitespace-cleanup
Fix whitespaces on file save.
#+begin_src emacs-lisp
(use-package whitespace-cleanup-mode
:diminish whitespace-cleanup-mode
:config
(global-whitespace-cleanup-mode 1))
#+end_src
** Spell-check
hunspell is installed in emacs packages.
Fix Ukrainian dictionary to play nicely with English one.
#+begin_src nix :noweb-ref flake-overlays
(final: prev: {
hunspellDicts = prev.hunspellDicts // {
# uk_UA affix file strips all latin characters from input, so
# combining en_US,uk_UA stops spell-checking english words
# entirely.
#
# Remove these iconv lines, so that we can use both dictionaries
# together.
uk_UA = prev.hunspellDicts.uk_UA.overrideAttrs (old: {
postInstall = ''
sed -i 's/^ICONV 64$/ICONV 2/' $out/share/hunspell/uk_UA.aff
sed -i '/^ICONV [^ ]* 0$/d' $out/share/hunspell/uk_UA.aff
'';
});
};
})
#+end_src
Make spellcheck work with Unicode apostrophe.
#+begin_src emacs-lisp
(use-package ispell
:defer 10
:config
(setq ispell-program-name "hunspell")
(setq ispell-dictionary "en_US,uk_UA")
(ispell-set-spellchecker-params)
(ispell-hunspell-add-multi-dic "en_US,uk_UA")
(ispell-change-dictionary "en_US,uk_UA" t)
;; This needs to be overriden for some reason, even though
;; ispell-dictionary-alist has the same value.
(setq ispell-local-dictionary-alist
`(("en_US,uk_UA" "[[:alpha:]]" "[^[:alpha:]]"
"['0-9`’-]" nil ("-d" "en_US,uk_UA") nil utf-8)))
;; Don't send ’ to the subprocess.
(defun rasen/replace-apostrophe (args)
(cons (replace-regexp-in-string
"’" "'" (car args))
(cdr args)))
(advice-add #'ispell-send-string :filter-args
#'rasen/replace-apostrophe)
;; Convert ' back to ’ from the subprocess.
(defun rasen/replace-quote (args)
(if (not (derived-mode-p 'org-mode))
args
(cons (replace-regexp-in-string
"'" "’" (car args))
(cdr args))))
(advice-add #'ispell-parse-output :filter-args
#'rasen/replace-quote))
#+end_src
** tab-bar-mode
New in Emacs 27.
#+begin_src emacs-lisp
(use-package tab-bar
:config
(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)
)
#+end_src
** Highlight current line
Highlight current line.
#+begin_src emacs-lisp
(use-package emacs
:config
(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)))
#+end_src
** 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.)
#+begin_src emacs-lisp
(setq scroll-margin 3
scroll-conservatively 101)
#+end_src
** visual-fill-column
Center all text in the buffer in some modes. (That's a nice distraction-free setup.)
#+begin_src emacs-lisp
(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))
#+end_src
** Completion
#+begin_src emacs-lisp
(use-package minibuffer
:ensure nil
:bind (:map minibuffer-visible-completions-up-down-map
("C-n" . minibuffer-next-completion)
("C-p" . minibuffer-previous-completion))
:hook ((minibuffer-setup . cursor-intangible-mode))
:custom
(tab-always-indent 'complete) ; Make TAB invoke complete
(tab-first-completion 'word) ; Don't complete mid-word
(completion-auto-help t)
(completion-auto-select nil) ; Don't auto-select *Completions* buffer
(completion-eager-update t)
(completion-eager-display t) ; Display completions buffer immediately
(minibuffer-visible-completions 'up-down)
(completion-ignore-case t)
(read-file-name-completion-ignore-case t)
(read-buffer-completion-ignore-case t)
(completion-show-help nil)
(completion-styles '(partial-completion flex))
(completions-format 'one-column) ; Show completions in a single column
(completions-max-height 16)
(completions-sort 'historical)
(enable-recursive-minibuffers t)
(minibuffer-prompt-properties '(read-only t intangible t cursor-intangible t face minibuffer-prompt))
(minibuffer-depth-indicate-mode t)
(minibuffer-electric-default-mode t))
(use-package completion-preview
:ensure nil
;; :bind (:map completion-preview-active-mode
;; ("M-n" . )
;; ("M-p" . nil)
;; ("C-i" . nil))
:config
(setq completion-preview-minimum-symbol-length 2)
(global-completion-preview-mode 1))
(use-package savehist
:config
(savehist-mode t))
#+end_src
** Misc
Use single-key =y/n= instead of a more verbose =yes/no=.
#+begin_src emacs-lisp
(fset 'yes-or-no-p 'y-or-n-p)
#+end_src
Automatically add a final newline in files.
#+begin_src emacs-lisp
(setq-default require-final-newline t)
#+end_src
* Environment
** EXWM
Emacs is my Window Manager, thanks to [[https://github.com/ch11ng/exwm][EXWM]].
NixOS has an EXWM module, but my feeling is that it's too limiting. (~<<nixos-section>>~)
#+begin_src nix :noweb-ref nixos-section
{
environment.systemPackages = [ pkgs.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.displayManager.defaultSession = "none+exwm";
}
#+end_src
Initialize EXWM configuration (emacs-lisp)
#+begin_src 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)))
;; do not forward anything besides keys defined with
;; `exwm-input-set-key' and `exwm-mode-map'
(setq exwm-input-prefix-keys '())
(when (eq system-type 'gnu/linux)
(exwm-enable)))
#+end_src
Add a couple of helpers functions. (emacs-lisp)
#+begin_src 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)
(exwm-input--update-global-prefix-keys))
#+end_src
** Window management
Common key bindings. (emacs-lisp)
#+begin_src emacs-lisp
(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") #'execute-extended-command)
(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") #'switch-to-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-n") #'yabai-windmove-left)
(exwm-input-set-key (kbd "s-e") #'yabai-windmove-down)
(exwm-input-set-key (kbd "s-u") #'yabai-windmove-up)
(exwm-input-set-key (kbd "s-o") #'yabai-windmove-right)
(exwm-input-set-key (kbd "s-h") (rasen/hard-way "s-n"))
(exwm-input-set-key (kbd "s-l") (rasen/hard-way "s-o"))
(exwm-input-set-key (kbd "s-j") (rasen/hard-way "s-u"))
(exwm-input-set-key (kbd "s-k") (rasen/hard-way "s-e"))
(exwm-input-set-key (kbd "s-s") #'split-window-below)
(exwm-input-set-key (kbd "s-v") #'split-window-right)
(exwm-input-set-key (kbd "s-.") #'rasen/exwm-next-workspace)
(exwm-input-set-key (kbd "s->") #'rasen/move-tab-other-frame) ;; s-S-.
(define-prefix-command 'rasen/tab-map)
(define-key rasen/tab-map (kbd "T") #'tab-bar-mode)
(define-key rasen/tab-map (kbd "1") #'tab-bar-select-tab)
(define-key rasen/tab-map (kbd "2") #'tab-bar-select-tab)
(define-key rasen/tab-map (kbd "3") #'tab-bar-select-tab)
(define-key rasen/tab-map (kbd "4") #'tab-bar-select-tab)
(define-key rasen/tab-map (kbd "5") #'tab-bar-select-tab)
(define-key rasen/tab-map (kbd "6") #'tab-bar-select-tab)
(define-key rasen/tab-map (kbd "7") #'tab-bar-select-tab)
(define-key rasen/tab-map (kbd "8") #'tab-bar-select-tab)
(define-key rasen/tab-map (kbd "9") #'tab-bar-select-tab)
(define-key rasen/tab-map (kbd "t") #'tab-bar-new-tab)
(define-key rasen/tab-map (kbd "n") #'tab-bar-switch-to-prev-tab)
(define-key rasen/tab-map (kbd "o") #'tab-bar-switch-to-next-tab)
(define-key rasen/tab-map (kbd ">") #'tab-bar-move-tab-to-frame)
(define-key rasen/tab-map (kbd "k") #'tab-bar-close-tab)
(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>") #'ghostel)
(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)
;; (define-key exwm-mode-map (kbd "C-c") nil)
(define-key exwm-mode-map (kbd "<f1> v") #'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)
#+end_src
** Window layout
Rules to automatically layout windows when they appear.
#+begin_src emacs-lisp
(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)))))
#+end_src
** Screen locking
I use ~xss-lock~ + ~slock~ for screen locking. Actual handling is coded in Emacs.
*** Slock
[[http://tools.suckless.org/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:
#+begin_src fundamental :tangle no
slock: unable to disable OOM killer. Make sure to suid or sgid slock.
#+end_src
#+begin_src nix :noweb-ref nixos-section
{
programs.slock.enable = true;
}
#+end_src
*** xss-lock
[[https://bitbucket.org/raymonad/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.
#+begin_src nix :noweb-ref home-manager-section
{
home.packages = pkgs.lib.linux-only [
pkgs.xss-lock
];
}
#+end_src
*** EXWM integration
Autostart ~xss-lock~ (emacs-lisp).
#+begin_src emacs-lisp
(when (eq system-type 'gnu/linux)
(rasen/autostart "xss-lock -n \"xset dpms force off\" slock"))
#+end_src
Bind ~s-M-l~ to lock screen immediately.
#+begin_src emacs-lisp
(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)
#+end_src
** System tray
Use built-in EXWM system tray (emacs-lisp)
#+begin_src emacs-lisp
(use-package exwm-systemtray
:after exwm
:config
(exwm-systemtray-mode))
#+end_src
** Screenshots
I use [[https://github.com/Roger/escrotum][Escrotum]] for screenshots.
Install it. (~<<home-manager-section>>~)
#+begin_src nix :noweb-ref home-manager-section
{
home.packages = pkgs.lib.linux-only [ pkgs.escrotum ];
}
#+end_src
Bind it to Print Screen button. (emacs-lisp)
#+begin_src 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)
#+end_src
** Misc
I definitely use X server:
#+begin_src nix :noweb-ref nixos-section
{
services.xserver.enable = true;
}
#+end_src
Use English as my only supported locale:
#+begin_src nix :noweb-ref nixos-section
{
i18n.supportedLocales = [ "en_US.UTF-8/UTF-8" ];
}
#+end_src
Setup timezone:
#+begin_src nix :noweb-ref nixos-section
{
time.timeZone = "Europe/Kiev";
}
#+end_src
* Input
** Keyboard
*** Workman
I use [[https://workmanlayout.org/][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:
#+DOWNLOADED: https://nic-west.com/images/workman.png @ 2020-06-13 02:39:31
[[file:images/20200612233931-workman.png]]
*** Keyboard layout
Besides Workman, I use Ukrainian layout. I also use Russian symbols, but they are on the third level (~<<nixos-section>>~).
#+begin_src nix :noweb-ref nixos-section
{
services.xserver.xkb.layout = "us,ua";
services.xserver.xkb.variant = "workman,";
# Use same config for linux console
console.useXkbConfig = true;
}
#+end_src
Same setting but for Home Manager (~<<home-manager-section>>~)
#+begin_src nix :noweb-ref home-manager-section
{
home.keyboard = {
layout = "us,ua";
variant = "workman,";
};
}
#+end_src
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>>~:
#+begin_src nix :noweb-ref nixos-section
{
services.xserver.xkb.options = "grp:lctrl_toggle,grp_led:caps,ctrl:nocaps";
# services.xserver.xkbOptions = "grp:caps_toggle,grp_led:caps";
}
#+end_src
On macOS, right option acts as AltGr. Make Emacs ignore it, so it can work correctly as a Meta modifier:
#+begin_src emacs-lisp
(setq ns-right-alternate-modifier 'none)
#+end_src
*** 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>>~).
#+begin_src nix :noweb-ref home-manager-section
{
xsession.initExtra = ''
xkbcomp ${./Xkeymap} $DISPLAY
'';
}
#+end_src
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).
#+begin_src emacs-lisp
(when (eq system-type 'gnu-linux)
(defun rasen/set-xkb-layout ()
(interactive)
(rasen/autostart "xkbcomp ~/dotfiles/Xkeymap $DISPLAY"))
(rasen/set-xkb-layout))
#+end_src
Install xkbcomp to execute these commands. (~<<home-manager-section>>~)
#+begin_src nix :noweb-ref home-manager-section
{
home.packages = [ pkgs.xkbcomp ];
}
#+end_src
*** Compose keys
Add some custom compose keys (~<<home-manager-section>>~):
#+begin_src nix :noweb-ref 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.
'';
}
#+end_src
*** xcape
Make short press on left control behave as Escape (=<<home-manager-section>>=):
#+begin_src nix :noweb-ref home-manager-section
{
services.xcape = {
enable = pkgs.stdenv.isLinux;
mapExpression = {
Control_L = "Escape";
};
};
}
#+end_src
*** 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.
#+begin_src emacs-lisp
(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"))
#+end_src
* Network
** NetworkManager
(~<<nixos-section>>~)
#+begin_src nix :noweb-ref nixos-section
{
networking = {
hostName = name;
networkmanager = {
enable = true;
wifi.powersave = false;
};
# disable wpa_supplicant
wireless.enable = false;
};
users.extraUsers.rasen.extraGroups = [ "networkmanager" ];
}
#+end_src
Install network manager applet for user. (~<<home-manager-section>>~)
#+begin_src nix :noweb-ref home-manager-section
{
home.packages = pkgs.lib.linux-only [ pkgs.networkmanagerapplet ];
}
#+end_src
Auto-start ~nm-applet~ (emacs-lisp)
#+begin_src emacs-lisp
(rasen/autostart "nm-applet")
#+end_src
** SSH
(~<<nixos-section>>~)
#+begin_src nix :noweb-ref nixos-section
{
services.openssh = {
enable = true;
settings.PasswordAuthentication = false;
};
}
#+end_src
*** Mosh
[[https://mosh.mit.edu/][Mosh (mobile shell)]] is a cool addition to ssh.
#+begin_src nix :noweb-ref nixos-section
{
programs.mosh.enable = true;
}
#+end_src
** dnsmasq
Use [[http://www.thekelleys.org.uk/dnsmasq/doc.html][dnsmasq]] as a DNS cache.
(~<<nixos-section>>~)
#+begin_src nix :noweb-ref nixos-section
{
services.dnsmasq = {
enable = true;
settings = {
# These are used in addition to resolv.conf
server = [
"8.8.8.8"
"8.8.4.4"
];
interface = "lo";
bind-interfaces = true;
listen-address = "127.0.0.1";
cache-size = 1000;
no-negcache = true;
};
};
}
#+end_src
** Firewall
Enable firewall. This blocks all ports for ingress traffic and pings.
(~<<nixos-section>>~)
#+begin_src nix :noweb-ref nixos-section
{
networking.firewall = {
enable = true;
allowPing = false;
connectionTrackingModules = [];
autoLoadConntrackHelpers = false;
};
}
#+end_src
* Services
** Locate
Update [[https://linux.die.net/man/1/locate][locate]] database daily.
#+begin_src nix :noweb-ref nixos-section
{
services.locate = {
enable = true;
};
}
#+end_src
** Gitolite
#+begin_src nix :noweb-ref nixos-section
{
services.gitolite = {
enable = true;
user = "git";
adminPubkey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDHH15uiQw3jBbrdlcRb8wOr8KVltuwbHP/JOFAzXFO1l/4QxnKs6Nno939ugULM7Lu0Vx5g6FreuCOa2NMWk5rcjIwOzjrZnHZ7aoAVnE7H9scuz8NGnrWdc1Oq0hmcDxdZrdKdB6CPG/diGWNZy77nLvz5JcX1kPLZENPeApCERwR5SvLecA4Es5JORHz9ssEcf8I7VFpAebfQYDu+VZZvEu03P2+5SXv8+5zjiuxM7qxzqRmv0U8eftii9xgVNC7FaoRBhhM7yKkpbnqX7IeSU3WeVcw4+d1d8b9wD/sFOyGc1xAcvafLaGdgeCQGU729DupRRJokpw6bBRQGH29 rasen@omicron";
};
}
#+end_src
** Syncthing
I use Syncthing to sync my org-mode files to my phone.
#+begin_src nix :noweb-ref nixos-section
{
services.syncthing = {
enable = true;
user = "rasen";
dataDir = "/home/rasen/.config/syncthing";
configDir = "/home/rasen/.config/syncthing";
openDefaultPorts = true;
};
}
#+end_src
On Darwin:
#+begin_src nix :noweb-ref darwin-section
{
environment.systemPackages = [ pkgs.syncthing ];
}
#+end_src
** Backup
I use borg for backups.
#+begin_src nix :noweb-ref machine-omicron
(let
commonOptions = {
# repo = "borg@10.13.0.3:.";
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;
};
})
#+end_src
Mount external drive when needed.
#+begin_src nix :noweb-ref nixos-section
{
# 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
];
};
}
#+end_src
** direnv
*** direnv + nix-direnv
direnv allows having per-directory environment configuration. You can think of automatic virtualenv, but it's more general and supports unloading.
(~<<home-manager-section>>~)
#+begin_src nix :noweb-ref home-manager-section
{
programs.direnv.enable = true;
programs.direnv.nix-direnv.enable = true;
}
#+end_src
direnv integration into Emacs via [[https://github.com/purcell/envrc][purcell/envrc]]:
#+begin_src nix :noweb-ref emacs-packages
epkgs.melpaPackages.envrc
#+end_src
(emacs-lisp)
#+begin_src emacs-lisp
(use-package envrc
:after exec-path-from-shell
:hook (after-init . envrc-global-mode))
#+end_src
** VirtualBox
#+begin_src nix :noweb-ref nixos-section
{
virtualisation.virtualbox.host.enable = true;
users.extraGroups.vboxusers.members = ["rasen"];
}
#+end_src
** User-level services
Install runit to manage user-level services.
#+begin_src nix :noweb-ref home-manager-section
{
home.packages = [ pkgs.runit ];
}
#+end_src
* Hardware
** Do not suspend on AC
#+begin_src nix :noweb-ref nixos-section
{
services.logind = {
lidSwitchDocked = "ignore";
lidSwitchExternalPower = "ignore";
};
}
#+end_src
** Autorandr
Configure EXWM to use autorandr. (emacs-lisp)
#+begin_src 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-mode))
#+end_src
(~<<home-manager-section>>~)
#+begin_src nix :noweb-ref 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";
};
};
};
};
};
}
#+end_src
** Screen brightness
=xbacklight= stopped working recently. =acpilight= is a drop-in replacement.
#+begin_src nix :noweb-ref nixos-section
{
hardware.acpilight.enable = true;
environment.systemPackages = [ pkgs.acpilight ];
users.extraUsers.rasen.extraGroups = [ "video" ];
}
#+end_src
For Home Manager–managed hosts.
#+begin_src nix :noweb-ref home-manager-section
{
home.packages = pkgs.lib.linux-only [ pkgs.acpilight ];
}
#+end_src
Bind it to keys (emacs-lisp).
#+begin_src 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")))
#+end_src
** Redshift
Redshift adjusts the color temperature of the screen according to the position of the sun.
Blue light blocks [[https://en.wikipedia.org/wiki/Melatonin][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).
#+begin_src nix :noweb-ref nixos-section
{
services.redshift = {
enable = true;
};
location.provider = "geoclue2";
}
#+end_src
** PipeWire
Use PipeWire as audio server.
#+begin_src nix :noweb-ref nixos-section
{
# PipeWire requires pulseaudio to be disabled.
services.pulseaudio.enable = false;
security.rtkit.enable = true;
services.pipewire = {
enable = true;
alsa.enable = true;
alsa.support32Bit = true;
pulse.enable = true;
};
}
#+end_src
=pavucontrol= is PulseAudio Volume Control—a nice utility for controlling pulseaudio settings. (~<<home-manager-section>>~)
#+begin_src nix :noweb-ref home-manager-section
{
home.packages = pkgs.lib.linux-only [ pkgs.pavucontrol ];
}
#+end_src
#+begin_src emacs-lisp
(defun rasen/pavucontrol ()
(interactive)
(rasen/switch-start "Pavucontrol" "pavucontrol"))
#+end_src
Bind volume control commands. (emacs-lisp)
#+begin_src 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%-")))
#+end_src
** Bluetooth
I have a bluetooth headset, so this enables bluetooth audio in NixOS.
(~<<nixos-section>>~)
#+begin_src nix :noweb-ref nixos-section
{
hardware.bluetooth.enable = true;
}
#+end_src
** ADB
I need to access my Android device. (~<<nixos-section>>~)
#+begin_src nix :noweb-ref nixos-section
{
services.udev.packages = [ pkgs.android-udev-rules ];
programs.adb.enable = true;
users.users.rasen.extraGroups = ["adbusers"];
}
#+end_src
** fwupd
fwupd is a service that allows applications to update firmware. (~<<nixos-section>>~)
#+begin_src nix :noweb-ref nixos-section
{
services.fwupd.enable = true;
}
#+end_src
Execute the following command to update firmware.
#+begin_src sh :tangle no
fwupdmgr get-updates
#+end_src
* Browsers
Firefox is default, Chrome for backup.
#+begin_src nix :noweb-ref home-manager-section
{
home.packages = pkgs.lib.linux-only [
pkgs.firefox
pkgs.google-chrome
];
}
#+end_src
** Tridactyl
Tridactyl is a Firefox plugin that provides Vim-like bindings.
Here is my config. (~<<tridactylrc>>~)
#+begin_src fundamental :noweb-ref tridactylrc :tangle no
" drop all existing configuration
sanitize tridactyllocal tridactylsync
bind J scrollline -10
bind K scrollline 10
bind j scrollline -2
bind k scrollline 2
#+end_src
Link tridactyl config to the place the tridactyl can find it. (~<<home-manager-section>>~)
#+begin_src nix :noweb-ref home-manager-section
{
xdg.configFile."tridactyl/tridactylrc".text = ''
<<tridactylrc>>
'';
}
#+end_src
** Edit text in browser
I use [[https://github.com/GhostText/GhostText][GhostText]] firefox extension.
~atomic-chrome~ Emacs extension is compatible with it. (emacs-lisp)
#+begin_src 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))
#+end_src
* Key bindings
I have my custom modal editing mode in [[file:.emacs.d/site-lisp/eon.el][eon.el]].
#+begin_src emacs-lisp
(use-package eon
:load-path "~/.emacs.d/user-lisp/"
:config
(eon-global-mode))
#+end_src
** Compile mode
#+begin_src emacs-lisp
(use-package compile ; built-in
:config
(setq compilation-scroll-output t))
#+end_src
** Minibuffer
#+begin_src emacs-lisp
(define-key minibuffer-local-map (kbd "C-e") #'exit-minibuffer)
#+end_src
** scheme
#+begin_src emacs-lisp
(use-package scheme
:config
(put 'module 'scheme-indent-function 'defun))
#+end_src
* Org-mode
** General
#+begin_src emacs-lisp
(use-package org
:mode ("\\.org$" . org-mode)
;; :bind
;; ("C-c l" . org-store-link)
;; ("s-SPC c" . org-capture)
;; ("s-SPC a" . org-agenda)
;; ("s-SPC o" . org-clock-out)
;; ("s-SPC l" . org-clock-in-last)
;; ("s-SPC j" . org-clock-goto)
;; :bind-keymap
;; ("s-SPC" . org-mode-map)
:hook (org-mode . flyspell-mode)
:init
<<org-init>>
:config
<<org-config>>
)
#+end_src
Do not indent inside tasks
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-adapt-indentation nil)
#+end_src
Do not indent org-babel blocks.
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-edit-src-content-indentation 0)
#+end_src
Do not indent tags.
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-tags-column 0)
#+end_src
When entering tags, offer tags from all agenda files. (This is the closes to global tag tracking I could find.))
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-complete-tags-always-offer-all-agenda-tags t)
#+end_src
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-ellipsis "…")
#+end_src
By default, show all of org file. (This can be changed on a per-file basis with ~#+STARTUP:~.)
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-startup-folded nil)
#+end_src
Make the table header float if scrolled out of view.
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-table-header-line-p t)
#+end_src
Hide emphasis markers (asterisks and slashes).
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-hide-emphasis-markers t)
#+end_src
Allow emphasis marks to follow ndash/mdash, and proper quotes.
#+begin_src emacs-lisp :noweb-ref org-config
;; 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))
#+end_src
Open pdfs in external viewer:
#+begin_src emacs-lisp :noweb-ref org-config
(add-to-list 'org-file-apps
`("\\.pdf\\'" . ,(if (eq system-type 'darwin)
"open %s"
"zathura %s")))
#+end_src
Use =whitespace-mode= in Org (but don't show too long lines).
#+begin_src emacs-lisp :noweb-ref org-config
(add-hook 'org-mode-hook (lambda ()
(setq-local whitespace-style '(face
tab-mark
empty
trailing))
(whitespace-mode t)))
#+end_src
My directory for org files.
#+begin_src emacs-lisp :noweb-ref org-config
(setq rasen/org-directory "~/org")
#+end_src
My helper to find all org files in a directory.
#+begin_src emacs-lisp :noweb-ref org-config
(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))
#+end_src
Package for =f-files= and =f-ext?= functions.
#+begin_src emacs-lisp :noweb-ref org-init
(use-package f
:commands (f-files f-ext? f-no-ext))
#+end_src
** Electric quote
Use proper quotes in org-mode.
#+begin_src emacs-lisp
(use-package org
:defer t
:config
(add-hook 'org-mode-hook #'electric-quote-local-mode))
#+end_src
** Todo
A special function that marks the task as done yesterday if prefix is supplied (useful for habits).
#+begin_src emacs-lisp :noweb-ref org-config
(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)))
#+end_src
Use the following states: =TODO= =NEXT= =DONE= =CANCELED= =WAIT=.
#+begin_src emacs-lisp :noweb-ref org-config
(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)
#+end_src
When repeated task is finished, go back to =TODO= state.
#+begin_src emacs-lisp :noweb-ref org-config
(setq-default org-todo-repeat-to-state "NEXT")
#+end_src
Log state changes to "LOGBOOK" drawer.
#+begin_src emacs-lisp :noweb-ref org-config
(setq-default org-log-into-drawer t)
#+end_src
Save =CLOSED= timestamp when task is done.
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-log-done t)
#+end_src
Disable force-logging repeated tasks because otherwise you’ll get duplicated log lines.
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-log-repeat nil)
#+end_src
Fontify the whole line for done tasks.
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-fontify-done-headline t)
#+end_src
Import =org-expiry= for =org-expiry-insert-created=---this inserts =CREATED= property.
#+begin_src emacs-lisp :noweb-ref org-config
(require 'org-expiry)
(setq org-expiry-inactive-timestamps t)
;; The next line was required but it fails with error now. Still works
;; somehow.
;; (org-expiry-insinuate)
#+end_src
Schedule task for today and mark it NEXT.
I use this a lot during daily planning.
#+begin_src emacs-lisp :noweb-ref org-config
(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"))
#+end_src
A command to fold everything except =NEXT= items.
#+begin_src emacs-lisp :noweb-ref org-config
(defun rasen/org-occur-next ()
(interactive)
(let ((org-highlight-sparse-tree-matches nil))
(org-occur (concat "^" org-outline-regexp " *" "NEXT" "\\>"))))
#+end_src
Disable highlighting for org-occur.
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-highlight-sparse-tree-matches nil)
#+end_src
** Highlight projects
Fontify all headlines with the =:PROJECT:= tag.
#+begin_src emacs-lisp :noweb-ref org-config
(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)
#+end_src
Note that the face is overridden in [[*Color theme][Color theme]] section.
** Clocking
Remove clocks with 0 duration.
#+begin_src emacs-lisp :noweb-ref org-config
(setq-default org-clock-out-remove-zero-time-clocks t)
#+end_src
Save more last clocks.
#+begin_src emacs-lisp :noweb-ref org-config
(setq-default org-clock-history-length 10)
#+end_src
** Capture
I use an extension that adds page url to the title (used for page tracking). Strip it down here
#+begin_src emacs-lisp :noweb-ref org-config
(defun rasen/strip-url-from-title (title)
(message "stripping: %s" title)
(replace-regexp-in-string
" @ [^ ]*$"
""
(replace-regexp-in-string " \\[[^]]*\\]\\[[^]]*\\]$" "" title)))
#+end_src
My capture templates.
#+begin_src emacs-lisp :noweb-ref org-config
(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"))
#+end_src
Enable org-protocol.
#+begin_src emacs-lisp :noweb-ref org-config
(require 'org-protocol)
#+end_src
=%l= in org-capture fails with multiline context, so use only the first line as a context.
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-context-in-file-links 1)
#+end_src
** Capturing images
#+begin_src emacs-lisp
(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))))
#+end_src
** 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.
#+begin_src emacs-lisp
;; 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)))
#+end_src
** cliplink
#+begin_src emacs-lisp
(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))
#+end_src
** Refile
#+begin_src emacs-lisp :noweb-ref org-config
(defun rasen/org-refile-files ()
(append
'("~/org/dd/index.org" "~/org/fluxon/index.org")
(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")
#+end_src
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.
#+begin_src emacs-lisp :noweb-ref org-config
(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))))
#+end_src
*** Refiling with hydras
Adapted from [[https://mollermara.com/blog/Fast-refiling-in-org-mode-with-hydras/][Fast refiling in org-mode with hydras | Josh Moller-Mara]]. Extended to support refiling by outline path.
#+begin_src emacs-lisp :noweb-ref org-config
(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 'org-mode-map "w" #'rasen/org-refile-hydra/body)
;; (leader-def 'org-agenda-mode-map "w" #'rasen/org-refile-hydra/body)
;; (leader-def 'org-capture-mode-map "w" #'rasen/org-refile-hydra/body)
#+end_src
*** 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.
#+begin_src emacs-lisp :noweb-ref org-config
(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)))))))
#+end_src
** Archive
#+begin_src emacs-lisp :noweb-ref org-config
;; (setq-default org-archive-default-command 'org-archive-to-archive-sibling)
(setq-default org-archive-default-command #'org-archive-subtree)
#+end_src
** Agenda
Set my org files location.
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-directory "~/org"
org-default-notes-file rasen/org-refile-file
org-agenda-files (cons "~/org/roam/dd/index.org" (cons "~/org/roam/fluxon/index.org" (rasen/org-files-in-dir "~/org"))))
#+end_src
Show agenda as the only window and restore window layout on quit (so that agenda does not mess up with my layout).
#+begin_src emacs-lisp
(setq org-agenda-window-setup 'only-window)
(setq org-agenda-restore-windows-after-quit t)
#+end_src
Configure my agenda view.
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-agenda-span 6)
#+end_src
Configure stuck projects.
#+begin_src emacs-lisp :noweb-ref org-config
(add-to-list 'org-tags-exclude-from-inheritance "PROJECT")
(setq org-stuck-projects
'("+PROJECT/-TODO-DONE-CANCELED-WAIT" ("NEXT" "WAIT") nil ""))
#+end_src
Do not align tags in agenda.
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-agenda-tags-column 0)
#+end_src
Do not show project tags.
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-agenda-hide-tags-regexp "PROJECT\\|fc\\|suspended")
#+end_src
#+begin_src emacs-lisp
(use-package org-super-agenda
:bind
(:map org-super-agenda-header-map
("k" . #'org-agenda-next-line)
("j" . #'org-agenda-previous-line))
:config
;; 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 "......" "----------------")
)
(defmacro rasen/mk-agenda (agenda-files inbox-files extra-categories)
`'((agenda "" ((org-agenda-files ,agenda-files)
;; start from yesterday
(org-agenda-start-day "-1d")
;; show 8 days
(org-agenda-span 8)
;; 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-habit-show-done-always-green t)
(org-super-agenda-groups
'(;; 1. time-grid
;; 2. deadlines
;; 3. scheduled
;; 3.5. WAIT scheduled today
;; 4. habits
;; 5. rest (anniversaries, etc.)
(: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 ; deadlines
:deadline t
:order 2)
(:name none ;; scheduled today
:and (:todo ("WAIT")
:scheduled today)
:order 3.5)
(:name none ; scheduled
:scheduled t
:order 3)
;; (:name none ;; scheduled today
;; :scheduled today
;; :order 2)
;; (:name none ;; deadline today
;; :deadline today
;; :order 1.7)
;; (:name none ;; deadline reminders
;; :deadline t
;; :order 1.5)
;; (:name none ;; scheduled reminders
;; :deadline t
;; :order 1.9)
(:name none ;; everything else
:anything t
:order 5)))))
(alltodo "" ((org-agenda-files ,agenda-files)
(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)
,@extra-categories
(:name "Next"
:todo "NEXT"
:order 8.99)
(: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 ,inbox-files)
(org-agenda-overriding-header "")
(org-super-agenda-groups
'((:name "Inbox"
:auto-category t
:anything t)))))))
(setq org-agenda-custom-commands
`(("o" "Overview" ,(rasen/mk-agenda
'("~/org/plan.org")
(mapcar (lambda (x) (concat rasen/org-directory "/" x))
'("refile-omicron.org"
"orgzly.org"))
nil))
("f" "Fluxon" ,(rasen/mk-agenda
'("~/org/roam/fluxon/index.org" "~/org/refile-bayraktar.org")
'("~/org/refile-bayraktar.org")
nil))
("d" "DataDog" ,(rasen/mk-agenda
'("~/org/roam/dd/index.org" "~/org/refile-oleksiishmalko.org")
'("~/org/refile-oleksiishmalko.org")
nil))
("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))
#+end_src
*** 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=.
#+begin_src emacs-lisp
(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))))))
#+end_src
** Babel
Code-highlight (fontify) org-babel (=#+begin_src=) blocks.
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-src-fontify-natively t)
#+end_src
Do not confirm evaluation for emacs-lisp.
#+begin_src emacs-lisp :noweb-ref org-config
(defun rasen/org-confirm-babel-evaluate (lang body)
(not (member lang '("emacs-lisp"))))
(setq org-confirm-babel-evaluate 'rasen/org-confirm-babel-evaluate)
#+end_src
Load more languages:
#+begin_src emacs-lisp :noweb-ref org-config
(org-babel-do-load-languages 'org-babel-load-languages
'((shell . t)))
#+end_src
** Latex preview
Install imagemagick and latex:
#+begin_src nix :noweb-ref home-manager-section
{
home.packages = [
pkgs.imagemagick
# 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
];
}
#+end_src
#+begin_src emacs-lisp :noweb-ref org-config
(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")))
#+end_src
** Image preview
#+begin_src emacs-lisp :noweb-ref org-config
;; Scale inline images by default
(setq org-image-actual-width '(800))
;; Show inline images by default
(setq org-startup-with-inline-images t)
#+end_src
** 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.
#+begin_src emacs-lisp :noweb-ref org-config
(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"))
#+end_src
** Crypt
Allow encrypted entries in org files.
#+begin_src emacs-lisp :noweb-ref org-config
(require 'org-crypt)
(org-crypt-use-before-save-magic)
(add-to-list 'org-tags-exclude-from-inheritance "crypt")
(setq org-crypt-key "rasen.dubi@gmail.com")
(add-hook 'org-babel-pre-tangle-hook 'org-decrypt-entries t)
#+end_src
** 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:
#+begin_src emacs-lisp :noweb-ref org-config
(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)))))
#+end_src
** org-checklist
Setting property =RESET_CHECK_BOXES= on a periodic task to =t= will clear all checkboxes when the task is closed.
#+begin_src emacs-lisp :noweb-ref org-config
(require 'org-checklist)
#+end_src
** Habits
#+begin_src emacs-lisp :noweb-ref org-config
(require 'org-habit)
(setq org-habit-show-habits-only-for-today t)
(setq org-habit-preceding-days 25)
(setq org-habit-following-days 3)
#+end_src
** adaptive-wrap
Better line wrapping. (Use proper wrap-prefix in lists, etc.)
#+begin_src emacs-lisp
(use-package adaptive-wrap
:config
(add-hook 'org-mode-hook #'adaptive-wrap-prefix-mode))
#+end_src
** org-id
Use short timestamps as ids. It has less precision but that’s enough for me.
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-id-method 'ts
org-id-ts-format "%Y%m%d%H%M%S")
#+end_src
Configure ~org-store-link~ to prefer ids for headlines.
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-id-link-to-org-use-id 'create-if-interactive)
#+end_src
Override ~org-id-new~ to use UTC timezone, so ids are not affected by daylight saving and my timezone.
#+begin_src emacs-lisp :noweb-ref org-config
(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 (when org-id-include-domain
(require 'message)
(concat "@" (message-make-fqdn)))))
(setq unique (concat etime postfix))))
((eq org-id-method 'ts)
(let ((ts (format-time-string org-id-ts-format (el-patch-add nil "UTC")))
(postfix (when org-id-include-domain
(require 'message)
(concat "@" (message-make-fqdn)))))
(setq unique (concat ts postfix))))
(t (error "Invalid `org-id-method'")))
(concat prefix unique)))
#+end_src
Highlight org-id links with different color.
#+begin_src emacs-lisp :noweb-ref org-config
(defface rasen/org-id-link
'((t :inherit org-link))
"Face for org-id links.")
(org-link-set-parameters "id" :face 'rasen/org-id-link)
#+end_src
Actual style is overridden in Color theme section.
** org-roam
#+begin_src emacs-lisp
(use-package org-roam
:after org
:diminish
:autoload (org-roam-db-update-file)
:commands (rasen/org-daily-dispatch)
:bind
(:map org-mode-map
("C-c i" . 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))
#+end_src
*** org-roam-exclude-org-fc
Do not treat [[*org-fc][org-fc]] cards as nodes.
#+begin_src emacs-lisp :noweb-ref org-roam-exclude-org-fc :tangle no
(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)))))
#+end_src
*** org-roam-slip-boxes
Slip boxes are basically different directories that I split notes into.
#+begin_src emacs-lisp :noweb-ref org-roam-slip-boxes :tangle no
(defconst rasen/slip-boxes
'(;; Default slip-box with permanent notes
("d" "default" "" "${rasen/capture-tsid}")
;; duplicate with another key to allow \\ n n
("n" "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}")
("D" "datadog" "dd/" "${rasen/capture-tsid}")
("P" "pulse" "pulse/" "${rasen/capture-tsid}" "#+DATE: %<%FT%T%z>\n#+LAST_MODIFIED: \n")
;; Posts
("p" "posts" "posts/" "${slug}" "#+DATE: %<%Y-%m-%d>\n#+LAST_MODIFIED: \n#+DRAFT: true")
;; 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))
#+end_src
And a helper command to move notes between slip boxes.
#+begin_src emacs-lisp :noweb-ref org-roam-slip-boxes :tangle no
(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)))
#+end_src
And some extra refile helpers for dealing with inbox.
#+begin_src emacs-lisp :noweb-ref org-roam-slip-boxes :tangle no
;; 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)))))
#+end_src
*** 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=.
#+begin_src emacs-lisp :noweb-ref org-roam-node-display :tangle no
(setq org-roam-node-display-template "${hierarchy:*} ${tags:10}")
(defun org-roam-node-filetitle (node)
"Return the file TITLE for the node."
(org-roam-get-keyword "TITLE" (org-roam-node-file node)))
(defun org-roam-node-hierarchy (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)))
(defun org-roam-node-directories (node)
(if-let ((dirs (file-name-directory (file-relative-name (org-roam-node-file node) org-roam-directory))))
(string-join (f-split dirs) "/")
nil))
;; (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))
#+end_src
*** org-roam-buffer
Configure org-roam-buffer (buffer with backlinks).
Show all sections:
#+begin_src emacs-lisp :noweb-ref org-roam-buffer :tangle no
(setq org-roam-mode-section-functions
(list #'org-roam-backlinks-section
#'org-roam-reflinks-section
#'org-roam-unlinked-references-section))
#+end_src
Make it prettier—enable word-wrap and variable pitch font.
#+begin_src emacs-lisp :noweb-ref org-roam-buffer :tangle no
(add-hook 'org-roam-mode-hook #'visual-line-mode)
(add-hook 'org-roam-mode-hook #'variable-pitch-mode)
#+end_src
*** 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.
#+begin_src emacs-lisp :noweb-ref org-roam-dailies :tangle no
(require 'org-roam-dailies)
(setq org-roam-dailies-directory "life/journal/")
(defconst rasen/org-roam-daily-template
;; 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"))
(setq org-roam-dailies-capture-templates
;; %U inserts incorrect date when now and target date don't
;; match. org-timestamp-inactive is a workaround to insert
;; current time as inactive timestamp correctly.
`(("j" "journal" entry "* %(org-timestamp-inactive '(16)) %?"
:target (file+head "%<%Y-%m-%d>.org" ,rasen/org-roam-daily-template)
:empty-lines-before 1
:unnarrowed t
:clock-in t
:clock-keep nil
:clock-resume t)))
;; Try to guess "today"
(setq org-extend-today-until 6)
;; Use 23:59 timestamp when past midnight. This is usually desired so
;; that org-habit counts repetitions properly.
(setq org-use-effective-time t)
(defun rasen/org-daily-dispatch (arg)
"Dispatch command for org-roam-dailies.
With no prefix ARG, capture a note for today.
With C-u, prompt for a date and capture a note for that day.
With C-u C-u, prompt for a date and open that daily note.
Intended as a unified entry point for working with org-roam-dailies."
(interactive "P")
(cond ((eq arg nil) (org-roam-dailies-capture-today))
((equal arg '(4)) (org-roam-dailies-capture-date))
((equal arg '(16)) (org-roam-dailies-goto-date))))
#+end_src
*** 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:
#+begin_src js
javascript:location.href = 'org-protocol://roam-ref?' + new URLSearchParams({
template: 'r',
ref: location.href.split('#')[0],
title: encodeURIComponent(document.title),
body: encodeURIComponent(window.getSelection())
})
#+end_src
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.
#+begin_src emacs-lisp :noweb-ref org-roam-protocol :tangle no
(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"))))
#+end_src
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.)
#+begin_src emacs-lisp :noweb-ref org-roam-protocol :tangle no
(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"))))
#+end_src
*** org-roam-graph
Install graphviz globally:
#+begin_src nix :noweb-ref home-manager-section
{
home.packages = [ pkgs.graphviz ];
}
#+end_src
Emacs configuration:
#+begin_src emacs-lisp :noweb-ref org-roam-graph :tangle no
(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>>
#+end_src
**** org-roam-graph-exclude-node
Patch org-roam to allow a custom function to exclude nodes.
#+begin_src emacs-lisp :noweb-ref org-roam-graph-exclude-node :tangle no
(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))))
#+end_src
Exclude all links that are not nodes (non-id) as well as links from non-permanent directories.
#+begin_src emacs-lisp :noweb-ref org-roam-graph-exclude-node :tangle no
(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))))))
#+end_src
*** org-roam-kebab-slugs
Patch slug function so it uses kebab-case instead of snake-case.
#+begin_src emacs-lisp :noweb-ref org-roam-kebab-slugs :tangle no
(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)))))
#+end_src
*** org-roam-update-ids
A helper function for the next time org-id fucks up its database.
#+begin_src emacs-lisp :noweb-ref org-roam-update-ids :tangle no
(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$")))
#+end_src
*** 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.
#+begin_src emacs-lisp :noweb-ref org-roam-new-node
(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"))
(defun rasen/org-roam-new-node-pulse ()
(interactive)
(rasen/org-roam-new-node "P"))
(defun rasen/org-roam-new-node-fluxon ()
(interactive)
(rasen/org-roam-new-node "f"))
;; (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 p" #'rasen/org-roam-new-node-pulse)
;; (s-leader-def "n F" (defun rasen/org-roam-new-node-fluxon ()
;; (interactive)
;; (rasen/org-roam-new-node "f")))
#+end_src
*** org-roam-update-title
#+begin_src emacs-lisp :noweb-ref 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)))
#+end_src
** org-roam-ui
#+begin_src emacs-lisp
(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))
#+end_src
** org-ref
#+begin_src emacs-lisp
(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)
(define-key bibtex-mode-map (kbd "C-c C-c") #'org-ref-clean-bibtex-entry)
(define-key bibtex-mode-map (kbd "C-c c") #'org-ref-clean-bibtex-entry)
(define-key bibtex-mode-map (kbd "C-c s") #'bibtex-sort-buffer)
(define-key bibtex-mode-map (kbd "C-c n") #'org-ref-open-bibtex-notes)
(define-key bibtex-mode-map (kbd "C-c a") #'org-ref-bibtex-assoc-pdf-with-entry)
(define-key bibtex-mode-map (kbd "C-c f") #'org-ref-bibtex-pdf)
(define-key bibtex-mode-map (kbd "C-c o") #'org-ref-url-html-to-bibtex)
(define-key bibtex-mode-map (kbd "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)))))
#+end_src
** org-ref-cite
#+begin_src emacs-lisp
;; (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))
#+end_src
** org-roam-bibtex
Citations and bibliography tools for org-mode.
#+begin_src emacs-lisp
(use-package org-roam-bibtex
:diminish
:after org-roam
:config
(require 'org-ref)
(org-roam-bibtex-mode))
#+end_src
** 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).
Install required dependencies:
#+begin_src nix :noweb-ref home-manager-section
{
home.packages = [
pkgs.findutils
pkgs.gawk
];
}
#+end_src
#+begin_src emacs-lisp
(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-index-positions (cards)
(mapcan (lambda (card) (org-fc-index-flatten-card card)) cards))
(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)))
(rasen/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*")))
(define-key org-fc-review-flip-mode-map (kbd "RET") 'org-fc-review-flip)
(define-key org-fc-review-flip-mode-map (kbd "n") 'org-fc-review-flip)
(define-key org-fc-review-flip-mode-map (kbd "s") 'org-fc-review-suspend-card)
(define-key org-fc-review-flip-mode-map (kbd "q") 'org-fc-review-quit)
(define-key org-fc-review-rate-mode-map (kbd "a") 'org-fc-review-rate-again)
(define-key org-fc-review-rate-mode-map (kbd "h") 'org-fc-review-rate-hard)
(define-key org-fc-review-rate-mode-map (kbd "g") 'org-fc-review-rate-good)
(define-key org-fc-review-rate-mode-map (kbd "n") 'org-fc-review-rate-good)
(define-key org-fc-review-rate-mode-map (kbd "e") 'org-fc-review-rate-easy)
(define-key org-fc-review-rate-mode-map (kbd "s") 'org-fc-review-suspend-card)
(define-key org-fc-review-rate-mode-map (kbd "q") 'org-fc-review-quit)
(define-key org-fc-dashboard-mode-map (kbd "r") #'org-fc-dashboard-review)
(define-key org-fc-dashboard-mode-map (kbd "g") (defun rasen/org-fc-dashboard ()
(interactive)
(org-fc-dashboard org-fc-context-all)))
(define-key org-fc-dashboard-mode-map (kbd "h") #'rasen/org-fc-upcoming-histogram)
(define-key org-fc-dashboard-mode-map (kbd "q") #'quit-window)
<<org-fc-review-todos>>)
#+end_src
** COMMENT org-fc review todos
My modifications to support reviewing arbitrary todo lists with spaced repetition.
#+begin_src emacs-lisp :noweb-ref org-fc-review-todos
(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))
#+end_src
** toggle markup/view
#+begin_src emacs-lisp
(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))
(define-key org-mode-map (kbd "C-c \\") #'rasen/toggle-org-view-mode)
#+end_src
** Org key bindings
When editing code blocks, use current window and do not reorganize frame.
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-src-window-setup 'current-window)
#+end_src
Open file links in the same window.
#+begin_src emacs-lisp :noweb-ref org-config
(push '(file . find-file) org-link-frame-setup)
#+end_src
** 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.
#+begin_src emacs-lisp
(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"))))
#+end_src
** 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:
#+begin_src emacs-lisp :tangle no
(org-defkey map (kbd ".")
(lambda () (interactive)
;; Are we at the beginning of the prompt?
(if (looking-back "^[^:]+: "
Not written in Markdown, so it's shown here as plain text — view it formatted on GitHub.
Truncated — view the full README on GitHub.
Nix
62.1%
Emacs Lisp
24.5%
Vim Script
5.5%
YASnippet
3.5%
Python
3.4%
#+PROPERTY: header-args :tangle yes :noweb yes :results silent
#+STARTUP: overview
* Introduction
Hi there! That's my dotfiles.
This repository contains configuration for several hosts:
- omicron — my main laptop which runs NixOS
- pie — my home server RPi running NixOS
- bayraktar — work MacBook Pro managed with nix-darwin
- oleksiishmalko — another work MacBook Pro managed with nix-darwin
Most of config files are generated by [[http://orgmode.org/worg/org-contrib/babel/][org-babel]] from org files in this repository (yes, including this very same ~README.org~). That's [[https://en.wikipedia.org/wiki/Literate_programming][literate programming]] applied to dotfiles.
This file contains Emacs configuration, NixOS and home-manager configuration for omicron, and NixOS configuration for pie.
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.
#+begin_src sh :tangle no
emacs -Q README.org --batch -f org-babel-tangle
#+end_src
#+RESULTS:
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/=
* Top-level
** README.org preamble
#+begin_src emacs-lisp
;;; -*- lexical-binding: t; -*-
#+end_src
** Flake
This repository is nix flakes–compatible.
The following goes to ~flake.nix~ file.
#+begin_src nix :tangle flake.nix :padline no
#
# 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 = "nixos-26.05";
};
<<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.attrsets.recursiveUpdate {};
in mergeSections [
<<flake-outputs-nixos>>
<<flake-outputs-home-manager>>
<<flake-outputs-packages>>
<<flake-outputs-overlays>>
<<flake-outputs-nix-darwin>>
];
nixConfig = {
extra-substituters = [
<<flake-extra-substituters>>
];
extra-trusted-public-keys = [
<<flake-extra-trusted-public-keys>>
];
};
}
#+end_src
Nix flakes are still an experimental feature, so you need the following in NixOS configuration to enable it.
#+begin_src nix :noweb-ref nixos-section
{
nix = {
extraOptions = ''
experimental-features = nix-command flakes
'';
};
}
#+end_src
For nix-darwin systems:
#+begin_src nix :noweb-ref darwin-section
{
nix = {
extraOptions = ''
experimental-features = nix-command flakes
'';
};
}
#+end_src
For non-NixOS system, put the following into =~/.config/nix/nix.conf=.
#+begin_src conf :tangle no
experimental-features = nix-command flakes
#+end_src
** Unstable packages
For packages that are not available in latest stable, expose the unstable channel as ~pkgs.unstable~.
Add input:
#+begin_src nix :noweb-ref flake-inputs
nixpkgs-unstable = {
type = "github";
owner = "NixOS";
repo = "nixpkgs";
ref = "nixpkgs-unstable";
};
#+end_src
Add overlay:
#+begin_src nix :noweb-ref flake-overlays
(final: prev: {
unstable = import inputs.nixpkgs-unstable {
inherit system;
overlays = self.overlays.${system};
config = { allowUnfree = true; };
};
})
#+end_src
** NixOS
Expose NixOS configurations.
#+begin_src nix :noweb-ref flake-outputs-nixos
(let
nixosHosts = {
omicron = { system = "x86_64-linux"; config = ./nixos-config.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;
})
#+end_src
** Home manager
Add home-manager to flake inputs.
#+begin_src nix :noweb-ref flake-inputs
home-manager = {
type = "github";
owner = "rycee";
repo = "home-manager";
ref = "release-26.05";
inputs.nixpkgs.follows = "nixpkgs";
};
#+end_src
Expose home-manager configurations.
#+begin_src nix :noweb-ref flake-outputs-home-manager
(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 = { name, lib, pkgs, config, ... }: {
imports = [
<<home-manager-section>>
];
home.stateVersion = "21.05";
};
homeManagerConfigurations = genHosts homeManagerHosts mkHomeManagerConfiguration;
})
#+end_src
#+begin_src nix :noweb-ref home-manager-section
{
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.";
};
}
#+end_src
Integrate home-manager module into NixOS.
#+begin_src nix :noweb-ref nixos-section
{
imports = [inputs.home-manager.nixosModules.home-manager];
home-manager = {
useUserPackages = true;
useGlobalPkgs = true;
users.rasen = inputs.self.lib.home-manager-common;
};
}
#+end_src
Integrate home-manager into nix-darwin.
#+begin_src nix :noweb-ref darwin-section
{
imports = [inputs.home-manager.darwinModules.home-manager];
home-manager = {
useUserPackages = false;
useGlobalPkgs = true;
users."${config.system.primaryUser}" = { ... }: {
imports = [
inputs.self.lib.home-manager-common
{ hostname = config.networking.hostName; }
];
};
# users.rasen = inputs.self.lib.home-manager-common;
backupFileExtension = "bak";
};
}
#+end_src
** nix-darwin
Add nix-darwin to flake inputs.
#+begin_src nix :noweb-ref flake-inputs
darwin = {
url = "github:lnl7/nix-darwin/nix-darwin-26.05";
inputs.nixpkgs.follows = "nixpkgs";
};
#+end_src
Add helper function to differentiate linux-only config:
#+begin_src nix :noweb-ref flake-overlays
(final: prev: {
lib = prev.lib // {
linux-only = prev.lib.mkIf final.stdenv.isLinux;
};
})
#+end_src
#+begin_src nix :noweb-ref flake-outputs-nix-darwin
(let
darwinHosts = {
<<darwin-host>>
};
mkDarwinConfiguration = { name, system, systemStateVersion, homeManagerStateVersion, primaryUser ? "rasen", modules ? [] }:
inputs.darwin.lib.darwinSystem {
inherit system;
modules = modules ++ [
{
networking.hostName = name;
system.primaryUser = primaryUser;
system.stateVersion = systemStateVersion;
home-manager.sharedModules = [
({lib, ...}: {
home.stateVersion = lib.mkForce homeManagerStateVersion;
})
];
}
self.darwin-common
];
};
in {
darwin-common = { lib, pkgs, config, ... }: {
imports = [
<<darwin-section>>
];
};
darwinConfigurations = genHosts darwinHosts mkDarwinConfiguration;
})
#+end_src
#+begin_src nix :noweb-ref darwin-section
{
nixpkgs.config = {
allowUnfree = true;
};
nixpkgs.overlays = self.overlays.aarch64-darwin;
}
#+end_src
Link installed apps to ~~/Applications/Home Manager Apps~:
#+begin_src nix :noweb-ref home-manager-section
{
targets.darwin.linkApps.enable = pkgs.stdenv.hostPlatform.isDarwin;
targets.darwin.copyApps.enable = false;
}
#+end_src
** Packages
Generate packages set for each supported system.
#+begin_src nix :noweb-ref flake-outputs-packages
(let
mkPackages = system:
let
pkgs = pkgsBySystem.${system};
in
mergeSections [
<<flake-packages>>
];
in {
packages = genAttrs systems mkPackages;
})
#+end_src
** Overlays
Generate overlays for all supported systems.
#+begin_src nix :noweb-ref flake-outputs-overlays
(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;
})
#+end_src
~<<flake-overlays>>~ are defined elsewhere.
* NixOS
** General
I'm a [[http://nixos.org/][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:
#+begin_src nix :tangle nixos-config.nix :noweb no-export
#
# This file is auto-generated from "README.org"
#
{ name, config, pkgs, lib, inputs, ... }:
let
machine-config = lib.getAttr name {
omicron = [
<<machine-omicron>>
];
};
in
{
imports = [
{
# The NixOS release to be compatible with for stateful data such as databases.
# TODO: make it per-machine
system.stateVersion = "21.05";
}
<<nixos-section>>
] ++ machine-config;
}
#+end_src
This =<<nixos-section>>= is replaced by other parts of this doc.
** Re-expose nixpkgs
For NixOS:
#+begin_src nix :noweb-ref nixos-section
{
# 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;
nixpkgs-unstable.flake = inputs.nixpkgs-unstable;
};
}
#+end_src
Same configuration for nix-darwin systems.
#+begin_src nix :noweb-ref darwin-section
{
# 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;
nixpkgs-unstable.flake = inputs.nixpkgs-unstable;
};
}
#+end_src
Same but for Home Manager–managed host.
#+begin_src nix :noweb-ref home-manager-section
{
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);
}
{
from = { id = "nixpkgs-unstable"; type = "indirect"; };
to = ({
type = "path";
path = inputs.nixpkgs-unstable.outPath;
} // lib.filterAttrs
(n: v: n == "lastModified" || n == "rev" || n == "revCount" || n == "narHash")
inputs.nixpkgs);
}
];
};
}
#+end_src
** Sandbox
Build all Nix packages in sandbox:
#+begin_src nix :noweb-ref nixos-section
{
nix.settings.sandbox = true;
}
#+end_src
** Users
I'm the only user of the system (nixos-section):
#+begin_src nix :noweb-ref nixos-section
{
users.extraUsers.rasen = {
isNormalUser = true;
uid = 1000;
extraGroups = [ "users" "wheel" ];
initialPassword = "HelloWorld";
};
nix.settings.trusted-users = ["rasen"];
}
#+end_src
=initialPassword= is used only first time when user is created. It must be changed as soon as possible with =passwd=.
* macOS
** Users
#+begin_src nix :noweb-ref darwin-section
{
users.users.${config.system.primaryUser} = {
description = "Oleksii Shmalko";
home = "/Users/${config.system.primaryUser}";
};
}
#+end_src
** Some defaults
#+begin_src nix :noweb-ref darwin-section
{
system.defaults = {
NSGlobalDomain = {
# auto switch light/dark mode
AppleInterfaceStyleSwitchesAutomatically = true;
# disable automatic capitalization
NSAutomaticCapitalizationEnabled = false;
# Use F1, F2, etc keys by default (without needing to press fn)
"com.apple.keyboard.fnState" = true;
# disable beeps
"com.apple.sound.beep.volume" = 0.0;
# Disable natural scrolling
"com.apple.swipescrolldirection" = false;
};
dock = {
# Auto-hide dock
autohide = true;
};
finder = {
# Show extensions in finder
AppleShowAllExtensions = true;
# Prefer list view
FXPreferredViewStyle = "Nlsv";
# Open home directory by default
NewWindowTarget = "Home";
# Show pathbar
ShowPathbar = true;
};
# Disable default fn keyboard layout switch. I have my own with Karabiner
hitoolbox.AppleFnUsageType = "Do Nothing";
# Show seconds in clock
menuExtraClock.ShowSeconds = true;
};
system.keyboard = {
enableKeyMapping = true;
# Move tilde back on non-US keyboards
nonUS.remapTilde = true;
# Remap caps lock to control
remapCapsLockToControl = true;
};
}
#+end_src
** xbar integration function
#+begin_src emacs-lisp
(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))))))
#+end_src
org-clock.1s.sh
#+begin_src sh
#!/usr/bin/env bash
~/.nix-profile/bin/emacs --batch --eval "(progn (require 'server) (princ (server-eval-at \"server\" '(rasen/xbar \"$0\"${1:+" '$1"}))))"
#+end_src
** Yabai integration
#+begin_src nix :noweb-ref darwin-section
{
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
'';
};
}
#+end_src
Skhd integration to move between windows.
https://gist.github.com/ethan-leba/760054f36a2f7c144c6b06ab6458fae6
#+begin_src nix :noweb-ref darwin-section
{
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 - e ~
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 - n : yabai -m window --ratio rel:-0.05
# default < lcmd + ctrl - o : yabai -m window --ratio rel:+0.05
default < lcmd + shift - n : yabai -m window --swap west
default < lcmd + shift - u : yabai -m window --swap north
default < lcmd + shift - e : yabai -m window --swap south
default < lcmd + shift - o : yabai -m window --swap east
default < lcmd - n [
,* : yabai -m window --focus west
"Emacs" ~
]
default < lcmd - u [
,* : yabai -m window --focus north
"Emacs" ~
]
default < lcmd - e [
,* : yabai -m window --focus south
"Emacs" ~
]
default < lcmd - o [
,* : yabai -m window --focus east
"Emacs" ~
]
'';
};
}
#+end_src
Emacs integration for yabai:
#+begin_src emacs-lisp
(defun yabai-move-on-error (direction move-fn)
"Execute `move-fn'. If that function errors, move yabai focus in the specified direction."
(interactive)
(condition-case nil
(funcall move-fn)
(user-error (rasen/yabai-move direction))))
(defun rasen/yabai-move (direction)
"Move yabai focus in the specified `direction'."
(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))
#+end_src
** Karabiner configuration
Configure Karabiner-Elements for advanced keyboard customization.
#+begin_src nix :noweb-ref home-manager-section
{
xdg.configFile."karabiner/karabiner.json".text = builtins.toJSON {
global.show_in_menu_bar = false;
profiles = [
{
name = "Default profile";
selected = true;
virtual_hid_keyboard = {
country_code = 0;
keyboard_type_v2 = "ansi";
};
complex_modifications.rules = [
{
description = "S-<return> to Terminal";
manipulators = [
{
type = "basic";
conditions = [
{
type = "frontmost_application_unless";
bundle_identifiers = ["org\\.gnu\\.Emacs"];
file_paths = ["/emacs$"];
}
];
from = {
key_code = "return_or_enter";
modifiers.mandatory = ["left_gui"];
};
to = [{ shell_command = "cd ~; /Users/rasen/.nix-profile/bin/emacsclient -c -e '(progn (vterm) (set-window-dedicated-p nil t))'"; }];
}
];
}
{
description = "remap s-C-j/l to S-C-left/right for space switching";
manipulators = [
{
type = "basic";
from = {
key_code = "j";
modifiers.mandatory = ["left_gui" "left_control"];
};
to = {
key_code = "left_arrow";
modifiers = ["left_gui" "left_control"];
};
}
{
type = "basic";
from = {
key_code = "l";
modifiers.mandatory = ["left_gui" "left_control"];
};
to = {
key_code = "right_arrow";
modifiers = ["left_gui" "left_control"];
};
}
];
}
{
description = "ctrl to switch keyboard layout";
manipulators = [
{
type = "basic";
conditions = [{ type = "input_source_unless"; input_sources = [{ language = "en"; }]; }];
from.key_code = "left_control";
to = [{ select_input_source.input_source_id = "org.sil.ukelele.keyboardlayout.workman.workman"; }];
}
{
type = "basic";
conditions = [{ type = "input_source_if"; input_sources = [{ language = "en"; }]; }];
from.key_code = "left_control";
to = [{ select_input_source.language = "^uk$"; }];
}
];
}
{
description = "fn key to switch keyboard layout";
manipulators = [
{
type = "basic";
conditions = [{ type = "input_source_unless"; input_sources = [{ language = "en"; }]; }];
from.apple_vendor_top_case_key_code = "keyboard_fn";
to = [{ apple_vendor_keyboard_key_code = "function"; }];
to_if_alone = [{ select_input_source.input_source_id = "org.sil.ukelele.keyboardlayout.workman.workman"; }];
parameters.basic.to_if_alone_timeout_milliseconds = 200;
}
{
type = "basic";
conditions = [{ type = "input_source_if"; input_sources = [{ language = "en"; }]; }];
from.apple_vendor_top_case_key_code = "keyboard_fn";
to = [{ apple_vendor_keyboard_key_code = "function"; }];
to_if_alone = [{ select_input_source.language = "^uk$"; }];
parameters.basic.to_if_alone_timeout_milliseconds = 200;
}
];
}
{
description = "Post escape if caps_lock is tapped";
manipulators = [
{
type = "basic";
from = {
key_code = "caps_lock";
modifiers.optional = ["any"];
};
parameters = {
basic = {
to_if_alone_timeout_milliseconds = 200;
to_if_held_down_threshold_milliseconds = 200;
};
};
to = [{ key_code = "left_control"; }];
to_if_alone = [{ key_code = "escape"; }];
to_if_held_down = [{ key_code = "left_control"; }];
}
];
}
];
}
];
};
}
#+end_src
** defaults bindings
Override keybindings in settings:
#+begin_src nix :noweb-ref darwin-section
{
system.defaults.CustomUserPreferences = {
"com.apple.symbolichotkeys" = {
AppleSymbolicHotKeys = let
disabled = { enabled = false; };
modifier = enabled: code: if enabled then code else 0;
enabled =
{ shift ? false
, control ? false
, option ? false
, command ? false
, function ? false
# Keycode from
# /System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h
, key
# ASCII code of the key
, ascii ? 65535
}:
let
modifiers = lib.foldl' builtins.bitOr 0 [
(modifier shift (lib.fromHexString "0x020000"))
(modifier control (lib.fromHexString "0x040000"))
(modifier option (lib.fromHexString "0x080000"))
(modifier command (lib.fromHexString "0x100000"))
(modifier function (lib.fromHexString "0x800000"))
];
in {
enabled = true;
value = {
parameters = [ascii key modifiers];
type = "standard";
};
};
in {
# Move left a space
"79" = enabled {
command = true;
control = true;
key = 123; # left arrow
};
# Move right a space
"81" = enabled {
command = true;
control = true;
key = 124; # right arrow
};
# quick note
"190" = disabled;
# Cmd-P -> Show Launchpad
"160" = enabled {
command = true;
key = 31; # o
ascii = 112; # p
};
# Show spotlight search
"64" = disabled;
};
};
};
system.activationScripts.activateHotkeys = {
enable = true;
text = ''
/System/Library/PrivateFrameworks/SystemAdministration.framework/Resources/activateSettings -u
'';
};
}
#+end_src
** Linux builder
#+begin_src nix :noweb-ref darwin-section
{
nix.settings.trusted-users = [ "@admin" ];
nix.linux-builder = {
enable = true;
config.virtualisation.cores = 8;
config.virtualisation.darwin-builder = {
diskSize = 40 * 1024; # bump from 20Gb
memorySize = 8 * 1024;
};
};
launchd.daemons.linux-builder = {
serviceConfig = {
StandardOutPath = "/var/log/linux-builder.log";
StandardErrorPath = "/var/log/linux-builder.log";
};
};
}
#+end_src
* Machines
** omicron
This is my small Dell XPS 13 running NixOS.
#+begin_src nix :noweb-ref machine-omicron
{
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.graphics = {
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;
}
#+end_src
~inputs.nixos-hardware~ comes from the following flake input.
#+begin_src nix :noweb-ref flake-inputs
nixos-hardware = {
type = "github";
owner = "NixOS";
repo = "nixos-hardware";
};
#+end_src
LVM on LUKS setup for disk encryption.
#+begin_src nix :noweb-ref machine-omicron
{
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"; }
];
}
#+end_src
Clickpad:
#+begin_src nix :noweb-ref machine-omicron
{
services.libinput = {
enable = true;
touchpad.accelSpeed = "0.7";
};
}
#+end_src
Fix screen tearing ([[https://wiki.archlinux.org/index.php/intel_graphics#Tearing][ArchWiki]]):
#+begin_src nix :noweb-ref machine-omicron
{
services.xserver.config = ''
Section "Device"
Identifier "Intel Graphics"
Driver "intel"
Option "TearFree" "true"
Option "TripleBuffer" "true"
EndSection
'';
}
#+end_src
** bayraktar
bayraktar is macbook pro managed with nix-darwin.
#+begin_src nix :noweb-ref darwin-host
bayraktar = {
system = "aarch64-darwin";
systemStateVersion = 4;
homeManagerStateVersion = "21.05";
modules = [
({ lib, ... }: {
home-manager.users."rasen" = {
programs.git.settings.user.email = lib.mkForce "oleksii@fluxon.com";
};
})
];
};
#+end_src
*** Work email
#+begin_src nix :noweb-ref home-manager-section
{
accounts.email.accounts = lib.mkIf (config.hostname == "bayraktar") (lib.mkForce {
fluxon = {
realName = "Oleksii Shmalko";
address = "oleksii@fluxon.com";
flavor = "gmail.com";
primary = true;
passwordCommand = "pass fluxon/google.com/as@fluxon.com/email";
maildir.path = "fluxon";
msmtp.enable = true;
notmuch.enable = true;
mbsync.enable = true;
mbsync.create = "maildir";
};
});
programs.mbsync.extraConfig = lib.mkForce "";
programs.notmuch.extraConfig = {
index."header.Sender" = "Sender";
};
}
#+end_src
** oleksiishmalko
Another work macbook pro.
#+begin_src nix :noweb-ref darwin-host
oleksiishmalko = {
system = "aarch64-darwin";
primaryUser = "oleksii.shmalko";
systemStateVersion = 6;
homeManagerStateVersion = "25.05";
modules = [
({ lib, ... }: {
home-manager.users."oleksii.shmalko" = {
programs.git.settings.user.email = lib.mkForce "oleksii.shmalko@datadoghq.com";
programs.git.settings.github.user = lib.mkForce "dd-oleksii";
};
})
];
};
#+end_src
** pie
Pie is a Raspberry Pi machine.
Build:
#+begin_src sh
nix run nixpkgs#nixos-rebuild -- build --flake path:.#pie
#+end_src
Deploy:
#+begin_src sh
nix run nixpkgs#nixos-rebuild -- switch --flake path:.#pie --target-host pie.local --use-remote-sudo --fast
#+end_src
Add nixos-raspberrypi input:
#+begin_src nix :noweb-ref flake-inputs
nixos-raspberrypi.url = "github:nvmd/nixos-raspberrypi/main";
#+end_src
Add configuration:
#+begin_src nix :noweb-ref flake-outputs-nixos
{
nixosConfigurations = {
pie = inputs.nixos-raspberrypi.lib.nixosSystem {
specialArgs = {
inherit inputs;
inherit (inputs) nixos-raspberrypi;
# Semi-secret: don't want to publish these values but it'll
# end up in nix store anyway.
sensitive = import ./secrets/sensitive.nix;
};
system = "aarch64-linux";
modules = [
inputs.agenix.nixosModules.default
inputs.disko.nixosModules.disko
inputs.nixos-raspberrypi.nixosModules.raspberry-pi-3.base
({ pkgs, lib, config, inputs, sensitive, ... }: {
imports = [
<<pie>>
];
})
];
};
};
}
#+end_src
*** General
General stuff first.
#+begin_src nix :noweb-ref pie
{
networking.hostName = "pie";
system.stateVersion = "25.11";
environment.systemPackages = [pkgs.libraspberrypi];
}
#+end_src
*** Flake support
#+begin_src nix :noweb-ref pie
{
nix = {
settings.trusted-users = ["@wheel"];
extraOptions = ''
experimental-features = nix-command flakes
'';
};
}
#+end_src
*** Bootloader
Configure bootloader via nixos-raspberrypi.
#+begin_src nix :noweb-ref pie
{
boot.loader.raspberry-pi.bootloader = "kernel";
}
#+end_src
*** Remote access
#+begin_src nix :noweb-ref pie
{
services.openssh = {
enable = true;
settings = {
PasswordAuthentication = false;
PermitRootLogin = "no";
};
};
users.mutableUsers = false;
users.users.rasen = {
isNormalUser = true;
home = "/home/rasen";
createHome = true;
openssh.authorizedKeys.keys = [
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDHH15uiQw3jBbrdlcRb8wOr8KVltuwbHP/JOFAzXFO1l/4QxnKs6Nno939ugULM7Lu0Vx5g6FreuCOa2NMWk5rcjIwOzjrZnHZ7aoAVnE7H9scuz8NGnrWdc1Oq0hmcDxdZrdKdB6CPG/diGWNZy77nLvz5JcX1kPLZENPeApCERwR5SvLecA4Es5JORHz9ssEcf8I7VFpAebfQYDu+VZZvEu03P2+5SXv8+5zjiuxM7qxzqRmv0U8eftii9xgVNC7FaoRBhhM7yKkpbnqX7IeSU3WeVcw4+d1d8b9wD/sFOyGc1xAcvafLaGdgeCQGU729DupRRJokpw6bBRQGH29 rasen@omicron"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIulco7Hi0uJ90GLwuDvgG/Xlv/i2R4ySa5+/dOYygsr rasen@bayraktar"
];
extraGroups = [ "wheel" ];
};
security.sudo.wheelNeedsPassword = false;
services.avahi = {
enable = true;
nssmdns4 = true;
publish = {
enable = true;
addresses = true;
userServices = true;
};
};
}
#+end_src
*** Netdata
#+begin_src nix :noweb-ref pie
{
nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [
"netdata"
];
services.netdata = {
enable = true;
package = pkgs.netdata.override {
withCloudUi = true;
};
};
networking.firewall.allowedTCPPorts = [ 19999 ];
}
#+end_src
*** Gitolite
#+begin_src nix :noweb-ref pie
{
services.gitolite = {
enable = true;
enableGitAnnex = true;
user = "git";
adminPubkey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIulco7Hi0uJ90GLwuDvgG/Xlv/i2R4ySa5+/dOYygsr rasen@bayraktar";
dataDir = "/media/data/gitolite";
};
}
#+end_src
*** Syncthing
#+begin_src nix :noweb-ref pie
{
services.syncthing = {
enable = true;
dataDir = "/media/data/syncthing";
openDefaultPorts = true;
settings = {
devices = {
bayraktar = {
id = sensitive.syncthing.bayraktar;
autoAcceptFolders = true;
};
bugdroid = {
id = sensitive.syncthing.bugdroid;
autoAcceptFolders = true;
};
lapdog = {
id = sensitive.syncthing.lapdog;
autoAcceptFolders = true;
};
omicron = {
id = sensitive.syncthing.omicron;
autoAcceptFolders = true;
};
};
};
};
}
#+end_src
*** Tools
#+begin_src nix :noweb-ref pie
{
programs.mosh.enable = true;
environment.systemPackages = [
pkgs.vim
pkgs.psmisc
];
}
#+end_src
*** Disk configuration
#+begin_src nix :noweb-ref pie
{
# This was manually transformed from the sd installer image.
disko.devices.disk.sd = {
type = "disk";
device = "/dev/mmcblk0";
content = {
type = "gpt";
partitions = {
firmware = {
name = "FIRMWARE";
size = "1024M";
type = "EF02";
content = {
type = "filesystem";
device = "/dev/mmcblk0p1";
format = "vfat";
mountpoint = "/boot/firmware";
mountOptions = [
"noatime"
"noauto"
"x-systemd.automount"
"x-systemd.idle-timeout=1min"
];
};
};
luks = {
size = "100%";
content = {
type = "luks";
device = "/dev/mmcblk0p3";
name = "root";
extraFormatArgs = [
# RPi does not have hardware-accelerated AES, so chacha
# + adiantum provide better performance
"--cipher=xchacha12,aes-adiantum-plain64"
# adiantum needs larger sector size to be efficient
"--sector-size=4096"
];
askPassword = true;
content = {
type = "btrfs";
extraArgs = ["-f"]; # override existing partition
postCreateHook = let
thisBtrfs = config.disko.devices.disk.sd.content.partitions.root.content.content;
device = thisBtrfs.device;
subvolumes = thisBtrfs.subvolumes;
makeBlankSnapshot = btrfsMntPoint: subvol: let
subvolAbsPath = lib.strings.normalizePath "${btrfsMntPoint}/${subvol.name}";
dst = "${subvolAbsPath}-blank";
# NOTE: this one-liner has the same functionality (inspired by zfs hook)
# btrfs subvolume list -s mnt/rootfs | grep -E ' rootfs-blank$' || btrfs subvolume snapshot -r mnt/rootfs mnt/rootfs-blank
in ''
if ! btrfs subvolume show "${dst}" > /dev/null 2>&1; then
btrfs subvolume snapshot -r "${subvolAbsPath}" "${dst}"
fi
'';
# Mount top-level subvolume (/) with "subvol=/", without it
# the default subvolume will be mounted. They're the same in
# this case, though. So "subvol=/" isn't really necessary
in ''
MNTPOINT=$(mktemp -d)
mount ${device} "$MNTPOINT" -o subvol=/
trap 'umount $MNTPOINT; rm -rf $MNTPOINT' EXIT
${makeBlankSnapshot "$MNTPOINT" subvolumes."@root"}
'';
subvolumes = {
"@root" = {
mountpoint = "/";
mountOptions = ["compress=zstd" "noatime"];
};
"@nix" = {
mountpoint = "/nix";
mountOptions = ["compress=zstd" "noatime"];
};
"@persist" = {
mountpoint = "/persist";
mountOptions = ["compress=zstd" "noatime"];
};
"@log" = {
mountpoint = "/var/log";
mountOptions = ["compress=zstd" "noatime"];
};
};
};
};
};
};
};
};
disko.devices.disk.main = {
type = "disk";
device = "/dev/sda";
content = {
type = "gpt";
partitions = {
luks = {
size = "100%";
content = {
type = "luks";
name = "main";
initrdUnlock = false;
extraFormatArgs = [
# RPi does not have hardware-accelerated AES, so chacha
# + adiantum provide better performance
"--cipher=xchacha12,aes-adiantum-plain64"
# adiantum needs larger sector size to be efficient
"--sector-size=4096"
];
askPassword = true;
settings.allowDiscards = true;
content = {
type = "btrfs";
extraArgs = ["-f"]; # override existing partition
subvolumes = {
"@data" = {
mountpoint = "/media/data";
mountOptions = [
"ssd"
"compress=zstd"
"noatime"
"noauto" # do not mount on boot
"nofail"
"x-systemd.automount" # mount when needed
"x-systemd.device-timeout=1ms" # don’t wait for device to appear if it’s not there
"x-systemd.idle-timeout=5m" # unmount after 5 mins of inactivity
];
};
"@tmp" = {
mountpoint = "/media/tmp";
mountOptions = [
"ssd"
"compress=zstd"
"noatime"
"noauto" # do not mount on boot
"nofail"
"x-systemd.automount" # mount when needed
"x-systemd.device-timeout=1ms" # don’t wait for device to appear if it’s not there
"x-systemd.idle-timeout=5m" # unmount after 5 mins of inactivity
];
};
};
};
};
};
};
};
};
environment.etc.crypttab = {
mode = "0600";
text = ''
main /dev/disk/by-partlabel/disk-main-luks none luks,discard,noauto,nofail,x-systemd.device-timeout=1ms
'';
};
fileSystems."/".neededForBoot = true;
fileSystems."/nix".neededForBoot = true;
fileSystems."/persist".neededForBoot = true;
fileSystems."/var/log".neededForBoot = true;
swapDevices = [{
device = "/media/tmp/swapfile";
size = 16*1024;
}];
boot.kernel.sysctl = {
"vm.swappiness" = 160;
};
# TODO: dedup https://wiki.nixos.org/wiki/Btrfs#Deduplication
services.btrfs.autoScrub.enable = true;
}
#+end_src
*** Backups
#+begin_src nix :noweb-ref pie
{
services.snapper.configs = {
# TODO: make root ephemeral and snapshot /persist instead
root = {
SUBVOLUME = "/";
TIMELINE_CREATE = true;
TIMELINE_CLEANUP = true;
};
data = {
SUBVOLUME = "/media/data/";
NUMBER_CLEANUP = true;
NUMBER_LIMIT = 10;
NUMBER_LIMIT_IMPORTANT = 10;
TIMELINE_CREATE = true;
TIMELINE_CLEANUP = true;
TIMELINE_LIMIT_MONTHLY = 6;
TIMELINE_LIMIT_YEARLY = 2;
};
};
age.secrets.restic-password.file = ./secrets/pie-restic-password.age;
age.secrets.restic-sa-key.file = ./secrets/pie-restic-sa-key.json.age;
services.restic.backups.data = {
paths = ["/media/data/"];
exclude = ["/media/data/.snapshots/"];
repository = sensitive.backups.repository;
passwordFile = config.age.secrets.restic-password.path;
environmentFile = toString (pkgs.writeText "restic-env" ''
GOOGLE_PROJECT_ID=${sensitive.backups.googleProjectId}
GOOGLE_APPLICATION_CREDENTIALS=${config.age.secrets.restic-sa-key.path}
RESTIC_KEY_HINT=${sensitive.backups.resticKeyHint}
RESTIC_READ_CONCURRENCY=4
RESTIC_CACHE_DIR=/media/tmp/restic/cache
TMPDIR=/media/tmp/restic/tmp
'');
progressFps = 0.1; # every 10 seconds
extraBackupArgs = [
"--skip-if-unchanged"
"--one-file-system"
];
timerConfig.OnCalendar = "hourly";
pruneOpts = [
"--keep-hourly 24"
"--keep-daily 7"
"--keep-weekly 4"
"--keep-monthly 12"
"--keep-yearly 80"
];
runCheck = true;
checkOpts = [
"--read-data-subset=50M"
];
# Remount snapshot in place of /media/data. We’re doing this with
# PrivateMounts, so it does not affect the main system.
backupPrepareCommand = ''
set -euxo pipefail
umount /media/data
SNAPSHOT_NUMBER=$(snapper -c data create --description 'restic backup' --read-only --cleanup-algorithm number --print-number)
mount /dev/mapper/main /media/data -o ro,subvol=@data/.snapshots/$SNAPSHOT_NUMBER/snapshot
'';
backupCleanupCommand = ''
umount /media/data
snapper -c data cleanup number
'';
};
systemd.services.restic-backups-data.path = [pkgs.util-linux pkgs.snapper];
systemd.services.restic-backups-data.serviceConfig = {
PrivateMounts = true;
Nice = 19;
IOSchedulingPriority = 7; # lowest
};
}
#+end_src
*** Binary cache
Substituters:
#+begin_src nix :noweb-ref flake-extra-substituters
"https://nixos-raspberrypi.cachix.org"
#+end_src
Trusted public keys:
#+begin_src nix :noweb-ref flake-extra-trusted-public-keys
"nixos-raspberrypi.cachix.org-1:4iMO9LXa8BqhU+Rpg6LQKiGa2lsNh/j2oiYLNOQ5sPI="
#+end_src
*** agenix
Flake inputs:
#+begin_src nix :noweb-ref flake-inputs
agenix = {
url = "github:ryantm/agenix";
inputs.nixpkgs.follows = "nixpkgs";
inputs.home-manager.follows = "home-manager";
inputs.darwin.follows = "darwin";
};
#+end_src
Secrets (tangled to secrets.nix):
#+begin_src nix :tangle ./secrets.nix :noweb no-export
# Auto-generated from README.org
let
systems = {
pie = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII6VmzumaVf0ELAIzh2Zmq2sjEv2h99Co6lXB5BaN8/y";
};
users = {
yubikey1 = "age1yubikey1qvqpv4f2kvwxzdf4rm349za00jayxhk23ads42l92sjywlcwce20yhm6pm2";
bayraktar = "age1kmlska3klt5ca98chxm0xu2q2rc5x2c3agkut6303ajg35rreqes7qg3yd";
};
in {
"secrets/pie-restic-sa-key.json.age".publicKeys = [
systems.pie
];
"secrets/pie-restic--password.age".publicKeys = [
systems.pie
];
}
#+end_src
*** disko
Flake inputs:
#+begin_src nix :noweb-ref flake-inputs
disko = {
url = "github:nix-community/disko/latest";
inputs.nixpkgs.follows = "nixpkgs";
};
#+end_src
*** impermanence
Flake inputs:
#+begin_src nix :noweb-ref flake-inputs
impermanence = {
url = "github:nix-community/impermanence";
inputs.nixpkgs.follows = "";
inputs.home-manager.follows = "";
};
#+end_src
*** Remote unlock
#+begin_src nix :noweb-ref pie
{
boot.initrd = {
enable = true;
availableKernelModules = [ "r8169" ];
luks.forceLuksSupportInInitrd = true;
luks.cryptoModules = lib.mkDefault (lib.mkAfter [
"aes"
"aes_generic"
"blowfish"
"twofish"
"serpent"
"cbc"
"xts"
"lrw"
"sha1"
"sha256"
"sha512"
"af_alg"
"algif_skcipher"
"cryptd"
"input_leds"
# extra:
"xchacha20"
"xchacha12" "adiantum" "nhpoly1305"
]);
network = {
enable = true;
udhcpc.enable = true;
flushBeforeStage2 = true;
ssh = {
enable = true;
port = 2222;
hostKeys = [./secrets/ssh_host_pie_ed25519_key];
# hostKeys = ["/boot/initrd/sshhost_ed25519_key"];
# ignoreEmptyHostKeys = true; # we’re going to deliver it out of bound
# extraConfig = ''
# HostKey /boot/ssh_host_ed25519_key
# '';
authorizedKeys = [
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDHH15uiQw3jBbrdlcRb8wOr8KVltuwbHP/JOFAzXFO1l/4QxnKs6Nno939ugULM7Lu0Vx5g6FreuCOa2NMWk5rcjIwOzjrZnHZ7aoAVnE7H9scuz8NGnrWdc1Oq0hmcDxdZrdKdB6CPG/diGWNZy77nLvz5JcX1kPLZENPeApCERwR5SvLecA4Es5JORHz9ssEcf8I7VFpAebfQYDu+VZZvEu03P2+5SXv8+5zjiuxM7qxzqRmv0U8eftii9xgVNC7FaoRBhhM7yKkpbnqX7IeSU3WeVcw4+d1d8b9wD/sFOyGc1xAcvafLaGdgeCQGU729DupRRJokpw6bBRQGH29 rasen@omicron"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIulco7Hi0uJ90GLwuDvgG/Xlv/i2R4ySa5+/dOYygsr rasen@bayraktar"
];
};
postCommands = ''
echo "cryptsetup-askpass && exit 0" >> /root/.profile
'';
};
};
}
#+end_src
* Emacs
** Install Emacs
I use emacs from [[https://github.com/nix-community/emacs-overlay][emacs-overlay]].
#+begin_src nix :noweb-ref flake-inputs
emacs-overlay = {
type = "github";
owner = "nix-community";
repo = "emacs-overlay";
inputs.nixpkgs.follows = "";
inputs.nixpkgs-stable.follows = "";
};
#+end_src
Use overlay (~<<flake-overlays>>~).
#+begin_src nix :noweb-ref flake-overlays
inputs.emacs-overlay.overlay
#+end_src
Expose Emacs with my packages as a top-level package (~<<flake-packages>>~).
#+begin_src nix :noweb-ref flake-packages
(let
homebrew-emacs-plus = "https://raw.githubusercontent.com/d12frosted/homebrew-emacs-plus/8da4d9bee657e392aab493b0281b91edd9d9196e";
emacs-base =
if pkgs.stdenv.isDarwin
then pkgs.unstable.emacs31.overrideAttrs (old: {
patches =
(old.patches or [])
++ [
(pkgs.fetchpatch {
url = "${homebrew-emacs-plus}/patches/emacs-31/fix-ns-x-colors.patch";
sha256 = "sha256-oe3DFgEXwp0cZJl+ufWqTonaeWSliikTRsVDNbcy4Yw=";
})
# Make emacs aware of OS-level light/dark mode
(pkgs.fetchpatch {
url = "${homebrew-emacs-plus}/patches/emacs-31/system-appearance.patch";
sha256 = "sha256-4+2U+4+2tpuaThNJfZOjy1JPnneGcsoge9r+WpgNDko=";
})
# Enable rounded window with no decoration
(pkgs.fetchpatch {
url = "${homebrew-emacs-plus}/patches/emacs-31/round-undecorated-frame.patch";
sha256 = "sha256-KCMEvJzN1OkwFYoMLpZghvdeoO1Ckcxk3Mo19YAf850=";
})
(pkgs.fetchpatch {
url = "${homebrew-emacs-plus}/patches/emacs-31/fix-ns-scroll-crash.patch";
sha256 = "sha256-syC9un5Vy1+bmBWIc+TEwTCM/nfPIxd4IhWYdEfP4qE=";
})
];
})
else pkgs.unstable.emacs31.override {
withX = true;
toolkit = "lucid";
withGTK3 = false;
};
emacs-packages = (epkgs:
(with epkgs; [
<<emacs-packages>>
]) ++
(with epkgs.melpaPackages; [
activity-watch-mode
aggressive-indent
atomic-chrome
avy
bash-completion
clojure-mode
cmake-mode
dart-mode
diminish
dockerfile-mode
doom-modeline
edit-indirect
el-patch
exec-path-from-shell
expand-region
fish-completion
fish-mode
flycheck
forge
forth-mode
just-mode
justl
go-mode
gptel
graphviz-dot-mode
haskell-mode
jinja2-mode
json-mode
ledger-mode
lua-mode
markdown-mode
nix-mode
notmuch
ol-notmuch
org-cliplink
org-download
org-ref
org-roam
org-roam-bibtex
org-super-agenda
pass
pip-requirements
plantuml-mode
protobuf-mode
racket-mode
restclient
rust-mode
svelte-mode
swift-mode
terraform-mode
visual-fill-column
vue-mode
wgrep
whitespace-cleanup-mode
yaml-mode
yasnippet
zig-mode
]) ++
[
epkgs.elpaPackages.org
epkgs.nongnuPackages.org-contrib
epkgs.elpaPackages.adaptive-wrap
# not available in melpa
epkgs.elpaPackages.exwm
epkgs.elpaPackages.valign
epkgs.elpaPackages.llm
(epkgs.trivialBuild rec {
pname = "org-fc";
version = "20201121";
src = pkgs.fetchFromGitHub {
owner = "l3kn";
repo = "org-fc";
rev = "cc191458a991138bdba53328690a569b8b563502";
sha256 = "sha256-wzMSgS4iZfpKOICqQQuQYNPb2h7i4tTWsMs7mVmgBt8=";
};
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.imagemagick
pkgs.shellcheck
pkgs.direnv
(pkgs.hunspell.withDicts (dicts: with dicts; [en_US-large uk_UA]))
# 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;
};
})
#+end_src
Install Emacs with Home manager (~<<home-manager-section>>~)
#+begin_src nix :noweb-ref 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;
services.emacs = {
enable = true;
client.enable = true;
defaultEditor = true;
package = config.programs.emacs.finalPackage;
};
# fonts used by emacs
home.packages = [
pkgs.ibm-plex
pkgs.monaspace
pkgs.dejavu_fonts
pkgs.libertine
pkgs.roboto-mono
pkgs.mononoki
pkgs.iosevka
];
}
#+end_src
** 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 [[https://orgmode.org/worg/org-contrib/babel/intro.html#emacs-initialization][org-babel documentation]] for more info.
The following snippet is an adaptation of that idea and goes to my ~.emacs.d/init.el~.
#+begin_src emacs-lisp :tangle .emacs.d/init.el
;;; -*- lexical-binding: t; -*-
;;;
;;; 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)
(require 'el-patch)
(org-babel-load-file (expand-file-name "README.org" rasen/dotfiles-directory))
#+end_src
Another important file that needs to be tangled is [[./.emacs.d/early-init.el]]. For now, just add a header to it.
#+begin_src emacs-lisp :tangle .emacs.d/early-init.el
;;; -*- lexical-binding: t; -*-
;;;
;;; This file is auto-generated from "README.org"
;;;
#+end_src
** GC hacks
Suppress GC in early init and restore it after init is complete. (~.emacs.d/early-init.el~)
#+begin_src emacs-lisp :tangle .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)))
#+end_src
** use-package
[[https://github.com/jwiegley/use-package][use-package]] is a cool emacs library that helps managing emacs configuration making it simpler and more structured. (emacs-lisp)
#+begin_src 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)
#+end_src
** package
All emacs packages are installed with Nix. (See <<install-emacs>>.)
Disable usage of emacs internal archives. (~.emacs.d/early-init.el~)
#+begin_src emacs-lisp :tangle .emacs.d/early-init.el
(require 'package)
(setq package-archives nil)
(setq package-enable-at-startup nil)
#+end_src
** Hard way
Hard way: prohibit usage of keybindings I have more efficient bindings for.
#+begin_src emacs-lisp
(defmacro rasen/hard-way (key)
"Prohibit usage of a keybinding and redirect to use `key' instead."
`(lambda () (interactive) (error "Don't use this key! Use %s instead" ,key)))
#+end_src
** Don't clutter system
Save custom configuration in the =~/.emacs.d/custom.el= file so emacs does not clutter =init.el=.
#+begin_src emacs-lisp
(setq custom-file (expand-file-name "custom.el" user-emacs-directory))
(load custom-file t)
#+end_src
Don't clutter the current directory with backups. Save them in a separate directory.
#+begin_src emacs-lisp
(setq backup-directory-alist '(("." . "~/.emacs.d/backups")))
#+end_src
Don't clutter the current directory with auto-save files.
#+begin_src emacs-lisp
(setq auto-save-file-name-transforms '((".*" "~/.emacs.d/backups/" t)))
#+end_src
Do not create lockfiles either. (I am the only user in the system and only use emacs through daemon, so that should be ok.)
#+begin_src emacs-lisp
(setq create-lockfiles nil)
#+end_src
Store native compilation cache inside =~/.emacs.d/var/eln-cache=. This goes to early init:
#+begin_src emacs-lisp :tangle .emacs.d/early-init.el
(when (and (fboundp 'startup-redirect-eln-cache)
(fboundp 'native-comp-available-p)
(native-comp-available-p))
(startup-redirect-eln-cache
(convert-standard-filename
(expand-file-name "var/eln-cache/" user-emacs-directory))))
#+end_src
** Helpers
Emacs lisp helper functions.
Timestamp-ids are used to uniquely identify things.
#+begin_src emacs-lisp
(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)))
#+end_src
Insert current date in =yyyy-mm-dd= format. Useful when creating dated notes or dumb commits.
#+begin_src emacs-lisp
(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))))
;; (define-key git-commit-mode-map (kbd "C-c .") #'rasen/insert-date)
#+end_src
#+begin_src emacs-lisp
(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)))
#+end_src
#+begin_src emacs-lisp
(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)))
#+end_src
Shamelessly stolen from https://github.com/purcell/emacs.d.
#+begin_src emacs-lisp
(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)))
#+end_src
#+begin_src emacs-lisp
(defun add-to-path (str)
"Add an STR to the PATH environment variable."
(setenv "PATH" (concat str ":" (getenv "PATH"))))
#+end_src
** avy
Jump anywhere with a few keystrokes in tree-like way.
#+begin_src emacs-lisp
(use-package avy
:custom
;; easy workman keys (excluding pinky)
(avy-keys '(?s ?h ?t ?n ?e ?o ?d ?r ?u ?p)))
#+end_src
** wgrep
Edit grep buffers and apply changes to the files.
#+begin_src emacs-lisp
(use-package wgrep)
#+end_src
** whitespace
A good mode to highlight whitespace issues (leading/trailing spaces/newlines) and too long lines.
#+begin_src emacs-lisp
(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)))
#+end_src
** whitespace-cleanup
Fix whitespaces on file save.
#+begin_src emacs-lisp
(use-package whitespace-cleanup-mode
:diminish whitespace-cleanup-mode
:config
(global-whitespace-cleanup-mode 1))
#+end_src
** Spell-check
hunspell is installed in emacs packages.
Fix Ukrainian dictionary to play nicely with English one.
#+begin_src nix :noweb-ref flake-overlays
(final: prev: {
hunspellDicts = prev.hunspellDicts // {
# uk_UA affix file strips all latin characters from input, so
# combining en_US,uk_UA stops spell-checking english words
# entirely.
#
# Remove these iconv lines, so that we can use both dictionaries
# together.
uk_UA = prev.hunspellDicts.uk_UA.overrideAttrs (old: {
postInstall = ''
sed -i 's/^ICONV 64$/ICONV 2/' $out/share/hunspell/uk_UA.aff
sed -i '/^ICONV [^ ]* 0$/d' $out/share/hunspell/uk_UA.aff
'';
});
};
})
#+end_src
Make spellcheck work with Unicode apostrophe.
#+begin_src emacs-lisp
(use-package ispell
:defer 10
:config
(setq ispell-program-name "hunspell")
(setq ispell-dictionary "en_US,uk_UA")
(ispell-set-spellchecker-params)
(ispell-hunspell-add-multi-dic "en_US,uk_UA")
(ispell-change-dictionary "en_US,uk_UA" t)
;; This needs to be overriden for some reason, even though
;; ispell-dictionary-alist has the same value.
(setq ispell-local-dictionary-alist
`(("en_US,uk_UA" "[[:alpha:]]" "[^[:alpha:]]"
"['0-9`’-]" nil ("-d" "en_US,uk_UA") nil utf-8)))
;; Don't send ’ to the subprocess.
(defun rasen/replace-apostrophe (args)
(cons (replace-regexp-in-string
"’" "'" (car args))
(cdr args)))
(advice-add #'ispell-send-string :filter-args
#'rasen/replace-apostrophe)
;; Convert ' back to ’ from the subprocess.
(defun rasen/replace-quote (args)
(if (not (derived-mode-p 'org-mode))
args
(cons (replace-regexp-in-string
"'" "’" (car args))
(cdr args))))
(advice-add #'ispell-parse-output :filter-args
#'rasen/replace-quote))
#+end_src
** tab-bar-mode
New in Emacs 27.
#+begin_src emacs-lisp
(use-package tab-bar
:config
(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)
)
#+end_src
** Highlight current line
Highlight current line.
#+begin_src emacs-lisp
(use-package emacs
:config
(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)))
#+end_src
** 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.)
#+begin_src emacs-lisp
(setq scroll-margin 3
scroll-conservatively 101)
#+end_src
** visual-fill-column
Center all text in the buffer in some modes. (That's a nice distraction-free setup.)
#+begin_src emacs-lisp
(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))
#+end_src
** Completion
#+begin_src emacs-lisp
(use-package minibuffer
:ensure nil
:bind (:map minibuffer-visible-completions-up-down-map
("C-n" . minibuffer-next-completion)
("C-p" . minibuffer-previous-completion))
:hook ((minibuffer-setup . cursor-intangible-mode))
:custom
(tab-always-indent 'complete) ; Make TAB invoke complete
(tab-first-completion 'word) ; Don't complete mid-word
(completion-auto-help t)
(completion-auto-select nil) ; Don't auto-select *Completions* buffer
(completion-eager-update t)
(completion-eager-display t) ; Display completions buffer immediately
(minibuffer-visible-completions 'up-down)
(completion-ignore-case t)
(read-file-name-completion-ignore-case t)
(read-buffer-completion-ignore-case t)
(completion-show-help nil)
(completion-styles '(partial-completion flex))
(completions-format 'one-column) ; Show completions in a single column
(completions-max-height 16)
(completions-sort 'historical)
(enable-recursive-minibuffers t)
(minibuffer-prompt-properties '(read-only t intangible t cursor-intangible t face minibuffer-prompt))
(minibuffer-depth-indicate-mode t)
(minibuffer-electric-default-mode t))
(use-package completion-preview
:ensure nil
;; :bind (:map completion-preview-active-mode
;; ("M-n" . )
;; ("M-p" . nil)
;; ("C-i" . nil))
:config
(setq completion-preview-minimum-symbol-length 2)
(global-completion-preview-mode 1))
(use-package savehist
:config
(savehist-mode t))
#+end_src
** Misc
Use single-key =y/n= instead of a more verbose =yes/no=.
#+begin_src emacs-lisp
(fset 'yes-or-no-p 'y-or-n-p)
#+end_src
Automatically add a final newline in files.
#+begin_src emacs-lisp
(setq-default require-final-newline t)
#+end_src
* Environment
** EXWM
Emacs is my Window Manager, thanks to [[https://github.com/ch11ng/exwm][EXWM]].
NixOS has an EXWM module, but my feeling is that it's too limiting. (~<<nixos-section>>~)
#+begin_src nix :noweb-ref nixos-section
{
environment.systemPackages = [ pkgs.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.displayManager.defaultSession = "none+exwm";
}
#+end_src
Initialize EXWM configuration (emacs-lisp)
#+begin_src 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)))
;; do not forward anything besides keys defined with
;; `exwm-input-set-key' and `exwm-mode-map'
(setq exwm-input-prefix-keys '())
(when (eq system-type 'gnu/linux)
(exwm-enable)))
#+end_src
Add a couple of helpers functions. (emacs-lisp)
#+begin_src 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)
(exwm-input--update-global-prefix-keys))
#+end_src
** Window management
Common key bindings. (emacs-lisp)
#+begin_src emacs-lisp
(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") #'execute-extended-command)
(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") #'switch-to-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-n") #'yabai-windmove-left)
(exwm-input-set-key (kbd "s-e") #'yabai-windmove-down)
(exwm-input-set-key (kbd "s-u") #'yabai-windmove-up)
(exwm-input-set-key (kbd "s-o") #'yabai-windmove-right)
(exwm-input-set-key (kbd "s-h") (rasen/hard-way "s-n"))
(exwm-input-set-key (kbd "s-l") (rasen/hard-way "s-o"))
(exwm-input-set-key (kbd "s-j") (rasen/hard-way "s-u"))
(exwm-input-set-key (kbd "s-k") (rasen/hard-way "s-e"))
(exwm-input-set-key (kbd "s-s") #'split-window-below)
(exwm-input-set-key (kbd "s-v") #'split-window-right)
(exwm-input-set-key (kbd "s-.") #'rasen/exwm-next-workspace)
(exwm-input-set-key (kbd "s->") #'rasen/move-tab-other-frame) ;; s-S-.
(define-prefix-command 'rasen/tab-map)
(define-key rasen/tab-map (kbd "T") #'tab-bar-mode)
(define-key rasen/tab-map (kbd "1") #'tab-bar-select-tab)
(define-key rasen/tab-map (kbd "2") #'tab-bar-select-tab)
(define-key rasen/tab-map (kbd "3") #'tab-bar-select-tab)
(define-key rasen/tab-map (kbd "4") #'tab-bar-select-tab)
(define-key rasen/tab-map (kbd "5") #'tab-bar-select-tab)
(define-key rasen/tab-map (kbd "6") #'tab-bar-select-tab)
(define-key rasen/tab-map (kbd "7") #'tab-bar-select-tab)
(define-key rasen/tab-map (kbd "8") #'tab-bar-select-tab)
(define-key rasen/tab-map (kbd "9") #'tab-bar-select-tab)
(define-key rasen/tab-map (kbd "t") #'tab-bar-new-tab)
(define-key rasen/tab-map (kbd "n") #'tab-bar-switch-to-prev-tab)
(define-key rasen/tab-map (kbd "o") #'tab-bar-switch-to-next-tab)
(define-key rasen/tab-map (kbd ">") #'tab-bar-move-tab-to-frame)
(define-key rasen/tab-map (kbd "k") #'tab-bar-close-tab)
(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>") #'ghostel)
(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)
;; (define-key exwm-mode-map (kbd "C-c") nil)
(define-key exwm-mode-map (kbd "<f1> v") #'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)
#+end_src
** Window layout
Rules to automatically layout windows when they appear.
#+begin_src emacs-lisp
(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)))))
#+end_src
** Screen locking
I use ~xss-lock~ + ~slock~ for screen locking. Actual handling is coded in Emacs.
*** Slock
[[http://tools.suckless.org/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:
#+begin_src fundamental :tangle no
slock: unable to disable OOM killer. Make sure to suid or sgid slock.
#+end_src
#+begin_src nix :noweb-ref nixos-section
{
programs.slock.enable = true;
}
#+end_src
*** xss-lock
[[https://bitbucket.org/raymonad/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.
#+begin_src nix :noweb-ref home-manager-section
{
home.packages = pkgs.lib.linux-only [
pkgs.xss-lock
];
}
#+end_src
*** EXWM integration
Autostart ~xss-lock~ (emacs-lisp).
#+begin_src emacs-lisp
(when (eq system-type 'gnu/linux)
(rasen/autostart "xss-lock -n \"xset dpms force off\" slock"))
#+end_src
Bind ~s-M-l~ to lock screen immediately.
#+begin_src emacs-lisp
(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)
#+end_src
** System tray
Use built-in EXWM system tray (emacs-lisp)
#+begin_src emacs-lisp
(use-package exwm-systemtray
:after exwm
:config
(exwm-systemtray-mode))
#+end_src
** Screenshots
I use [[https://github.com/Roger/escrotum][Escrotum]] for screenshots.
Install it. (~<<home-manager-section>>~)
#+begin_src nix :noweb-ref home-manager-section
{
home.packages = pkgs.lib.linux-only [ pkgs.escrotum ];
}
#+end_src
Bind it to Print Screen button. (emacs-lisp)
#+begin_src 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)
#+end_src
** Misc
I definitely use X server:
#+begin_src nix :noweb-ref nixos-section
{
services.xserver.enable = true;
}
#+end_src
Use English as my only supported locale:
#+begin_src nix :noweb-ref nixos-section
{
i18n.supportedLocales = [ "en_US.UTF-8/UTF-8" ];
}
#+end_src
Setup timezone:
#+begin_src nix :noweb-ref nixos-section
{
time.timeZone = "Europe/Kiev";
}
#+end_src
* Input
** Keyboard
*** Workman
I use [[https://workmanlayout.org/][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:
#+DOWNLOADED: https://nic-west.com/images/workman.png @ 2020-06-13 02:39:31
[[file:images/20200612233931-workman.png]]
*** Keyboard layout
Besides Workman, I use Ukrainian layout. I also use Russian symbols, but they are on the third level (~<<nixos-section>>~).
#+begin_src nix :noweb-ref nixos-section
{
services.xserver.xkb.layout = "us,ua";
services.xserver.xkb.variant = "workman,";
# Use same config for linux console
console.useXkbConfig = true;
}
#+end_src
Same setting but for Home Manager (~<<home-manager-section>>~)
#+begin_src nix :noweb-ref home-manager-section
{
home.keyboard = {
layout = "us,ua";
variant = "workman,";
};
}
#+end_src
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>>~:
#+begin_src nix :noweb-ref nixos-section
{
services.xserver.xkb.options = "grp:lctrl_toggle,grp_led:caps,ctrl:nocaps";
# services.xserver.xkbOptions = "grp:caps_toggle,grp_led:caps";
}
#+end_src
On macOS, right option acts as AltGr. Make Emacs ignore it, so it can work correctly as a Meta modifier:
#+begin_src emacs-lisp
(setq ns-right-alternate-modifier 'none)
#+end_src
*** 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>>~).
#+begin_src nix :noweb-ref home-manager-section
{
xsession.initExtra = ''
xkbcomp ${./Xkeymap} $DISPLAY
'';
}
#+end_src
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).
#+begin_src emacs-lisp
(when (eq system-type 'gnu-linux)
(defun rasen/set-xkb-layout ()
(interactive)
(rasen/autostart "xkbcomp ~/dotfiles/Xkeymap $DISPLAY"))
(rasen/set-xkb-layout))
#+end_src
Install xkbcomp to execute these commands. (~<<home-manager-section>>~)
#+begin_src nix :noweb-ref home-manager-section
{
home.packages = [ pkgs.xkbcomp ];
}
#+end_src
*** Compose keys
Add some custom compose keys (~<<home-manager-section>>~):
#+begin_src nix :noweb-ref 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.
'';
}
#+end_src
*** xcape
Make short press on left control behave as Escape (=<<home-manager-section>>=):
#+begin_src nix :noweb-ref home-manager-section
{
services.xcape = {
enable = pkgs.stdenv.isLinux;
mapExpression = {
Control_L = "Escape";
};
};
}
#+end_src
*** 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.
#+begin_src emacs-lisp
(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"))
#+end_src
* Network
** NetworkManager
(~<<nixos-section>>~)
#+begin_src nix :noweb-ref nixos-section
{
networking = {
hostName = name;
networkmanager = {
enable = true;
wifi.powersave = false;
};
# disable wpa_supplicant
wireless.enable = false;
};
users.extraUsers.rasen.extraGroups = [ "networkmanager" ];
}
#+end_src
Install network manager applet for user. (~<<home-manager-section>>~)
#+begin_src nix :noweb-ref home-manager-section
{
home.packages = pkgs.lib.linux-only [ pkgs.networkmanagerapplet ];
}
#+end_src
Auto-start ~nm-applet~ (emacs-lisp)
#+begin_src emacs-lisp
(rasen/autostart "nm-applet")
#+end_src
** SSH
(~<<nixos-section>>~)
#+begin_src nix :noweb-ref nixos-section
{
services.openssh = {
enable = true;
settings.PasswordAuthentication = false;
};
}
#+end_src
*** Mosh
[[https://mosh.mit.edu/][Mosh (mobile shell)]] is a cool addition to ssh.
#+begin_src nix :noweb-ref nixos-section
{
programs.mosh.enable = true;
}
#+end_src
** dnsmasq
Use [[http://www.thekelleys.org.uk/dnsmasq/doc.html][dnsmasq]] as a DNS cache.
(~<<nixos-section>>~)
#+begin_src nix :noweb-ref nixos-section
{
services.dnsmasq = {
enable = true;
settings = {
# These are used in addition to resolv.conf
server = [
"8.8.8.8"
"8.8.4.4"
];
interface = "lo";
bind-interfaces = true;
listen-address = "127.0.0.1";
cache-size = 1000;
no-negcache = true;
};
};
}
#+end_src
** Firewall
Enable firewall. This blocks all ports for ingress traffic and pings.
(~<<nixos-section>>~)
#+begin_src nix :noweb-ref nixos-section
{
networking.firewall = {
enable = true;
allowPing = false;
connectionTrackingModules = [];
autoLoadConntrackHelpers = false;
};
}
#+end_src
* Services
** Locate
Update [[https://linux.die.net/man/1/locate][locate]] database daily.
#+begin_src nix :noweb-ref nixos-section
{
services.locate = {
enable = true;
};
}
#+end_src
** Gitolite
#+begin_src nix :noweb-ref nixos-section
{
services.gitolite = {
enable = true;
user = "git";
adminPubkey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDHH15uiQw3jBbrdlcRb8wOr8KVltuwbHP/JOFAzXFO1l/4QxnKs6Nno939ugULM7Lu0Vx5g6FreuCOa2NMWk5rcjIwOzjrZnHZ7aoAVnE7H9scuz8NGnrWdc1Oq0hmcDxdZrdKdB6CPG/diGWNZy77nLvz5JcX1kPLZENPeApCERwR5SvLecA4Es5JORHz9ssEcf8I7VFpAebfQYDu+VZZvEu03P2+5SXv8+5zjiuxM7qxzqRmv0U8eftii9xgVNC7FaoRBhhM7yKkpbnqX7IeSU3WeVcw4+d1d8b9wD/sFOyGc1xAcvafLaGdgeCQGU729DupRRJokpw6bBRQGH29 rasen@omicron";
};
}
#+end_src
** Syncthing
I use Syncthing to sync my org-mode files to my phone.
#+begin_src nix :noweb-ref nixos-section
{
services.syncthing = {
enable = true;
user = "rasen";
dataDir = "/home/rasen/.config/syncthing";
configDir = "/home/rasen/.config/syncthing";
openDefaultPorts = true;
};
}
#+end_src
On Darwin:
#+begin_src nix :noweb-ref darwin-section
{
environment.systemPackages = [ pkgs.syncthing ];
}
#+end_src
** Backup
I use borg for backups.
#+begin_src nix :noweb-ref machine-omicron
(let
commonOptions = {
# repo = "borg@10.13.0.3:.";
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;
};
})
#+end_src
Mount external drive when needed.
#+begin_src nix :noweb-ref nixos-section
{
# 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
];
};
}
#+end_src
** direnv
*** direnv + nix-direnv
direnv allows having per-directory environment configuration. You can think of automatic virtualenv, but it's more general and supports unloading.
(~<<home-manager-section>>~)
#+begin_src nix :noweb-ref home-manager-section
{
programs.direnv.enable = true;
programs.direnv.nix-direnv.enable = true;
}
#+end_src
direnv integration into Emacs via [[https://github.com/purcell/envrc][purcell/envrc]]:
#+begin_src nix :noweb-ref emacs-packages
epkgs.melpaPackages.envrc
#+end_src
(emacs-lisp)
#+begin_src emacs-lisp
(use-package envrc
:after exec-path-from-shell
:hook (after-init . envrc-global-mode))
#+end_src
** VirtualBox
#+begin_src nix :noweb-ref nixos-section
{
virtualisation.virtualbox.host.enable = true;
users.extraGroups.vboxusers.members = ["rasen"];
}
#+end_src
** User-level services
Install runit to manage user-level services.
#+begin_src nix :noweb-ref home-manager-section
{
home.packages = [ pkgs.runit ];
}
#+end_src
* Hardware
** Do not suspend on AC
#+begin_src nix :noweb-ref nixos-section
{
services.logind = {
lidSwitchDocked = "ignore";
lidSwitchExternalPower = "ignore";
};
}
#+end_src
** Autorandr
Configure EXWM to use autorandr. (emacs-lisp)
#+begin_src 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-mode))
#+end_src
(~<<home-manager-section>>~)
#+begin_src nix :noweb-ref 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";
};
};
};
};
};
}
#+end_src
** Screen brightness
=xbacklight= stopped working recently. =acpilight= is a drop-in replacement.
#+begin_src nix :noweb-ref nixos-section
{
hardware.acpilight.enable = true;
environment.systemPackages = [ pkgs.acpilight ];
users.extraUsers.rasen.extraGroups = [ "video" ];
}
#+end_src
For Home Manager–managed hosts.
#+begin_src nix :noweb-ref home-manager-section
{
home.packages = pkgs.lib.linux-only [ pkgs.acpilight ];
}
#+end_src
Bind it to keys (emacs-lisp).
#+begin_src 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")))
#+end_src
** Redshift
Redshift adjusts the color temperature of the screen according to the position of the sun.
Blue light blocks [[https://en.wikipedia.org/wiki/Melatonin][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).
#+begin_src nix :noweb-ref nixos-section
{
services.redshift = {
enable = true;
};
location.provider = "geoclue2";
}
#+end_src
** PipeWire
Use PipeWire as audio server.
#+begin_src nix :noweb-ref nixos-section
{
# PipeWire requires pulseaudio to be disabled.
services.pulseaudio.enable = false;
security.rtkit.enable = true;
services.pipewire = {
enable = true;
alsa.enable = true;
alsa.support32Bit = true;
pulse.enable = true;
};
}
#+end_src
=pavucontrol= is PulseAudio Volume Control—a nice utility for controlling pulseaudio settings. (~<<home-manager-section>>~)
#+begin_src nix :noweb-ref home-manager-section
{
home.packages = pkgs.lib.linux-only [ pkgs.pavucontrol ];
}
#+end_src
#+begin_src emacs-lisp
(defun rasen/pavucontrol ()
(interactive)
(rasen/switch-start "Pavucontrol" "pavucontrol"))
#+end_src
Bind volume control commands. (emacs-lisp)
#+begin_src 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%-")))
#+end_src
** Bluetooth
I have a bluetooth headset, so this enables bluetooth audio in NixOS.
(~<<nixos-section>>~)
#+begin_src nix :noweb-ref nixos-section
{
hardware.bluetooth.enable = true;
}
#+end_src
** ADB
I need to access my Android device. (~<<nixos-section>>~)
#+begin_src nix :noweb-ref nixos-section
{
services.udev.packages = [ pkgs.android-udev-rules ];
programs.adb.enable = true;
users.users.rasen.extraGroups = ["adbusers"];
}
#+end_src
** fwupd
fwupd is a service that allows applications to update firmware. (~<<nixos-section>>~)
#+begin_src nix :noweb-ref nixos-section
{
services.fwupd.enable = true;
}
#+end_src
Execute the following command to update firmware.
#+begin_src sh :tangle no
fwupdmgr get-updates
#+end_src
* Browsers
Firefox is default, Chrome for backup.
#+begin_src nix :noweb-ref home-manager-section
{
home.packages = pkgs.lib.linux-only [
pkgs.firefox
pkgs.google-chrome
];
}
#+end_src
** Tridactyl
Tridactyl is a Firefox plugin that provides Vim-like bindings.
Here is my config. (~<<tridactylrc>>~)
#+begin_src fundamental :noweb-ref tridactylrc :tangle no
" drop all existing configuration
sanitize tridactyllocal tridactylsync
bind J scrollline -10
bind K scrollline 10
bind j scrollline -2
bind k scrollline 2
#+end_src
Link tridactyl config to the place the tridactyl can find it. (~<<home-manager-section>>~)
#+begin_src nix :noweb-ref home-manager-section
{
xdg.configFile."tridactyl/tridactylrc".text = ''
<<tridactylrc>>
'';
}
#+end_src
** Edit text in browser
I use [[https://github.com/GhostText/GhostText][GhostText]] firefox extension.
~atomic-chrome~ Emacs extension is compatible with it. (emacs-lisp)
#+begin_src 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))
#+end_src
* Key bindings
I have my custom modal editing mode in [[file:.emacs.d/site-lisp/eon.el][eon.el]].
#+begin_src emacs-lisp
(use-package eon
:load-path "~/.emacs.d/user-lisp/"
:config
(eon-global-mode))
#+end_src
** Compile mode
#+begin_src emacs-lisp
(use-package compile ; built-in
:config
(setq compilation-scroll-output t))
#+end_src
** Minibuffer
#+begin_src emacs-lisp
(define-key minibuffer-local-map (kbd "C-e") #'exit-minibuffer)
#+end_src
** scheme
#+begin_src emacs-lisp
(use-package scheme
:config
(put 'module 'scheme-indent-function 'defun))
#+end_src
* Org-mode
** General
#+begin_src emacs-lisp
(use-package org
:mode ("\\.org$" . org-mode)
;; :bind
;; ("C-c l" . org-store-link)
;; ("s-SPC c" . org-capture)
;; ("s-SPC a" . org-agenda)
;; ("s-SPC o" . org-clock-out)
;; ("s-SPC l" . org-clock-in-last)
;; ("s-SPC j" . org-clock-goto)
;; :bind-keymap
;; ("s-SPC" . org-mode-map)
:hook (org-mode . flyspell-mode)
:init
<<org-init>>
:config
<<org-config>>
)
#+end_src
Do not indent inside tasks
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-adapt-indentation nil)
#+end_src
Do not indent org-babel blocks.
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-edit-src-content-indentation 0)
#+end_src
Do not indent tags.
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-tags-column 0)
#+end_src
When entering tags, offer tags from all agenda files. (This is the closes to global tag tracking I could find.))
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-complete-tags-always-offer-all-agenda-tags t)
#+end_src
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-ellipsis "…")
#+end_src
By default, show all of org file. (This can be changed on a per-file basis with ~#+STARTUP:~.)
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-startup-folded nil)
#+end_src
Make the table header float if scrolled out of view.
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-table-header-line-p t)
#+end_src
Hide emphasis markers (asterisks and slashes).
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-hide-emphasis-markers t)
#+end_src
Allow emphasis marks to follow ndash/mdash, and proper quotes.
#+begin_src emacs-lisp :noweb-ref org-config
;; 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))
#+end_src
Open pdfs in external viewer:
#+begin_src emacs-lisp :noweb-ref org-config
(add-to-list 'org-file-apps
`("\\.pdf\\'" . ,(if (eq system-type 'darwin)
"open %s"
"zathura %s")))
#+end_src
Use =whitespace-mode= in Org (but don't show too long lines).
#+begin_src emacs-lisp :noweb-ref org-config
(add-hook 'org-mode-hook (lambda ()
(setq-local whitespace-style '(face
tab-mark
empty
trailing))
(whitespace-mode t)))
#+end_src
My directory for org files.
#+begin_src emacs-lisp :noweb-ref org-config
(setq rasen/org-directory "~/org")
#+end_src
My helper to find all org files in a directory.
#+begin_src emacs-lisp :noweb-ref org-config
(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))
#+end_src
Package for =f-files= and =f-ext?= functions.
#+begin_src emacs-lisp :noweb-ref org-init
(use-package f
:commands (f-files f-ext? f-no-ext))
#+end_src
** Electric quote
Use proper quotes in org-mode.
#+begin_src emacs-lisp
(use-package org
:defer t
:config
(add-hook 'org-mode-hook #'electric-quote-local-mode))
#+end_src
** Todo
A special function that marks the task as done yesterday if prefix is supplied (useful for habits).
#+begin_src emacs-lisp :noweb-ref org-config
(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)))
#+end_src
Use the following states: =TODO= =NEXT= =DONE= =CANCELED= =WAIT=.
#+begin_src emacs-lisp :noweb-ref org-config
(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)
#+end_src
When repeated task is finished, go back to =TODO= state.
#+begin_src emacs-lisp :noweb-ref org-config
(setq-default org-todo-repeat-to-state "NEXT")
#+end_src
Log state changes to "LOGBOOK" drawer.
#+begin_src emacs-lisp :noweb-ref org-config
(setq-default org-log-into-drawer t)
#+end_src
Save =CLOSED= timestamp when task is done.
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-log-done t)
#+end_src
Disable force-logging repeated tasks because otherwise you’ll get duplicated log lines.
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-log-repeat nil)
#+end_src
Fontify the whole line for done tasks.
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-fontify-done-headline t)
#+end_src
Import =org-expiry= for =org-expiry-insert-created=---this inserts =CREATED= property.
#+begin_src emacs-lisp :noweb-ref org-config
(require 'org-expiry)
(setq org-expiry-inactive-timestamps t)
;; The next line was required but it fails with error now. Still works
;; somehow.
;; (org-expiry-insinuate)
#+end_src
Schedule task for today and mark it NEXT.
I use this a lot during daily planning.
#+begin_src emacs-lisp :noweb-ref org-config
(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"))
#+end_src
A command to fold everything except =NEXT= items.
#+begin_src emacs-lisp :noweb-ref org-config
(defun rasen/org-occur-next ()
(interactive)
(let ((org-highlight-sparse-tree-matches nil))
(org-occur (concat "^" org-outline-regexp " *" "NEXT" "\\>"))))
#+end_src
Disable highlighting for org-occur.
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-highlight-sparse-tree-matches nil)
#+end_src
** Highlight projects
Fontify all headlines with the =:PROJECT:= tag.
#+begin_src emacs-lisp :noweb-ref org-config
(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)
#+end_src
Note that the face is overridden in [[*Color theme][Color theme]] section.
** Clocking
Remove clocks with 0 duration.
#+begin_src emacs-lisp :noweb-ref org-config
(setq-default org-clock-out-remove-zero-time-clocks t)
#+end_src
Save more last clocks.
#+begin_src emacs-lisp :noweb-ref org-config
(setq-default org-clock-history-length 10)
#+end_src
** Capture
I use an extension that adds page url to the title (used for page tracking). Strip it down here
#+begin_src emacs-lisp :noweb-ref org-config
(defun rasen/strip-url-from-title (title)
(message "stripping: %s" title)
(replace-regexp-in-string
" @ [^ ]*$"
""
(replace-regexp-in-string " \\[[^]]*\\]\\[[^]]*\\]$" "" title)))
#+end_src
My capture templates.
#+begin_src emacs-lisp :noweb-ref org-config
(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"))
#+end_src
Enable org-protocol.
#+begin_src emacs-lisp :noweb-ref org-config
(require 'org-protocol)
#+end_src
=%l= in org-capture fails with multiline context, so use only the first line as a context.
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-context-in-file-links 1)
#+end_src
** Capturing images
#+begin_src emacs-lisp
(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))))
#+end_src
** 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.
#+begin_src emacs-lisp
;; 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)))
#+end_src
** cliplink
#+begin_src emacs-lisp
(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))
#+end_src
** Refile
#+begin_src emacs-lisp :noweb-ref org-config
(defun rasen/org-refile-files ()
(append
'("~/org/dd/index.org" "~/org/fluxon/index.org")
(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")
#+end_src
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.
#+begin_src emacs-lisp :noweb-ref org-config
(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))))
#+end_src
*** Refiling with hydras
Adapted from [[https://mollermara.com/blog/Fast-refiling-in-org-mode-with-hydras/][Fast refiling in org-mode with hydras | Josh Moller-Mara]]. Extended to support refiling by outline path.
#+begin_src emacs-lisp :noweb-ref org-config
(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 'org-mode-map "w" #'rasen/org-refile-hydra/body)
;; (leader-def 'org-agenda-mode-map "w" #'rasen/org-refile-hydra/body)
;; (leader-def 'org-capture-mode-map "w" #'rasen/org-refile-hydra/body)
#+end_src
*** 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.
#+begin_src emacs-lisp :noweb-ref org-config
(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)))))))
#+end_src
** Archive
#+begin_src emacs-lisp :noweb-ref org-config
;; (setq-default org-archive-default-command 'org-archive-to-archive-sibling)
(setq-default org-archive-default-command #'org-archive-subtree)
#+end_src
** Agenda
Set my org files location.
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-directory "~/org"
org-default-notes-file rasen/org-refile-file
org-agenda-files (cons "~/org/roam/dd/index.org" (cons "~/org/roam/fluxon/index.org" (rasen/org-files-in-dir "~/org"))))
#+end_src
Show agenda as the only window and restore window layout on quit (so that agenda does not mess up with my layout).
#+begin_src emacs-lisp
(setq org-agenda-window-setup 'only-window)
(setq org-agenda-restore-windows-after-quit t)
#+end_src
Configure my agenda view.
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-agenda-span 6)
#+end_src
Configure stuck projects.
#+begin_src emacs-lisp :noweb-ref org-config
(add-to-list 'org-tags-exclude-from-inheritance "PROJECT")
(setq org-stuck-projects
'("+PROJECT/-TODO-DONE-CANCELED-WAIT" ("NEXT" "WAIT") nil ""))
#+end_src
Do not align tags in agenda.
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-agenda-tags-column 0)
#+end_src
Do not show project tags.
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-agenda-hide-tags-regexp "PROJECT\\|fc\\|suspended")
#+end_src
#+begin_src emacs-lisp
(use-package org-super-agenda
:bind
(:map org-super-agenda-header-map
("k" . #'org-agenda-next-line)
("j" . #'org-agenda-previous-line))
:config
;; 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 "......" "----------------")
)
(defmacro rasen/mk-agenda (agenda-files inbox-files extra-categories)
`'((agenda "" ((org-agenda-files ,agenda-files)
;; start from yesterday
(org-agenda-start-day "-1d")
;; show 8 days
(org-agenda-span 8)
;; 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-habit-show-done-always-green t)
(org-super-agenda-groups
'(;; 1. time-grid
;; 2. deadlines
;; 3. scheduled
;; 3.5. WAIT scheduled today
;; 4. habits
;; 5. rest (anniversaries, etc.)
(: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 ; deadlines
:deadline t
:order 2)
(:name none ;; scheduled today
:and (:todo ("WAIT")
:scheduled today)
:order 3.5)
(:name none ; scheduled
:scheduled t
:order 3)
;; (:name none ;; scheduled today
;; :scheduled today
;; :order 2)
;; (:name none ;; deadline today
;; :deadline today
;; :order 1.7)
;; (:name none ;; deadline reminders
;; :deadline t
;; :order 1.5)
;; (:name none ;; scheduled reminders
;; :deadline t
;; :order 1.9)
(:name none ;; everything else
:anything t
:order 5)))))
(alltodo "" ((org-agenda-files ,agenda-files)
(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)
,@extra-categories
(:name "Next"
:todo "NEXT"
:order 8.99)
(: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 ,inbox-files)
(org-agenda-overriding-header "")
(org-super-agenda-groups
'((:name "Inbox"
:auto-category t
:anything t)))))))
(setq org-agenda-custom-commands
`(("o" "Overview" ,(rasen/mk-agenda
'("~/org/plan.org")
(mapcar (lambda (x) (concat rasen/org-directory "/" x))
'("refile-omicron.org"
"orgzly.org"))
nil))
("f" "Fluxon" ,(rasen/mk-agenda
'("~/org/roam/fluxon/index.org" "~/org/refile-bayraktar.org")
'("~/org/refile-bayraktar.org")
nil))
("d" "DataDog" ,(rasen/mk-agenda
'("~/org/roam/dd/index.org" "~/org/refile-oleksiishmalko.org")
'("~/org/refile-oleksiishmalko.org")
nil))
("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))
#+end_src
*** 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=.
#+begin_src emacs-lisp
(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))))))
#+end_src
** Babel
Code-highlight (fontify) org-babel (=#+begin_src=) blocks.
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-src-fontify-natively t)
#+end_src
Do not confirm evaluation for emacs-lisp.
#+begin_src emacs-lisp :noweb-ref org-config
(defun rasen/org-confirm-babel-evaluate (lang body)
(not (member lang '("emacs-lisp"))))
(setq org-confirm-babel-evaluate 'rasen/org-confirm-babel-evaluate)
#+end_src
Load more languages:
#+begin_src emacs-lisp :noweb-ref org-config
(org-babel-do-load-languages 'org-babel-load-languages
'((shell . t)))
#+end_src
** Latex preview
Install imagemagick and latex:
#+begin_src nix :noweb-ref home-manager-section
{
home.packages = [
pkgs.imagemagick
# 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
];
}
#+end_src
#+begin_src emacs-lisp :noweb-ref org-config
(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")))
#+end_src
** Image preview
#+begin_src emacs-lisp :noweb-ref org-config
;; Scale inline images by default
(setq org-image-actual-width '(800))
;; Show inline images by default
(setq org-startup-with-inline-images t)
#+end_src
** 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.
#+begin_src emacs-lisp :noweb-ref org-config
(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"))
#+end_src
** Crypt
Allow encrypted entries in org files.
#+begin_src emacs-lisp :noweb-ref org-config
(require 'org-crypt)
(org-crypt-use-before-save-magic)
(add-to-list 'org-tags-exclude-from-inheritance "crypt")
(setq org-crypt-key "rasen.dubi@gmail.com")
(add-hook 'org-babel-pre-tangle-hook 'org-decrypt-entries t)
#+end_src
** 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:
#+begin_src emacs-lisp :noweb-ref org-config
(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)))))
#+end_src
** org-checklist
Setting property =RESET_CHECK_BOXES= on a periodic task to =t= will clear all checkboxes when the task is closed.
#+begin_src emacs-lisp :noweb-ref org-config
(require 'org-checklist)
#+end_src
** Habits
#+begin_src emacs-lisp :noweb-ref org-config
(require 'org-habit)
(setq org-habit-show-habits-only-for-today t)
(setq org-habit-preceding-days 25)
(setq org-habit-following-days 3)
#+end_src
** adaptive-wrap
Better line wrapping. (Use proper wrap-prefix in lists, etc.)
#+begin_src emacs-lisp
(use-package adaptive-wrap
:config
(add-hook 'org-mode-hook #'adaptive-wrap-prefix-mode))
#+end_src
** org-id
Use short timestamps as ids. It has less precision but that’s enough for me.
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-id-method 'ts
org-id-ts-format "%Y%m%d%H%M%S")
#+end_src
Configure ~org-store-link~ to prefer ids for headlines.
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-id-link-to-org-use-id 'create-if-interactive)
#+end_src
Override ~org-id-new~ to use UTC timezone, so ids are not affected by daylight saving and my timezone.
#+begin_src emacs-lisp :noweb-ref org-config
(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 (when org-id-include-domain
(require 'message)
(concat "@" (message-make-fqdn)))))
(setq unique (concat etime postfix))))
((eq org-id-method 'ts)
(let ((ts (format-time-string org-id-ts-format (el-patch-add nil "UTC")))
(postfix (when org-id-include-domain
(require 'message)
(concat "@" (message-make-fqdn)))))
(setq unique (concat ts postfix))))
(t (error "Invalid `org-id-method'")))
(concat prefix unique)))
#+end_src
Highlight org-id links with different color.
#+begin_src emacs-lisp :noweb-ref org-config
(defface rasen/org-id-link
'((t :inherit org-link))
"Face for org-id links.")
(org-link-set-parameters "id" :face 'rasen/org-id-link)
#+end_src
Actual style is overridden in Color theme section.
** org-roam
#+begin_src emacs-lisp
(use-package org-roam
:after org
:diminish
:autoload (org-roam-db-update-file)
:commands (rasen/org-daily-dispatch)
:bind
(:map org-mode-map
("C-c i" . 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))
#+end_src
*** org-roam-exclude-org-fc
Do not treat [[*org-fc][org-fc]] cards as nodes.
#+begin_src emacs-lisp :noweb-ref org-roam-exclude-org-fc :tangle no
(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)))))
#+end_src
*** org-roam-slip-boxes
Slip boxes are basically different directories that I split notes into.
#+begin_src emacs-lisp :noweb-ref org-roam-slip-boxes :tangle no
(defconst rasen/slip-boxes
'(;; Default slip-box with permanent notes
("d" "default" "" "${rasen/capture-tsid}")
;; duplicate with another key to allow \\ n n
("n" "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}")
("D" "datadog" "dd/" "${rasen/capture-tsid}")
("P" "pulse" "pulse/" "${rasen/capture-tsid}" "#+DATE: %<%FT%T%z>\n#+LAST_MODIFIED: \n")
;; Posts
("p" "posts" "posts/" "${slug}" "#+DATE: %<%Y-%m-%d>\n#+LAST_MODIFIED: \n#+DRAFT: true")
;; 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))
#+end_src
And a helper command to move notes between slip boxes.
#+begin_src emacs-lisp :noweb-ref org-roam-slip-boxes :tangle no
(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)))
#+end_src
And some extra refile helpers for dealing with inbox.
#+begin_src emacs-lisp :noweb-ref org-roam-slip-boxes :tangle no
;; 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)))))
#+end_src
*** 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=.
#+begin_src emacs-lisp :noweb-ref org-roam-node-display :tangle no
(setq org-roam-node-display-template "${hierarchy:*} ${tags:10}")
(defun org-roam-node-filetitle (node)
"Return the file TITLE for the node."
(org-roam-get-keyword "TITLE" (org-roam-node-file node)))
(defun org-roam-node-hierarchy (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)))
(defun org-roam-node-directories (node)
(if-let ((dirs (file-name-directory (file-relative-name (org-roam-node-file node) org-roam-directory))))
(string-join (f-split dirs) "/")
nil))
;; (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))
#+end_src
*** org-roam-buffer
Configure org-roam-buffer (buffer with backlinks).
Show all sections:
#+begin_src emacs-lisp :noweb-ref org-roam-buffer :tangle no
(setq org-roam-mode-section-functions
(list #'org-roam-backlinks-section
#'org-roam-reflinks-section
#'org-roam-unlinked-references-section))
#+end_src
Make it prettier—enable word-wrap and variable pitch font.
#+begin_src emacs-lisp :noweb-ref org-roam-buffer :tangle no
(add-hook 'org-roam-mode-hook #'visual-line-mode)
(add-hook 'org-roam-mode-hook #'variable-pitch-mode)
#+end_src
*** 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.
#+begin_src emacs-lisp :noweb-ref org-roam-dailies :tangle no
(require 'org-roam-dailies)
(setq org-roam-dailies-directory "life/journal/")
(defconst rasen/org-roam-daily-template
;; 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"))
(setq org-roam-dailies-capture-templates
;; %U inserts incorrect date when now and target date don't
;; match. org-timestamp-inactive is a workaround to insert
;; current time as inactive timestamp correctly.
`(("j" "journal" entry "* %(org-timestamp-inactive '(16)) %?"
:target (file+head "%<%Y-%m-%d>.org" ,rasen/org-roam-daily-template)
:empty-lines-before 1
:unnarrowed t
:clock-in t
:clock-keep nil
:clock-resume t)))
;; Try to guess "today"
(setq org-extend-today-until 6)
;; Use 23:59 timestamp when past midnight. This is usually desired so
;; that org-habit counts repetitions properly.
(setq org-use-effective-time t)
(defun rasen/org-daily-dispatch (arg)
"Dispatch command for org-roam-dailies.
With no prefix ARG, capture a note for today.
With C-u, prompt for a date and capture a note for that day.
With C-u C-u, prompt for a date and open that daily note.
Intended as a unified entry point for working with org-roam-dailies."
(interactive "P")
(cond ((eq arg nil) (org-roam-dailies-capture-today))
((equal arg '(4)) (org-roam-dailies-capture-date))
((equal arg '(16)) (org-roam-dailies-goto-date))))
#+end_src
*** 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:
#+begin_src js
javascript:location.href = 'org-protocol://roam-ref?' + new URLSearchParams({
template: 'r',
ref: location.href.split('#')[0],
title: encodeURIComponent(document.title),
body: encodeURIComponent(window.getSelection())
})
#+end_src
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.
#+begin_src emacs-lisp :noweb-ref org-roam-protocol :tangle no
(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"))))
#+end_src
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.)
#+begin_src emacs-lisp :noweb-ref org-roam-protocol :tangle no
(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"))))
#+end_src
*** org-roam-graph
Install graphviz globally:
#+begin_src nix :noweb-ref home-manager-section
{
home.packages = [ pkgs.graphviz ];
}
#+end_src
Emacs configuration:
#+begin_src emacs-lisp :noweb-ref org-roam-graph :tangle no
(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>>
#+end_src
**** org-roam-graph-exclude-node
Patch org-roam to allow a custom function to exclude nodes.
#+begin_src emacs-lisp :noweb-ref org-roam-graph-exclude-node :tangle no
(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))))
#+end_src
Exclude all links that are not nodes (non-id) as well as links from non-permanent directories.
#+begin_src emacs-lisp :noweb-ref org-roam-graph-exclude-node :tangle no
(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))))))
#+end_src
*** org-roam-kebab-slugs
Patch slug function so it uses kebab-case instead of snake-case.
#+begin_src emacs-lisp :noweb-ref org-roam-kebab-slugs :tangle no
(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)))))
#+end_src
*** org-roam-update-ids
A helper function for the next time org-id fucks up its database.
#+begin_src emacs-lisp :noweb-ref org-roam-update-ids :tangle no
(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$")))
#+end_src
*** 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.
#+begin_src emacs-lisp :noweb-ref org-roam-new-node
(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"))
(defun rasen/org-roam-new-node-pulse ()
(interactive)
(rasen/org-roam-new-node "P"))
(defun rasen/org-roam-new-node-fluxon ()
(interactive)
(rasen/org-roam-new-node "f"))
;; (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 p" #'rasen/org-roam-new-node-pulse)
;; (s-leader-def "n F" (defun rasen/org-roam-new-node-fluxon ()
;; (interactive)
;; (rasen/org-roam-new-node "f")))
#+end_src
*** org-roam-update-title
#+begin_src emacs-lisp :noweb-ref 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)))
#+end_src
** org-roam-ui
#+begin_src emacs-lisp
(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))
#+end_src
** org-ref
#+begin_src emacs-lisp
(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)
(define-key bibtex-mode-map (kbd "C-c C-c") #'org-ref-clean-bibtex-entry)
(define-key bibtex-mode-map (kbd "C-c c") #'org-ref-clean-bibtex-entry)
(define-key bibtex-mode-map (kbd "C-c s") #'bibtex-sort-buffer)
(define-key bibtex-mode-map (kbd "C-c n") #'org-ref-open-bibtex-notes)
(define-key bibtex-mode-map (kbd "C-c a") #'org-ref-bibtex-assoc-pdf-with-entry)
(define-key bibtex-mode-map (kbd "C-c f") #'org-ref-bibtex-pdf)
(define-key bibtex-mode-map (kbd "C-c o") #'org-ref-url-html-to-bibtex)
(define-key bibtex-mode-map (kbd "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)))))
#+end_src
** org-ref-cite
#+begin_src emacs-lisp
;; (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))
#+end_src
** org-roam-bibtex
Citations and bibliography tools for org-mode.
#+begin_src emacs-lisp
(use-package org-roam-bibtex
:diminish
:after org-roam
:config
(require 'org-ref)
(org-roam-bibtex-mode))
#+end_src
** 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).
Install required dependencies:
#+begin_src nix :noweb-ref home-manager-section
{
home.packages = [
pkgs.findutils
pkgs.gawk
];
}
#+end_src
#+begin_src emacs-lisp
(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-index-positions (cards)
(mapcan (lambda (card) (org-fc-index-flatten-card card)) cards))
(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)))
(rasen/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*")))
(define-key org-fc-review-flip-mode-map (kbd "RET") 'org-fc-review-flip)
(define-key org-fc-review-flip-mode-map (kbd "n") 'org-fc-review-flip)
(define-key org-fc-review-flip-mode-map (kbd "s") 'org-fc-review-suspend-card)
(define-key org-fc-review-flip-mode-map (kbd "q") 'org-fc-review-quit)
(define-key org-fc-review-rate-mode-map (kbd "a") 'org-fc-review-rate-again)
(define-key org-fc-review-rate-mode-map (kbd "h") 'org-fc-review-rate-hard)
(define-key org-fc-review-rate-mode-map (kbd "g") 'org-fc-review-rate-good)
(define-key org-fc-review-rate-mode-map (kbd "n") 'org-fc-review-rate-good)
(define-key org-fc-review-rate-mode-map (kbd "e") 'org-fc-review-rate-easy)
(define-key org-fc-review-rate-mode-map (kbd "s") 'org-fc-review-suspend-card)
(define-key org-fc-review-rate-mode-map (kbd "q") 'org-fc-review-quit)
(define-key org-fc-dashboard-mode-map (kbd "r") #'org-fc-dashboard-review)
(define-key org-fc-dashboard-mode-map (kbd "g") (defun rasen/org-fc-dashboard ()
(interactive)
(org-fc-dashboard org-fc-context-all)))
(define-key org-fc-dashboard-mode-map (kbd "h") #'rasen/org-fc-upcoming-histogram)
(define-key org-fc-dashboard-mode-map (kbd "q") #'quit-window)
<<org-fc-review-todos>>)
#+end_src
** COMMENT org-fc review todos
My modifications to support reviewing arbitrary todo lists with spaced repetition.
#+begin_src emacs-lisp :noweb-ref org-fc-review-todos
(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))
#+end_src
** toggle markup/view
#+begin_src emacs-lisp
(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))
(define-key org-mode-map (kbd "C-c \\") #'rasen/toggle-org-view-mode)
#+end_src
** Org key bindings
When editing code blocks, use current window and do not reorganize frame.
#+begin_src emacs-lisp :noweb-ref org-config
(setq org-src-window-setup 'current-window)
#+end_src
Open file links in the same window.
#+begin_src emacs-lisp :noweb-ref org-config
(push '(file . find-file) org-link-frame-setup)
#+end_src
** 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.
#+begin_src emacs-lisp
(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"))))
#+end_src
** 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:
#+begin_src emacs-lisp :tangle no
(org-defkey map (kbd ".")
(lambda () (interactive)
;; Are we at the beginning of the prompt?
(if (looking-back "^[^:]+: "
Not written in Markdown, so it's shown here as plain text — view it formatted on GitHub.
Truncated — view the full README on GitHub.
Nix
62.1%
Emacs Lisp
24.5%
Vim Script
5.5%
YASnippet
3.5%
Python
3.4%