https://github.com/rasendubi/dotfiles
My dotfiles
https://github.com/rasendubi/dotfiles
dotfiles emacs-configuration nix-dotfiles nixos org-mode-configuration
Last synced: 6 months ago
JSON representation
My dotfiles
- Host: GitHub
- URL: https://github.com/rasendubi/dotfiles
- Owner: rasendubi
- Created: 2013-10-03T20:38:16.000Z (about 12 years ago)
- Default Branch: master
- Last Pushed: 2025-02-17T02:07:54.000Z (8 months ago)
- Last Synced: 2025-03-30T19:38:13.862Z (7 months ago)
- Topics: dotfiles, emacs-configuration, nix-dotfiles, nixos, org-mode-configuration
- Language: Emacs Lisp
- Homepage:
- Size: 1.57 MB
- Stars: 227
- Watchers: 14
- Forks: 83
- Open Issues: 0
-
Metadata Files:
- Readme: README.org
Awesome Lists containing this project
README
#+PROPERTY: header-args :tangle yes :noweb yes :results silent
#+STARTUP: overview* Introduction
Hi there! That's my dotfiles.This repository contains configuration for three hosts:
- omicron — my main laptop which runs NixOS
- pie — my home server RPi running NixOS (see [[./pie.org][pie.org]])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.
[[./pie.org][pie.org]] is a separate NixOS config for pie host.
To generate actual nix files, you can open this file in Emacs, and execute =M-x org-babel-tangle=. Or from command line with the following command.
#+begin_src sh :tangle no
emacs README.org --batch -f org-babel-tangle
#+end_src#+RESULTS:
Note that you need to patch org-babel to correctly generate configs (<>)
I keep generated files in sync with org files (so this repo is a valid Nix Flake), but they are not worth looking at—you'll have much better time reading this doc instead.
Pieces not (yet) covered in org files are:
- scripts at =bin/=* Table of Contents :TOC_3:
- [[#introduction][Introduction]]
- [[#top-level][Top-level]]
- [[#flake][Flake]]
- [[#stable-packages][Stable packages]]
- [[#nixos][NixOS]]
- [[#home-manager][Home manager]]
- [[#packages][Packages]]
- [[#overlays][Overlays]]
- [[#nixos-1][NixOS]]
- [[#general][General]]
- [[#re-expose-nixpkgs][Re-expose nixpkgs]]
- [[#sandbox][Sandbox]]
- [[#users][Users]]
- [[#machines][Machines]]
- [[#omicron][omicron]]
- [[#emacs][Emacs]]
- [[#install-emacs][Install Emacs]]
- [[#bootstrap-emacs-config][Bootstrap Emacs config]]
- [[#patch-ob-tangle][Patch ob-tangle]]
- [[#gc-hacks][GC hacks]]
- [[#use-package][use-package]]
- [[#package][package]]
- [[#general-package][General (package)]]
- [[#dont-clutter-system][Don't clutter system]]
- [[#helpers][Helpers]]
- [[#ivy][ivy]]
- [[#smex][smex]]
- [[#counsel][counsel]]
- [[#avy][avy]]
- [[#imenu--imenu-list][imenu / imenu-list]]
- [[#wgrep][wgrep]]
- [[#whitespace][whitespace]]
- [[#whitespace-cleanup][whitespace-cleanup]]
- [[#which-key][which-key]]
- [[#google-translate][Google translate]]
- [[#tab-bar-mode][tab-bar-mode]]
- [[#highlight-current-line][Highlight current line]]
- [[#scrolling][Scrolling]]
- [[#visual-fill-column][visual-fill-column]]
- [[#misc][Misc]]
- [[#environment][Environment]]
- [[#exwm][EXWM]]
- [[#window-management][Window management]]
- [[#window-layout][Window layout]]
- [[#screen-locking][Screen locking]]
- [[#slock][Slock]]
- [[#xss-lock][xss-lock]]
- [[#exwm-integration][EXWM integration]]
- [[#system-tray][System tray]]
- [[#screenshots][Screenshots]]
- [[#misc-1][Misc]]
- [[#input][Input]]
- [[#keyboard][Keyboard]]
- [[#workman][Workman]]
- [[#keyboard-layout][Keyboard layout]]
- [[#xkeymap][Xkeymap]]
- [[#compose-keys][Compose keys]]
- [[#xcape][xcape]]
- [[#emacs-quail][Emacs quail]]
- [[#mouse][Mouse]]
- [[#python-listener][Python listener]]
- [[#emacs-handler][Emacs handler]]
- [[#network][Network]]
- [[#networkmanager][NetworkManager]]
- [[#ssh][SSH]]
- [[#mosh][Mosh]]
- [[#dnsmasq][dnsmasq]]
- [[#firewall][Firewall]]
- [[#services][Services]]
- [[#locate][Locate]]
- [[#gitolite][Gitolite]]
- [[#syncthing][Syncthing]]
- [[#docker][Docker]]
- [[#backup][Backup]]
- [[#direnv][direnv]]
- [[#flake-1][flake]]
- [[#direnv--lorri][direnv + lorri]]
- [[#virtualbox][VirtualBox]]
- [[#hardware][Hardware]]
- [[#do-not-suspend-on-ac][Do not suspend on AC]]
- [[#autorandr][Autorandr]]
- [[#screen-brightness][Screen brightness]]
- [[#redshift][Redshift]]
- [[#pipewire][PipeWire]]
- [[#bluetooth][Bluetooth]]
- [[#adb][ADB]]
- [[#fwupd][fwupd]]
- [[#browsers][Browsers]]
- [[#tridactyl][Tridactyl]]
- [[#edit-text-in-browser][Edit text in browser]]
- [[#evil-mode][Evil-mode]]
- [[#general-1][General]]
- [[#swap-k-and-j][Swap k and j]]
- [[#evil-numbers][evil-numbers]]
- [[#evil-collection][evil-collection]]
- [[#evil-surrond][evil-surrond]]
- [[#calc][calc]]
- [[#evilify-compile-mode][Evilify compile mode]]
- [[#evilify-minibuffer][Evilify minibuffer]]
- [[#evilify-shell-mode][Evilify shell mode]]
- [[#lispyville][lispyville]]
- [[#org-mode][Org-mode]]
- [[#general-2][General]]
- [[#todo][Todo]]
- [[#highlight-projects][Highlight projects]]
- [[#clocking][Clocking]]
- [[#capture][Capture]]
- [[#org-capture-keybindings][org-capture keybindings]]
- [[#capturing-images][Capturing images]]
- [[#datetree][datetree]]
- [[#cliplink][cliplink]]
- [[#refile][Refile]]
- [[#refiling-with-hydras][Refiling with hydras]]
- [[#refile-last-but-before-archive][Refile last but *before* archive]]
- [[#archive][Archive]]
- [[#agenda][Agenda]]
- [[#allow-next-projects-to-stuck][Allow NEXT projects to stuck]]
- [[#babel][Babel]]
- [[#latex-preview][Latex preview]]
- [[#image-preview][Image preview]]
- [[#export][Export]]
- [[#crypt][Crypt]]
- [[#org-list][org-list]]
- [[#org-checklist][org-checklist]]
- [[#habits][Habits]]
- [[#adaptive-wrap][adaptive-wrap]]
- [[#org-id][org-id]]
- [[#org-roam][org-roam]]
- [[#org-roam-exclude-org-fc][org-roam-exclude-org-fc]]
- [[#org-roam-slip-boxes][org-roam-slip-boxes]]
- [[#org-roam-node-display][org-roam-node-display]]
- [[#org-roam-buffer][org-roam-buffer]]
- [[#org-roam-dailies][org-roam-dailies]]
- [[#org-roam-protocol][org-roam-protocol]]
- [[#org-roam-graph][org-roam-graph]]
- [[#org-roam-kebab-slugs][org-roam-kebab-slugs]]
- [[#org-roam-update-ids][org-roam-update-ids]]
- [[#org-roam-new-node][org-roam-new-node]]
- [[#org-roam-ui][org-roam-ui]]
- [[#org-ref][org-ref]]
- [[#org-roam-bibtex][org-roam-bibtex]]
- [[#toc-org][toc-org]]
- [[#org-fc][org-fc]]
- [[#org-fc-review-todos][org-fc review todos]]
- [[#toggle-markupview][toggle markup/view]]
- [[#evilify-org-mode][Evilify org-mode]]
- [[#timestamps][Timestamps]]
- [[#fix-focus-steal-when-inserting--in-date-field][Fix focus steal when inserting “.” in date field]]
- [[#mail-setup][Mail setup]]
- [[#applications][Applications]]
- [[#older-mbsync][Older mbsync]]
- [[#interface][Interface]]
- [[#emacs-1][Emacs]]
- [[#applications-1][Applications]]
- [[#gpg][GPG]]
- [[#fix-epg--filter-revoked-keys-wrongly-filtering-out-my-keys][Fix epg--filter-revoked-keys wrongly filtering out my keys]]
- [[#yubikey][Yubikey]]
- [[#password-store][password-store]]
- [[#kde-apps][KDE apps]]
- [[#zathura][Zathura]]
- [[#user-applications][User applications]]
- [[#development][Development]]
- [[#vim][Vim]]
- [[#terminal--shell][Terminal / shell]]
- [[#rxvt-unicode][rxvt-unicode]]
- [[#vterm][vterm]]
- [[#fish][fish]]
- [[#eshell][eshell]]
- [[#tmux][tmux]]
- [[#other-terminal-goodies][Other terminal goodies]]
- [[#git][git]]
- [[#git-config][git config]]
- [[#magit][magit]]
- [[#git-commit][git-commit]]
- [[#diff-hl][diff-hl]]
- [[#man-pages][Man pages]]
- [[#emacs-2][Emacs]]
- [[#use-spaces-for-indentation][Use spaces for indentation]]
- [[#make-underscore-part-of-words][Make underscore part of words]]
- [[#color-compilation-mode][Color compilation mode]]
- [[#projectile][projectile]]
- [[#company][company]]
- [[#hippie-expand][Hippie expand]]
- [[#flycheck][flycheck]]
- [[#flycheck-inline][flycheck-inline]]
- [[#electric-pair][electric-pair]]
- [[#color-identifiers][Color identifiers]]
- [[#dtrt-indent][dtrt-indent]]
- [[#paren-face][paren-face]]
- [[#lsp][LSP]]
- [[#commenting][Commenting]]
- [[#yasnippet][yasnippet]]
- [[#languages][Languages]]
- [[#emacs-lisp][Emacs lisp]]
- [[#nix][Nix]]
- [[#haskell][Haskell]]
- [[#rust][Rust]]
- [[#go][Go]]
- [[#cc][C/C++]]
- [[#cmake][CMake]]
- [[#python][Python]]
- [[#javascript][JavaScript]]
- [[#typescript][Typescript]]
- [[#vue][Vue]]
- [[#web-mode][Web-mode]]
- [[#clojurescript][Clojure(Script)]]
- [[#racket][Racket]]
- [[#groovy][Groovy]]
- [[#kotlin][Kotlin]]
- [[#forth][Forth]]
- [[#lua][Lua]]
- [[#ledger--hledger][Ledger / Hledger]]
- [[#markdown][Markdown]]
- [[#json][JSON]]
- [[#yaml][YAML]]
- [[#jinja2][Jinja2]]
- [[#docker-1][Docker]]
- [[#restclient][restclient]]
- [[#terraform][terraform]]
- [[#graphviz][graphviz]]
- [[#protobuf][protobuf]]
- [[#sql][SQL]]
- [[#plantuml][PlantUML]]
- [[#common-lisp][Common Lisp]]
- [[#look-and-feel][Look and Feel]]
- [[#remove-the-clutter][Remove the clutter]]
- [[#beacon-mode][beacon-mode]]
- [[#fonts][Fonts]]
- [[#custom-input-font][Custom Input font]]
- [[#variable-pitch-fonts-in-org-mode][Variable-pitch fonts in org-mode]]
- [[#align-org-mode-tables-with-variable-pitch-fonts][Align org-mode tables with variable-pitch fonts]]
- [[#hi-dpi][Hi-DPI]]
- [[#color-theme][Color theme]]
- [[#emacs-modeline][Emacs modeline]]
- [[#misc-2][Misc]]
- [[#quick-access][quick access]]
- [[#configure-path][Configure PATH]]* Top-level
** Flake
This repository is nix flakes–compatible.The following goes to ~flake.nix~ file.
#+begin_src nix :tangle flake.nix :noweb no-export :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-24.11";
};<>
};outputs = { self, ... }@inputs:
let
# Flakes are evaluated hermetically, thus are unable to access
# host environment (including looking up current system).
#
# That's why flakes must explicitly export sets for each system
# supported.
systems = ["x86_64-linux" "aarch64-linux" "aarch64-darwin"];# genAttrs applies f to all elements of a list of strings, and
# returns an attrset { name -> result }
#
# Useful for generating sets for all systems or hosts.
genAttrs = list: f: inputs.nixpkgs.lib.genAttrs list f;# Generate pkgs set for each system. This takes into account my
# nixpkgs config (allowUnfree) and my overlays.
pkgsBySystem =
let mkPkgs = system: import inputs.nixpkgs {
inherit system;
overlays = self.overlays.${system};
config = {
allowUnfree = true;
input-fonts.acceptLicense = true;
};
};
in genAttrs systems mkPkgs;# genHosts takes an attrset { name -> options } and calls mkHost
# with options+name. The result is accumulated into an attrset
# { name -> result }.
#
# Used in NixOS and Home Manager configurations.
genHosts = hosts: mkHost:
genAttrs (builtins.attrNames hosts) (name: mkHost ({ inherit name; } // hosts.${name}));# merges a list of attrsets into a single attrset
mergeSections = inputs.nixpkgs.lib.foldr inputs.nixpkgs.lib.mergeAttrs {};in mergeSections [
<>
<>
<>
<>
<>
];
}
#+end_srcNix flakes are still an experimental feature, so you need the following in NixOS configuration to enable it.
#+name: nixos-section
#+begin_src nix
{
nix = {
extraOptions = ''
experimental-features = nix-command flakes
'';
};
}
#+end_srcFor nix-darwin systems:
#+name: darwin-section
#+begin_src nix
{
nix = {
extraOptions = ''
experimental-features = nix-command flakes
'';
};
}
#+end_srcFor 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:
#+name: flake-inputs
#+begin_src nix
nixpkgs-unstable = {
type = "github";
owner = "NixOS";
repo = "nixpkgs";
ref = "nixpkgs-unstable";
};
#+end_srcAdd overlay:
#+name: flake-overlays
#+begin_src nix
(final: prev: {
unstable = import inputs.nixpkgs-unstable {
inherit system;
overlays = self.overlays.${system};
config = { allowUnfree = true; };
};
})
#+end_src
** NixOS
Expose NixOS configurations.
#+name: flake-outputs-nixos
#+begin_src nix
(let
nixosHosts = {
omicron = { system = "x86_64-linux"; config = ./nixos-config.nix; };# pie uses a separate config as it is very different
# from other hosts.
pie = { system = "aarch64-linux"; config = ./pie.nix; };
};mkNixosConfiguration = { name, system, config }:
let pkgs = pkgsBySystem.${system};
in inputs.nixpkgs.lib.nixosSystem {
inherit system;
modules = [
{ nixpkgs = { inherit pkgs; }; }
(import config)
];
specialArgs = { inherit name inputs; };
};in {
nixosConfigurations = genHosts nixosHosts mkNixosConfiguration;
})
#+end_src
** Home manager
Add home-manager to flake inputs.
#+name: flake-inputs
#+begin_src nix
home-manager = {
type = "github";
owner = "rycee";
repo = "home-manager";
ref = "release-24.11";
inputs.nixpkgs.follows = "nixpkgs";
};
#+end_srcExpose home-manager configurations.
#+name: flake-outputs-home-manager
#+begin_src nix
(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.stateVersion = "21.05";
};
homeManagerConfigurations = genHosts homeManagerHosts mkHomeManagerConfiguration;
})
#+end_src#+name: home-manager-section
#+begin_src nix
{
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_srcIntegrate home-manager module into NixOS.
#+name: nixos-section
#+begin_src nix
{
imports = [inputs.home-manager.nixosModules.home-manager];
home-manager = {
useUserPackages = true;
useGlobalPkgs = true;
users.rasen = inputs.self.lib.home-manager-common;
};
}
#+end_srcIntegrate home-manager into nix-darwin.
#+name: darwin-section
#+begin_src nix
{
imports = [inputs.home-manager.darwinModules.home-manager];
home-manager = {
useUserPackages = false;
useGlobalPkgs = true;
users.rasen = { ... }: {
imports = [
inputs.self.lib.home-manager-common
{ hostname = config.networking.hostName; }
];
};
# users.rasen = inputs.self.lib.home-manager-common;
};
}
#+end_src** nix-darwin
Add nix-darwin to flake inputs.
#+name: flake-inputs
#+begin_src nix
darwin = {
url = "github:lnl7/nix-darwin/nix-darwin-24.11";
inputs.nixpkgs.follows = "nixpkgs";
};
#+end_srcAdd helper function to differentiate linux-only config:
#+name: flake-overlays
#+begin_src nix
(final: prev: {
lib = prev.lib // {
linux-only = prev.lib.mkIf final.stdenv.isLinux;
};
})
#+end_src#+name: flake-outputs-nix-darwin
#+begin_src nix
(let
darwinHosts = {
<>
};mkDarwinConfiguration = { name, system, modules ? [] }:
inputs.darwin.lib.darwinSystem {
inherit system;
modules = modules ++ [
{ networking.hostName = name; }
self.darwin-common
];
};in {
darwin-common = { lib, pkgs, config, ... }: {
imports = [
<>
];
};darwinConfigurations = genHosts darwinHosts mkDarwinConfiguration;
})
#+end_src#+name: darwin-section
#+begin_src nix
{
nixpkgs.config = {
allowUnfree = true;
};
nixpkgs.overlays = self.overlays.aarch64-darwin;
services.nix-daemon.enable = true;system.stateVersion = 4;
}
#+end_srcAnd enable this in home-manager ([[https://github.com/nix-community/home-manager/blob/master/modules/targets/darwin/linkapps.nix][home-manager/linkapps.nix at master · nix-community/home-manager · GitHub]]):
#+name: home-manager-section
#+begin_src nix
({ config, lib, pkgs, ... }:{
config = lib.mkIf pkgs.stdenv.hostPlatform.isDarwin {
# Install MacOS applications to the user environment.
home.file."Applications/Home Manager Apps".source = let
apps = pkgs.buildEnv {
name = "home-manager-applications";
paths = config.home.packages;
pathsToLink = "/Applications";
};
in "${apps}/Applications";
};
})
#+end_src
** Packages
Generate packages set for each supported system.
#+name: flake-outputs-packages
#+begin_src nix
(let
mkPackages = system:
let
pkgs = pkgsBySystem.${system};
in
mergeSections [
<>
];in {
packages = genAttrs systems mkPackages;
})
#+end_src
** Overlays
Generate overlays for all supported systems.
#+name: flake-outputs-overlays
#+begin_src nix
(let
mkOverlays = system: [
# mix-in all local packages, so they are available as pkgs.${packages-name}
(final: prev: self.packages.${system})<>
];
in {
overlays = genAttrs systems mkOverlays;
})
#+end_src~<>~ 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 :padline no
#
# This file is auto-generated from "README.org"
#
{ name, config, pkgs, lib, inputs, ... }:
let
machine-config = lib.getAttr name {
omicron = [
<>
];
};in
{
imports = [
{
nixpkgs.config.allowUnfree = true;# The NixOS release to be compatible with for stateful data such as databases.
system.stateVersion = "21.05";
}<>
] ++ machine-config;
}
#+end_srcThis =<>= is replaced by other parts of this doc.
** Re-expose nixpkgs
#+name: nixos-section
#+begin_src nix
{
# 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;
};
}
#+end_srcSame but for Home Manager–managed host.
#+name: home-manager-section
#+begin_src nix
{
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);
}
];
};
}
#+end_src
** Sandbox
Build all packages in sandbox:
#+name: nixos-section
#+begin_src nix
{
nix.settings.sandbox = true;
}
#+end_src
** Users
I'm the only user of the system:#+name: nixos-section
#+begin_src nix
{
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=.
** Machines
* macOS
** Users
#+name: darwin-section
#+begin_src nix
{
users.users.rasen = {
description = "Oleksii Shmalko";
home = "/Users/rasen/";
};
}
#+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_srcorg-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
#+name: darwin-section
#+begin_src nix
{
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_srcSkhd integration to move between windows.
[https://gist.github.com/ethan-leba/760054f36a2f7c144c6b06ab6458fae6]
#+name: darwin-section
#+begin_src nix
{
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 ; defaultdefault < 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_srcEmacs 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
* Machines
** omicron
This is my small Dell XPS 13 running NixOS.
#+name: machine-omicron
#+begin_src nix
{
imports = [
(import "${inputs.nixos-hardware}/dell/xps/13-9360")
inputs.nixpkgs.nixosModules.notDetected
];boot.initrd.availableKernelModules = [ "xhci_pci" "nvme" "usb_storage" "sd_mod" "rtsx_pci_sdmmc" ];
boot.kernelModules = [ "kvm-intel" "wl" ];
boot.extraModulePackages = [ config.boot.kernelPackages.rtl88x2bu config.boot.kernelPackages.broadcom_sta ];hardware.opengl = {
enable = true;
extraPackages = [
pkgs.vaapiIntel
pkgs.vaapiVdpau
pkgs.libvdpau-va-gl
];
};nix.settings.max-jobs = lib.mkDefault 4;
# powerManagement.cpuFreqGovernor = "powersave";
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
}
#+end_src~inputs.nixos-hardware~ comes from the following flake input.
#+name: flake-inputs
#+begin_src nix
nixos-hardware = {
type = "github";
owner = "NixOS";
repo = "nixos-hardware";
flake = false;
};
#+end_srcLVM on LUKS setup for disk encryption.
#+name: machine-omicron
#+begin_src nix
{
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_srcClickpad:
#+name: machine-omicron
#+begin_src nix
{
services.xserver.libinput = {
enable = true;
touchpad.accelSpeed = "0.7";
};
}
#+end_srcFix screen tearing ([[https://wiki.archlinux.org/index.php/intel_graphics#Tearing][ArchWiki]]):
#+name: machine-omicron
#+begin_src nix
{
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.
#+name: darwin-host
#+begin_src nix
bayraktar = {
system = "aarch64-darwin";
};
#+end_src*** Work email
#+name: home-manager-section
#+begin_src nix
{
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* Emacs
** Install Emacs
#+name: install-emacsI use emacs from [[https://github.com/nix-community/emacs-overlay][emacs-overlay]].
#+name: flake-inputs
#+begin_src nix
emacs-overlay = {
type = "github";
owner = "nix-community";
repo = "emacs-overlay";
};
#+end_srcUse overlay (~<>~).
#+name: flake-overlays
#+begin_src nix
inputs.emacs-overlay.overlay
#+end_srcExpose Emacs with my packages as a top-level package (~<>~).
#+name: flake-packages
#+begin_src nix
(let
emacs-base =
if pkgs.stdenv.isDarwin
then pkgs.emacs.overrideAttrs (old: {
patches =
(old.patches or [])
++ [
# Fix OS window role so that yabai can pick up emacs
(pkgs.fetchpatch {
url = "https://raw.githubusercontent.com/d12frosted/homebrew-emacs-plus/master/patches/emacs-28/fix-window-role.patch";
sha256 = "sha256-+z/KfsBm1lvZTZNiMbxzXQGRTjkCFO4QPlEK35upjsE=";
})
# Use poll instead of select to get file descriptors
# (pkgs.fetchpatch {
# url = "https://raw.githubusercontent.com/d12frosted/homebrew-emacs-plus/master/patches/emacs-29/poll.patch";
# sha256 = "sha256-jN9MlD8/ZrnLuP2/HUXXEVVd6A+aRZNYFdZF8ReJGfY=";
# })
# Enable rounded window with no decoration
(pkgs.fetchpatch {
url = "https://raw.githubusercontent.com/d12frosted/homebrew-emacs-plus/master/patches/emacs-29/round-undecorated-frame.patch";
sha256 = "sha256-uYIxNTyfbprx5mCqMNFVrBcLeo+8e21qmBE3lpcnd+4=";
})
# Make emacs aware of OS-level light/dark mode
(pkgs.fetchpatch {
url = "https://raw.githubusercontent.com/d12frosted/homebrew-emacs-plus/master/patches/emacs-28/system-appearance.patch";
sha256 = "sha256-oM6fXdXCWVcBnNrzXmF0ZMdp8j0pzkLE66WteeCutv8=";
})
];
configureFlags =
(old.configureFlags or [])
++ [
"LDFLAGS=-headerpad_max_install_names"
];
})
else pkgs.emacs.override {
withX = true;
# select lucid toolkit
toolkit = "lucid";
withGTK2 = false; withGTK3 = false;
};
emacs-packages = (epkgs:
(with epkgs.melpaPackages; [activity-watch-mode
aggressive-indent
atomic-chrome
avy
bash-completion
beacon
blacken
cider
clojure-mode
cmake-mode
color-identifiers-mode
company
company-box
counsel
counsel-projectile
dart-mode
diff-hl
diminish
direnv
dockerfile-mode
doom-modeline
dtrt-indent
edit-indirect
el-patch
elpy
emojify
envrc
epresent
evil
evil-collection
evil-numbers
evil-org
evil-surround
evil-swap-keys
exec-path-from-shell
expand-region
fish-completion
fish-mode
flycheck
flycheck-inline
flycheck-jest
flycheck-rust
forge
forth-mode
just-mode
justl
general
go-mode
google-translate
gptel
graphviz-dot-mode
groovy-mode
haskell-mode
imenu-list
ivy
ivy-bibtex
ivy-pass
jinja2-mode
js2-mode
json-mode
ledger-mode
lispyville
lsp-haskell
lsp-mode
lsp-ui
lua-mode
magit
markdown-mode
modus-themes
nix-mode
nix-sandbox
notmuch
ol-notmuch
org-cliplink
org-download
org-drill
org-ref
org-roam
org-roam-bibtex
org-super-agenda
paren-face
pass
php-mode
pip-requirements
plantuml-mode
prettier-js
projectile
protobuf-mode
psc-ide
purescript-mode
py-autopep8
racer
racket-mode
restclient
rjsx-mode
ryo-modal
god-mode
multiple-cursors
rust-mode
slime
smex
spaceline
svelte-mode
swift-mode
terraform-mode
tide
toc-org
typescript-mode
visual-fill-column
vterm
vue-mode
w3m
web-mode
wgrep
which-key
whitespace-cleanup-mode
writegood-mode
yaml-mode
yasnippet
zig-modecorfu
cape
vertico
orderless
consult
embark
marginalia
smartparens
git-link]) ++
[
epkgs.elpaPackages.org
epkgs.nongnuPackages.org-contrib
epkgs.elpaPackages.adaptive-wrap
epkgs.exwm
# not available in melpa
epkgs.elpaPackages.valignepkgs.elpaPackages.eglot
(epkgs.trivialBuild rec {
pname = "org-roam-ui";
version = "20210830";
src = pkgs.fetchFromGitHub {
owner = "org-roam";
repo = "org-roam-ui";
rev = "9ad111d2102c24593f6ac012206bb4b2c9c6c4e1";
sha256 = "sha256-x6notv/U+y9Es8m58R/Qh7GEAtRqXqXvr7gy5OiDDUM=";
};
packageRequires = [
epkgs.melpaPackages.f
epkgs.melpaPackages.org-roam
epkgs.melpaPackages.websocket
epkgs.melpaPackages.simple-httpd
];postInstall = ''
cp -r ./out/ $LISPDIR/
'';meta = {
description = "A graphical frontend for exploring your org-roam Zettelkasten";
license = pkgs.lib.licenses.gpl3;
};
})(epkgs.trivialBuild rec {
pname = "org-fc";
version = "20201121";
src = pkgs.fetchFromGitHub {
# owner = "rasendubi";
# repo = "org-fc";
# rev = "35ec13fd0412cd17cbf0adba7533ddf0998d1a90";
# sha256 = "sha256-2h1dIR7WHYFsLZ/0D4HgkoNDxKQy+v3OaiiCwToynvU=";
owner = "l3kn";
repo = "org-fc";
rev = "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.sqlitepkgs.notmuch
pkgs.w3m
pkgs.imagemagick
pkgs.shellcheck(pkgs.python3.withPackages (pypkgs: [
pypkgs.autopep8
pypkgs.black
pypkgs.flake8
pypkgs.mypy
pypkgs.pylint
pypkgs.virtualenv
]))(pkgs.aspellWithDicts (dicts: with dicts; [en en-computers en-science uk]))
# latex for displaying fragments in org-mode
(pkgs.texlive.combine {
inherit (pkgs.texlive)
scheme-small
dvipng
dvisvgm
mhchem # chemistry
tikz-cd # category theory diagrams
# required for org export
wrapfig
capt-of
;
})
pkgs.ghostscript
]
);overrides = self: super: {
# select org from elpa
org = super.elpaPackages.org;
};emacs-final = ((pkgs.emacsPackagesFor emacs-base).overrideScope overrides).emacsWithPackages emacs-packages;
in {
my-emacs = emacs-final // {
base = emacs-base;
overrides = overrides;
packages = emacs-packages;
};
})
#+end_srcInstall Emacs with Home manager (~<>~)
#+name: home-manager-section
#+begin_src nix
{
programs.emacs = {
enable = true;
package = pkgs.my-emacs.base;
extraPackages = pkgs.my-emacs.packages;
overrides = pkgs.my-emacs.overrides;
};
# services.emacs.enable = true;# fonts used by emacs
home.packages = [
pkgs.input-mono
pkgs.libertine
];
}
#+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
;;
;; 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)<>
(org-babel-load-file (expand-file-name "README.org" rasen/dotfiles-directory))
#+end_srcAnother 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
;;;
;;; This file is auto-generated from "README.org"
;;;
#+end_src
** Patch ob-tangle
#+name: patch-ob-tangle
*This patch is critical to getting this config working. Without it, org-babel will tangle this file incorrectly*This patches ob-tangle to allow defining sections with the same name multiple times. All sections with the same name are concatenated. (This was the default behavior some time ago, so this restores it.)
(~<>~)
#+name: ob-tangle-patch
#+begin_src emacs-lisp :noweb no
(require 'el-patch)
;; org-babel fixes to tangle ALL matching sections
(defun rasen/map-regex (regex fn)
"Map the REGEX over the BUFFER executing FN.FN is called with the match-data of the regex.
Returns the results of the FN as a list."
(save-excursion
(goto-char (point-min))
(let (res)
(save-match-data
(while (re-search-forward regex nil t)
(let ((f (match-data)))
(setq res
(append res
(list
(save-match-data
(funcall fn f))))))))
res)))(el-patch-feature ob-core)
(el-patch-defun org-babel-expand-noweb-references (&optional info parent-buffer)
"Expand Noweb references in the body of the current source code block.For example the following reference would be replaced with the
body of the source-code block named `example-block'.<>
Note that any text preceding the <> construct on a line will
be interposed between the lines of the replacement text. So for
example if <> is placed behind a comment, then the entire
replacement text will also be commented.This function must be called from inside of the buffer containing
the source-code block which holds BODY.In addition the following syntax can be used to insert the
results of evaluating the source-code block named `example-block'.<>
Any optional arguments can be passed to example-block by placing
the arguments inside the parenthesis following the convention
defined by `org-babel-lob'. For example<>
would set the value of argument \"a\" equal to \"9\". Note that
these arguments are not evaluated in the current source-code
block but are passed literally to the \"example-block\"."
(let* ((parent-buffer (or parent-buffer (current-buffer)))
(info (or info (org-babel-get-src-block-info 'light)))
(lang (nth 0 info))
(body (nth 1 info))
(comment (string= "noweb" (cdr (assq :comments (nth 2 info)))))
(noweb-re (format "\\(.*?\\)\\(%s\\)"
(with-current-buffer parent-buffer
(org-babel-noweb-wrap))))
(cache nil)
(c-wrap
(lambda (s)
;; Comment string S, according to LANG mode. Return new
;; string.
(unless org-babel-tangle-uncomment-comments
(with-temp-buffer
(funcall (org-src-get-lang-mode lang))
(comment-region (point)
(progn (insert s) (point)))
(org-trim (buffer-string))))))
(expand-body
(lambda (i)
;; Expand body of code represented by block info I.
(let ((b (if (org-babel-noweb-p (nth 2 i) :eval)
(org-babel-expand-noweb-references i)
(nth 1 i))))
(if (not comment) b
(let ((cs (org-babel-tangle-comment-links i)))
(concat (funcall c-wrap (car cs)) "\n"
b "\n"
(funcall c-wrap (cadr cs))))))))
(expand-references
(lambda (ref cache)
(pcase (gethash ref cache)
(`(,last . ,previous)
;; Ignore separator for last block.
(let ((strings (list (funcall expand-body last))))
(dolist (i previous)
(let ((parameters (nth 2 i)))
;; Since we're operating in reverse order, first
;; push separator, then body.
(push (or (cdr (assq :noweb-sep parameters)) "\n")
strings)
(push (funcall expand-body i) strings)))
(mapconcat #'identity strings "")))
;; Raise an error about missing reference, or return the
;; empty string.
((guard (or org-babel-noweb-error-all-langs
(member lang org-babel-noweb-error-langs)))
(error "Cannot resolve %s (see `org-babel-noweb-error-langs')"
(org-babel-noweb-wrap ref)))
(_ "")))))
(replace-regexp-in-string
noweb-re
(lambda (m)
(with-current-buffer parent-buffer
(save-match-data
(let* ((prefix (match-string 1 m))
(id (match-string 3 m))
(evaluate (string-match-p "(.*)" id))
(expansion
(cond
(evaluate
;; Evaluation can potentially modify the buffer
;; and invalidate the cache: reset it.
(setq cache nil)
(let ((raw (org-babel-ref-resolve id)))
(if (stringp raw) raw (format "%S" raw))))
;; Retrieve from the Library of Babel.
((nth 2 (assoc-string id org-babel-library-of-babel)))
;; Return the contents of headlines literally.
((org-babel-ref-goto-headline-id id)
(org-babel-ref-headline-body))
;; Look for a source block named SOURCE-NAME. If
;; found, assume it is unique; do not look after
;; `:noweb-ref' header argument.
((org-with-point-at 1
(let ((r (org-babel-named-src-block-regexp-for-name id)))
(and (re-search-forward r nil t)
(not (org-in-commented-heading-p))
(el-patch-swap
(funcall expand-body
(org-babel-get-src-block-info t))
(mapconcat
#'identity
(rasen/map-regex r
(lambda (md)
(funcall expand-body
(org-babel-get-src-block-info t))))
"\n"))))))
;; All Noweb references were cached in a previous
;; run. Extract the information from the cache.
((hash-table-p cache)
(funcall expand-references id cache))
;; Though luck. We go into the long process of
;; checking each source block and expand those
;; with a matching Noweb reference. Since we're
;; going to visit all source blocks in the
;; document, cache information about them as well.
(t
(setq cache (make-hash-table :test #'equal))
(org-with-wide-buffer
(org-babel-map-src-blocks nil
(if (org-in-commented-heading-p)
(org-forward-heading-same-level nil t)
(let* ((info (org-babel-get-src-block-info t))
(ref (cdr (assq :noweb-ref (nth 2 info)))))
(push info (gethash ref cache))))))
(funcall expand-references id cache)))))
;; Interpose PREFIX between every line.
(mapconcat #'identity
(split-string expansion "[\n\r]")
(concat "\n" prefix))))))
body t t 2)))
#+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 <>.)
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** General (package)
I use [[https://github.com/noctuid/general.el][general]] to define my keybindings. (emacs-lisp)
#+begin_src emacs-lisp
<>(use-package general)
;; Definer for my leader
(general-create-definer --leader-def :prefix "SPC")
(general-create-definer --s-leader-def :keymaps '(motion insert emacs) :prefix "s-SPC" :non-normal-prefix "s-SPC");; Extra-hackery to define key with multiple prefixes
(defmacro leader-def (&rest args)
(declare (indent defun))
`(progn (--leader-def ,@args)
(--s-leader-def ,@args)));; Definer for my leader + applied globally across all windows.
(general-create-definer s-leader-def
:keymaps '(motion emacs insert) :prefix "SPC"
:non-normal-prefix "s-SPC"
:global-prefix "s-SPC")(s-leader-def "" nil)
(with-eval-after-load "evil"
;; free-up prefix
(s-leader-def :keymaps '(motion normal visual) "" nil))
#+end_src** General keybindings
I use a custom modal mode.
#+begin_src emacs-lisp
(use-package ryo-modal
:disabled t
:config
(with-eval-after-load 'org
(setq org-support-shift-select 'always))(defun rasen/shift-translate ()
"Make the following commands behave as if shift-translated."
(interactive)
(setq this-command-keys-shift-translated t))(defun rasen/dwim-beginning-of-line-paragraph ()
(interactive "^")
(let ((orig-point (point)))
(if (eq orig-point (line-beginning-position))
(backward-paragraph)
(back-to-indentation)
(when (= orig-point (point))
(move-beginning-of-line 1)))))(defun rasen/extend-beginning-of-line-paragraph ()
(interactive)
(let ((this-command-keys-shift-translated t))
(call-interactively 'rasen/dwim-beginning-of-line-paragraph)))(defun rasen/dwim-end-of-line-paragraph ()
(interactive "^")
(let ((orig-point (point)))
(if (eq orig-point (line-end-position))
(forward-paragraph)
(end-of-line))))
(defun rasen/extend-end-of-line-paragraph ()
(interactive)
(let ((this-command-keys-shift-translated t))
(call-interactively 'rasen/dwim-end-of-line-paragraph)))(let ((keys
'(("n" "")
("N" "S-")
("o" "")
("O" "S-")
("u" "")
("U" "S-")
("e" "")
("E" "S-")
("f" "M-b")
("F" "M-S-b")
("p" "M-f")
("P" "M-S-f"))))
(mapc (lambda (it)
(define-key ryo-modal-mode-map (kbd (car it)) (kbd (cadr it))))
keys))
(global-set-key (kbd "") (defun rasen/enable-ryo ()
(interactive)
(ryo-modal-mode 1)))(define-key ryo-modal-mode-map (kbd "Y") nil)
(ryo-modal-keys
("t" ryo-modal-mode)
("y" rasen/dwim-beginning-of-line-paragraph)
("Y" rasen/dwim-beginning-of-line-paragraph :first '(rasen/shift-translate))
("i" rasen/dwim-end-of-line-paragraph)
("I" rasen/dwim-end-of-line-paragraph :first '(rasen/shift-translate))))
#+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_srcDon'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_srcDon't clutter the current directory with auto-save files.
#+begin_src emacs-lisp
(setq auto-save-file-name-transforms '((".*" "~/.emacs.d/backups/" t)))
#+end_srcDo 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
** 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_srcInsert 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))))(general-def 'insert '(git-commit-mode-map ivy-minibuffer-map)
"C-c ." #'rasen/insert-date)
(general-def 'ivy-minibuffer-map
"C-c ." #'rasen/insert-date)(general-def 'normal 'org-mode-map
"RET ." #'rasen/insert-date)
#+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_srcShamelessly 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
** ivy
#+begin_src emacs-lisp
(use-package ivy
:disabled t
:demand
:general
(s-leader-def
"b" #'ivy-switch-buffer)
:diminish ivy-mode
:config
<>
(ivy-mode 1)
)
#+end_srcDo not start input with =^= and ignore the case.
#+name: ivy-config
#+begin_src emacs-lisp :tangle no
(setq-default ivy-initial-inputs-alist nil)
(setq-default ivy-re-builders-alist '((t . ivy--regex-ignore-order)))
#+end_srcDo not show ~./~ and ~../~ during file name completion.
#+name: ivy-config
#+begin_src emacs-lisp :tangle no
(setq-default ivy-extra-directories nil)
#+end_srcThe normal =C-j= is not placed conveniently on Workman layout, so move its function to =C-e= (which is qwerty =k=).
#+name: ivy-config
#+begin_src emacs-lisp :tangle no
(general-def 'ivy-minibuffer-map
"C-e" #'ivy-alt-done
"C-M-e" #'ivy-immediate-done)
#+end_srcEvilify ivy-occur.
#+name: ivy-config
#+begin_src emacs-lisp :tangle no
(with-eval-after-load "evil"
(general-def
:keymaps '(ivy-occur-mode-map ivy-occur-grep-mode-map)
:states 'normal
"k" #'ivy-occur-next-line
"j" #'ivy-occur-previous-line
"C-n" #'ivy-occur-next-line
"C-p" #'ivy-occur-previous-line
"RET" #'ivy-occur-press-and-switch
"TAB" #'ivy-occur-press
"C-e" #'ivy-occur-press-and-switch
"g r" #'ivy-occur-revert-buffer
"g g" #'evil-goto-first-line
"d" #'ivy-occur-delete-candidate
"r" #'read-only-mode
"a" #'ivy-occur-read-action
"c" #'ivy-occur-toggle-calling
"f" #'ivy-occur-press
"o" #'ivy-occur-dispatch
"q" #'quit-window)(general-def 'normal 'ivy-occur-grep-mode-map
"w" #'ivy-wgrep-change-to-wgrep-mode))
#+end_src** smex
I use smex for improved =counsel-M-x= (show most frequently used commands first).
#+begin_src emacs-lisp
(use-package smex
:config
:disabled t
(smex-initialize))
#+end_src
** counsel
#+begin_src emacs-lisp
(use-package counsel
:demand
:disabled t
:diminish counsel-mode
:general
(s-leader-def
"x" #'counsel-M-x
"f" #'counsel-find-file)
('motion
"g r" #'counsel-git-grep
"g /" #'counsel-rg)
('read-expression-map
"C-r" #'counsel-expression-history)
:config
;; reset ivy initial inputs for counsel
(setq-default ivy-initial-inputs-alist nil)
(counsel-mode 1))
#+end_srcInstall ripgrep (rg) to user environment:
#+name: home-manager-section
#+begin_src nix
{
home.packages = [ pkgs.ripgrep ];
}
#+end_src
** avy
Jump anywhere with a few keystrokes in tree-like way.
#+begin_src emacs-lisp
(use-package avy
:general
('motion
"K" #'avy-goto-char)
:custom
;; easy workman keys (excluding pinky)
(avy-keys '(?s ?h ?t ?n ?e ?o ?d ?r ?u ?p)))
#+end_src
** imenu / imenu-list
Use imenu to jump to symbols in the current buffer.
#+begin_src emacs-lisp
(use-package imenu-list
:disabled t
:general
(:keymaps 'imenu-list-major-mode-map
:states 'normal
"RET" #'imenu-list-goto-entry
"TAB" #'imenu-list-display-entry
"" #'hs-toggle-hiding
"g r" #'imenu-list-refresh
"q" #'imenu-list-quit-window)(defun rasen/imenu-or-list (arg)
"Invoke `counsel-imenu'. If prefix is provided, toggle imenu-list"
(interactive "P")
(if arg
(imenu-list-smart-toggle)
(counsel-imenu)))(leader-def 'motion "g" #'rasen/imenu-or-list))
#+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))
(global-whitespace-mode))
#+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
** which-key
[[https://github.com/justbur/emacs-which-key][which-key]] is a minor mode for Emacs that displays the key bindings following your currently entered incomplete command (a prefix) in a popup.
#+begin_src emacs-lisp
(use-package which-key
:disabled t
:defer 2
:diminish which-key-mode
:config
(which-key-mode))
#+end_src
** Google translate
#+begin_src emacs-lisp
(use-package google-translate
:disabled t
:general
('normal
'(markdown-mode-map org-mode-map)
"z g t" #'rasen/google-translate-at-point
"z g T" #'google-translate-smooth-translate):commands (google-translate-smooth-translate)
:config
(defun rasen/google-translate-at-point (arg)
"Translate word at point. If prefix is provided, do reverse translation"
(interactive "P")
(if arg
(google-translate-at-point-reverse)
(google-translate-at-point)))(require 'google-translate-default-ui)
(require 'google-translate-smooth-ui)
(setq google-translate-show-phonetic t)(setq google-translate-default-source-language "en"
google-translate-default-target-language "ua")(setq google-translate-translation-directions-alist '(("en" . "ua") ("ua" . "en")))
;; auto-toggle input method
(setq google-translate-input-method-auto-toggling t
google-translate-preferable-input-methods-alist '((nil . ("en"))
(ukrainian-computer . ("ua")))))
#+end_src
** tab-bar-mode
New in Emacs 27.
#+begin_src emacs-lisp
(use-package tab-bar
:general
('motion
"M-h" #'tab-bar-switch-to-prev-tab
"M-l" #'tab-bar-switch-to-next-tab)
:config
(general-def 'normal
"M-h" #'tab-bar-switch-to-prev-tab
"M-l" #'tab-bar-switch-to-next-tab)
(general-def '(normal visual) 'org-mode-map
"M-h" nil
"M-l" nil)
(general-def '(normal visual) 'evil-org-mode-map
"M-h" nil
"M-l" nil)
(general-def 'org-mode-map
"M-h" nil
"M-l" nil)(setq tab-bar-select-tab-modifiers '(meta))
(setq tab-bar-tab-hints t);; Show tab bar only if there are >1 tab
(setq tab-bar-show 1)
;; Do not show buttons
(setq tab-bar-close-button-show nil
tab-bar-new-button-show nil)
;; (tab-bar-mode)
)
#+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
** 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_srcAutomatically 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. (~<>~)
#+name: nixos-section
#+begin_src nix
{
environment.systemPackages = [ pkgs.xorg.xhost ];
services.xserver.windowManager.session = lib.singleton {
name = "exwm";
start = ''
xhost +SI:localuser:$USER
exec emacs
'';
# exec ${pkgs.my-emacs}/bin/emacsclient -a "" -c
};
services.xserver.displayManager.lightdm.enable = true;
# services.xserver.displayManager.startx.enable = true;
services.xserver.displayManager.defaultSession = "none+exwm";
}
#+end_srcInitialize 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)))(with-eval-after-load 'evil
(evil-set-initial-state 'exwm-mode 'motion));; do not forward anything besides keys defined with
;; `exwm-input-set-key' and `exwm-mode-map'
(setq exwm-input-prefix-keys '())(exwm-enable))
#+end_srcAdd 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)
;; Alternatively, try general-setq (which calls customize handler)
(exwm-input--update-global-prefix-keys))
#+end_src
** Window management
Common key bindings. (emacs-lisp)
#+begin_src emacs-lisp
(use-package evil
:disabled t
:defer t
:commands (evil-window-split
evil-window-vsplit))(defun rasen/exwm-next-workspace ()
(interactive)
;; (let ((cur exwm-workspace-current-index)
;; (max exwm-workspace-number))
;; (exwm-workspace-switch (% (+ cur 1) max)))
(other-frame 1))(defun rasen/move-tab-other-frame ()
(interactive)
(tab-bar-move-tab-to-frame nil));; despite the fact s-SPC binds to nil, EXWM will add s-SPC to
;; global prefix key.
(exwm-input-set-key (kbd "s-SPC") nil)
(exwm-input-set-key (kbd "s-x") #'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 "") #'rasen/switch-to-previous-buffer)
(exwm-input-set-key (kbd "") #'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-.(general-def
:prefix-command 'rasen/tab-map
"T" #'tab-bar-mode
"1" #'tab-bar-select-tab
"2" #'tab-bar-select-tab
"3" #'tab-bar-select-tab
"4" #'tab-bar-select-tab
"5" #'tab-bar-select-tab
"6" #'tab-bar-select-tab
"7" #'tab-bar-select-tab
"8" #'tab-bar-select-tab
"9" #'tab-bar-select-tab
"t" #'tab-bar-new-tab
"n" #'tab-bar-switch-to-prev-tab
"o" #'tab-bar-switch-to-next-tab
">" #'tab-bar-move-tab-to-frame
"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 "") (lookup-key (current-global-map) (kbd "")))
(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 "") #'vterm)
(exwm-input-set-key (kbd "") #'rasen/terminal)(exwm-input-set-key (kbd "s-z") #'exwm-layout-toggle-mode-line)
(exwm-input-set-key (kbd "s-f") #'exwm-layout-toggle-fullscreen)
(exwm-input-set-key (kbd "s-C-SPC") #'exwm-floating-toggle-floating)(general-def 'exwm-mode-map
"C-c" nil ;; disable default bindings" v" #'counsel-describe-variable)
;; Without the next line, EXWM won't intercept necessary prefix keys
;; (if you rebind them after EXWM has started)
(exwm-input--update-global-prefix-keys)
#+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#+name: nixos-section
#+begin_src nix
{
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.#+name: home-manager-section
#+begin_src nix
{
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_srcBind ~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. (~<>~)
#+name: home-manager-section
#+begin_src nix
{
home.packages = pkgs.lib.linux-only [ pkgs.escrotum ];
}
#+end_srcBind 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 "") #'rasen/screenshot)
#+end_src
** Misc
I definitely use X server:
#+name: nixos-section
#+begin_src nix
{
services.xserver.enable = true;
}
#+end_srcUse English as my only supported locale:
#+name: nixos-section
#+begin_src nix
{
i18n.supportedLocales = [ "en_US.UTF-8/UTF-8" ];
}
#+end_srcSetup timezone:
#+name: nixos-section
#+begin_src nix
{
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 (~<>~).
#+name: nixos-section
#+begin_src nix
{
services.xserver.layout = "us,ua";
services.xserver.xkbVariant = "workman,";# Use same config for linux console
console.useXkbConfig = true;
}
#+end_srcSame setting but for Home Manager (~<>~)
#+name: home-manager-section
#+begin_src nix
{
home.keyboard = {
layout = "us,ua";
variant = "workman,";
};
}
#+end_srcMap 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.~<>~:
#+name: nixos-section
#+begin_src nix
{
services.xserver.xkbOptions = "grp:lctrl_toggle,grp_led:caps,ctrl:nocaps";
# services.xserver.xkbOptions = "grp:caps_toggle,grp_led:caps";
}
#+end_srcOn 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 (~<>~).
#+name: home-manager-section
#+begin_src nix
{
xsession.initExtra = ''
xkbcomp ${./Xkeymap} $DISPLAY
'';
}
#+end_srcOne 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
(defun rasen/set-xkb-layout ()
(interactive)
(rasen/autostart "xkbcomp ~/dotfiles/Xkeymap $DISPLAY"))(rasen/set-xkb-layout)
#+end_srcInstall xkbcomp to execute these commands. (~<>~)
#+name: home-manager-section
#+begin_src nix
{
home.packages = [ pkgs.xorg.xkbcomp ];
}
#+end_src*** Compose keys
Add some custom compose keys (~<>~):
#+name: home-manager-section
#+begin_src nix
{
home.file.".XCompose".text = ''
include "%L": "⇐" U21D0 # Leftwards Double Arrow
: "⇒" U21D2 # RIGHTWARDS DOUBLE ARROW
: "⇔" U21D4 # LEFT RIGHT DOUBLE ARROW
: "⇔" U21D4 # LEFT RIGHT DOUBLE ARROW
: "↔" U2194 # LEFT RIGHT ARROW# White Right Pointing Index
: "☞" U261E: "℃"
: "℉": "❌" # Cross Mark
: "́" # stress
: "⌀" U2300 # DIAMETER SIGN
: "⌀" U2300 # DIAMETER SIGN
: "√" U221A # SQUARE ROOT
<3> : "∛" U221B # CUBE ROOT
: "∀" U2200 # FOR ALL
: "∃" U2203 # THERE EXISTS
: "∊" U220A # SMALL ELEMENT OF
: "∂" U2202 # PARTIAL DIFFERENTIAL
: "∆" U2206 # INCREMENT, Laplace operator
: "∑" U2211 # N-ARY SUMMATION, Sigma
: "∫" U222B # INTEGRAL
: "−" U2212 # MINUS SIGN
: "≈" U2248 # ALMOST EQUAL TO
: "≈" U2248 # ALMOST EQUAL TO
: "‾" U023E # OVERLINE
: "≠" U2260 # NOT EQUAL TO
: "≠" U2260 # NOT EQUAL TO
: "≡" U2261 # IDENTICAL TO
: "≡" U2261 # IDENTICAL TO
: "≤" U2264 # LESS-THAN OR EQUAL TO
: "≥" U2265 # GREATER-THAN OR EQUAL TO
: "∞" # infty
<_> : "ᵢ" # subscript i
<^> : "ⁱ" # superscript i
<_> : "₋" # subscript minus
<^> : "⁻" # superscript minus
<_> : "₊" # subscript plus
<^> : "⁺" # superscript plus
: "∘" # ring (function compose) operator
: "∙" # dot operator
: "∝" # proportional to
: "∎" # q.e.d.
'';
}
#+end_src*** xcape
Make short press on left control behave as Escape (=<>=):
#+name: home-manager-section
#+begin_src nix
{
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
(~<>~)
#+name: nixos-section
#+begin_src nix
{
networking = {
hostName = name;networkmanager = {
enable = true;
wifi.powersave = false;
};# disable wpa_supplicant
wireless.enable = false;
};users.extraUsers.rasen.extraGroups = [ "networkmanager" ];
}
#+end_srcInstall network manager applet for user. (~<>~)
#+name: home-manager-section
#+begin_src nix
{
home.packages = pkgs.lib.linux-only [ pkgs.networkmanagerapplet ];
}
#+end_srcAuto-start ~nm-applet~ (emacs-lisp)
#+begin_src emacs-lisp
(rasen/autostart "nm-applet")
#+end_src
** SSH
(~<>~)
#+name: nixos-section
#+begin_src nix
{
services.openssh = {
enable = true;
passwordAuthentication = false;
};
}
#+end_src
*** Mosh
[[https://mosh.mit.edu/][Mosh (mobile shell)]] is a cool addition to ssh.
#+name: nixos-section
#+begin_src nix
{
programs.mosh.enable = true;
}
#+end_src
** dnsmasq
Use [[http://www.thekelleys.org.uk/dnsmasq/doc.html][dnsmasq]] as a DNS cache.(~<>~)
#+name: nixos-section
#+begin_src nix
{
services.dnsmasq = {
enable = true;# These are used in addition to resolv.conf
servers = [
"8.8.8.8"
"8.8.4.4"
];extraConfig = ''
interface=lo
bind-interfaces
listen-address=127.0.0.1
cache-size=1000no-negcache
'';
};
}
#+end_src
** Firewall
Enable firewall. This blocks all ports for ingress traffic and pings.(~<>~)
#+name: nixos-section
#+begin_src nix
{
networking.firewall = {
enable = true;
allowPing = false;connectionTrackingModules = [];
autoLoadConntrackHelpers = false;
};
}
#+end_src
* Services
** Locate
Update [[https://linux.die.net/man/1/locate][locate]] database daily.
#+name: nixos-section
#+begin_src nix
{
services.locate = {
enable = true;
localuser = "rasen";
};
}
#+end_src
** Gitolite
#+name: nixos-section
#+begin_src nix
{
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.#+name: nixos-section
#+begin_src nix
{
services.syncthing = {
enable = true;
user = "rasen";
dataDir = "/home/rasen/.config/syncthing";
configDir = "/home/rasen/.config/syncthing";
openDefaultPorts = true;
};
}
#+end_srcOn Darwin:
#+name: darwin-section
#+begin_src nix
{
environment.systemPackages = [ pkgs.syncthing ];
}
#+end_src
** Backup
I use borg for backups.#+name: machine-omicron
#+begin_src nix
(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/"
"/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_srcMount external drive when needed.
#+name: nixos-section
#+begin_src nix
{
# 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 + lorri
direnv allows having per-directory environment configuration. You can think of automatic virtualenv, but it's more general and supports unloading.(~<>~)
#+name: home-manager-section
#+begin_src nix
{
programs.direnv.enable = true;
programs.direnv.nix-direnv.enable = true;
services.lorri.enable = pkgs.stdenv.isLinux;
}
#+end_src# Enable Emacs integration. (emacs-lisp)
# #+begin_src emacs-lisp
# (use-package direnv
# :after exec-path-from-shell
# :config
# (direnv-mode))
# #+end_srcBetter (?) direnv integration via [[https://github.com/purcell/envrc][purcell/envrc]]. (emacs-lisp)
#+begin_src emacs-lisp
(use-package envrc
:after exec-path-from-shell
:hook (after-init . envrc-global-mode))
#+end_src
** VirtualBox
#+name: nixos-section
#+begin_src nix
{
virtualisation.virtualbox.host.enable = true;
users.extraGroups.vboxusers.members = ["rasen"];
}
#+end_src
* Hardware
** Do not suspend on AC
#+name: nixos-section
#+begin_src nix
{
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(~<>~)
#+name: home-manager-section
#+begin_src nix
{
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.
#+name: nixos-section
#+begin_src nix
{
hardware.acpilight.enable = true;
environment.systemPackages = [ pkgs.acpilight ];
users.extraUsers.rasen.extraGroups = [ "video" ];
}
#+end_srcFor Home Manager–managed hosts.
#+name: home-manager-section
#+begin_src nix
{
home.packages = pkgs.lib.linux-only [ pkgs.acpilight ];
}
#+end_srcBind it to keys (emacs-lisp).
#+begin_src emacs-lisp
(rasen/exwm-input-set-key (kbd "")
(lambda () (interactive) (rasen/start-command "xbacklight -inc 10")))
(rasen/exwm-input-set-key (kbd "")
(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).#+name: nixos-section
#+begin_src nix
{
services.redshift = {
enable = true;
};
location.provider = "geoclue2";
}
#+end_src
** PipeWire
Use PipeWire as audio server.
#+name: nixos-section
#+begin_src nix
{
# PipeWire requires pulseaudio to be disabled.
hardware.pulseaudio.enable = false;security.rtkit.enable = true;
services.pipewire = {
enable = true;
alsa.enable = true;
alsa.support32Bit = true;
pulse.enable = true;media-session.config.bluez-monitor.rules = [
{
# Matches all cards
matches = [ { "device.name" = "~bluez_card.*"; } ];
actions = {
"update-props" = {
"bluez5.reconnect-profiles" = [ "hfp_hf" "hsp_hs" "a2dp_sink" ];
# mSBC is not expected to work on all headset + adapter combinations.
"bluez5.msbc-support" = true;
# SBC-XQ is not expected to work on all headset + adapter combinations.
"bluez5.sbc-xq-support" = true;
};
};
}
{
matches = [
# Matches all sources
{ "node.name" = "~bluez_input.*"; }
# Matches all outputs
{ "node.name" = "~bluez_output.*"; }
];
actions = {
"node.pause-on-idle" = false;
};
}
];
};
}
#+end_src=pavucontrol= is PulseAudio Volume Control—a nice utility for controlling pulseaudio settings. (~<>~)
#+name: home-manager-section
#+begin_src nix
{
home.packages = pkgs.lib.linux-only [ pkgs.pavucontrol ];
}
#+end_src#+begin_src emacs-lisp
(defun rasen/pavucontrol ()
(interactive)
(rasen/switch-start "Pavucontrol" "pavucontrol"))
#+end_srcBind volume control commands. (emacs-lisp)
#+begin_src emacs-lisp
(rasen/exwm-input-set-key (kbd "")
(lambda () (interactive) (rasen/start-command "amixer set Master toggle")))
(rasen/exwm-input-set-key (kbd "")
(lambda () (interactive) (rasen/start-command "amixer set Master 2%+")))
(rasen/exwm-input-set-key (kbd "")
(lambda () (interactive) (rasen/start-command "amixer set Master 2%-")))
#+end_src
** Bluetooth
I have a bluetooth headset, so this enables bluetooth audio in NixOS.(~<>~)
#+name: nixos-section
#+begin_src nix
{
hardware.bluetooth.enable = true;
}
#+end_src
** ADB
I need to access my Android device. (~<>~)
#+name: nixos-section
#+begin_src nix
{
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. (~<>~)
#+name: nixos-section
#+begin_src nix
{
services.fwupd.enable = true;
}
#+end_srcExecute the following command to update firmware.
#+begin_src sh :tangle no
fwupdmgr get-updates
#+end_src
* Browsers
Firefox is default, Chrome for backup.
#+name: home-manager-section
#+begin_src nix
{
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. (~<>~)
#+name: tridactylrc
#+begin_src fundamental :tangle no
" drop all existing configuration
sanitize tridactyllocal tridactylsyncbind J scrollline -10
bind K scrollline 10
bind j scrollline -2
bind k scrollline 2
#+end_srcLink tridactyl config to the place the tridactyl can find it. (~<>~)
#+name: home-manager-section
#+begin_src nix
{
xdg.configFile."tridactyl/tridactylrc".text = ''
<>
'';
}
#+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
* Evil-mode
** General
#+begin_src emacs-lisp
(use-package evil
:disabled t
:custom
(evil-undo-system 'undo-redo)
:config
<>
(evil-mode 1))
#+end_srcSwap =.= and =;=.
#+name: evil-config
#+begin_src emacs-lisp :tangle no
(general-def 'normal
";" #'evil-repeat
"." nil
"C-;" #'evil-repeat-pop
"C-." nil)(general-def 'motion
"." #'evil-repeat-find-char
";" nil
"g." #'goto-last-change
"g;" nil)
#+end_src#+name: evil-config
#+begin_src emacs-lisp :tangle no
(s-leader-def
";" #'eval-expression)
#+end_srcClose other window.
#+begin_src emacs-lisp
(defun rasen/quit-other ()
(interactive)
(other-window 1)
(quit-window))(s-leader-def
"q" #'rasen/quit-other)
#+end_srcMove to beginning/end of line with =H= and =L= respectively.
#+name: evil-config
#+begin_src emacs-lisp :tangle no
(defun rasen/smart-move-beginning-of-line (arg)
"Move point back to indentation of beginning of line.Move point to the first non-whitespace character on this line.
If point is already there, move to the beginning of the line.
Effectively toggle between the first non-whitespace character and
the beginning of the line.If ARG is not nil or 1, move forward ARG - 1 lines first. If
point reaches the beginning or end of the buffer, stop there."
(interactive "^p")
(setq arg (or arg 1));; Move lines first
(when (/= arg 1)
(let ((line-move-visual nil))
(forward-line (1- arg))))(let ((orig-point (point)))
(back-to-indentation)
(when (= orig-point (point))
(move-beginning-of-line 1))))(general-def 'motion
"H" #'rasen/smart-move-beginning-of-line
"L" #'evil-end-of-line)
#+end_srcSave buffer with =SPC SPC=.
#+begin_src emacs-lisp
(defun rasen/save-buffer (arg)
"Save current buffer. With PREFIX, save all buffers."
(interactive "P")
(if arg
(save-some-buffers)
(save-buffer)))(with-eval-after-load "evil"
(s-leader-def 'normal
"SPC" #'rasen/save-buffer)
(s-leader-def
"s-SPC" #'save-some-buffers))
#+end_src
** Swap k and j
With workman layout, =j= is located on qwerty =y= and =k=---on qwerty =n=; thus =j= is higher than =k=, and it is not convenient to press lower key for going up. Just swap them.
#+name: evil-config
#+begin_src emacs-lisp :tangle no
(general-def 'motion
"k" #'evil-next-visual-line
"j" #'evil-previous-visual-line
"gk" #'evil-next-line
"gj" #'evil-previous-line)(general-def 'operator
"k" #'evil-next-line
"j" #'evil-previous-line
"gk" #'evil-next-visual-line
"gj" #'evil-previous-visual-line)(general-def 'motion
"C-h" #'windmove-left
"C-k" #'windmove-down
"C-j" #'windmove-up
"C-l" #'windmove-right)(general-swap-key nil 'motion
"C-w j" "C-w k")
#+end_src
** evil-numbers
I use Vim's =C-a= and =C-x= (increment/decrement number at point) a lot.
=evil-numbers= provides that functionality for evil.
#+begin_src emacs-lisp
(use-package evil-numbers
:after evil
:general
('normal
"C-a" #'evil-numbers/inc-at-pt
"C-x" #'evil-numbers/dec-at-pt))
#+end_srcNow, remap =C-x= to =RET=. (Because =C-x= is used for decrementing numbers.)
#+name: evil-config
#+begin_src emacs-lisp
(general-def 'motion
"RET" (lookup-key (current-global-map) (kbd "C-x")))
;; Unmap it from magit
(general-def magit-file-mode-map
"C-x" nil)
#+end_src
** evil-collection
evil-collection is a collection of evil bindings for different modes.These variables need to be set before evil loads (which seems to be required by =general=) or shortly after. So the following needs to be placed before general:
#+name: before-general
#+begin_src emacs-lisp
(setq evil-want-integration t
evil-want-keybinding nil)
#+end_src#+begin_src emacs-lisp
(require 'warnings)
(add-to-list 'warning-suppress-types '(evil-collection))(use-package evil-collection
:disabled t
:after (evil)
:init
(setq evil-want-integration t
evil-want-keybinding nil)
:config
(defun rasen/rotate-keys (_mode mode-keymaps &rest _rest)
;; (evil-collection-translate-key 'normal mode-keymaps
;; "k" "j"
;; "j" "k"
;; "gk" "gj"
;; "gj" "gk"
;; (kbd "C-j") (kbd "C-k")
;; (kbd "C-k") (kbd "C-j")
;; (kbd "M-j") (kbd "M-k")
;; (kbd "M-k") (kbd "M-j")
;; "." ";"
;; ";" ".")
)
(add-hook 'evil-collection-setup-hook #'rasen/rotate-keys)(setq evil-collection-mode-list
'(dired
compile
flycheck
help
js2-mode
;; notmuch bindings aren't that cool and are less efficient than native
;; keymap
;; notmuch
magit
forge
python
racer
restclient
tide
typescript-mode
vterm
which-key
xref))(setq evil-collection-magit-use-y-for-yank t
evil-collection-magit-state 'normal)(evil-collection-init)
;; Evilify magit-blame.
(general-def 'normal magit-blame-read-only-mode-map
"k" #'evil-next-visual-line
"j" #'evil-previous-visual-line
"C-k" #'magit-blame-next-chunk
"C-j" #'magit-blame-previous-chunk
"gk" #'magit-blame-next-chunk-same-commit
"gj" #'magit-blame-previous-chunk-same-commit)(general-def magit-blame-read-only-mode-map "SPC" nil) ;; expose my leader
(general-def
:states `(,evil-collection-magit-state visual)
:keymaps 'magit-status-mode-map
"C-j" #'magit-section-backward-sibling
"C-k" #'magit-section-forward-sibling
"gj" #'magit-section-backward
"gk" #'magit-section-forward
"j" #'evil-previous-visual-line
"k" #'evil-next-visual-line))
#+end_src
** evil-surrond
#+begin_src emacs-lisp
(use-package evil-surround
:disabled t
:config
(global-evil-surround-mode t))
#+end_src
** calc
#+begin_src emacs-lisp
(use-package calc ; built-in
:general
(leader-def 'motion
"=" #'quick-calc
"+" #'calc)
('motion
"g =" #'quick-calc
"g +" #'calc))
#+end_src
** Evilify compile mode
#+begin_src emacs-lisp
(use-package compile ; built-in
:config
(setq compilation-scroll-output t))
#+end_srcAnd evil commands to go to navigate errors.
#+name: evil-config
#+begin_src emacs-lisp :tangle no
(leader-def 'motion
"," #'previous-error
"." #'next-error)
(general-def 'motion
"M-," #'previous-error
"M-." #'next-error)
#+end_src
** Evilify minibuffer
Not really "evilify."
#+begin_src emacs-lisp
(general-def 'minibuffer-local-map
;; Finish input with C-e ("e" in Workman is qwerty's "k")
"C-e" #'exit-minibuffer)
#+end_src
** Evilify shell mode
Default bindings for ~RET~ prevent many of my commands from working. Remap ~RET~ to ~C-RET~.
#+begin_src emacs-lisp
(general-def 'shell-mode-map
"RET" nil
"" #'comint-send-input)
#+end_src
** lispyville
#+begin_src emacs-lisp
(use-package lispyville
:disabled t
:hook
((clojure-mode emacs-lisp-mode lisp-mode scheme-mode) . lispyville-mode)
:config
(lispyville-set-key-theme
'(operators
c-w
;; < and >
slurp/barf-cp
(atom-movement t)
commentary
;; wrap with M-(, M-[, or M-{
wrap
additional
;; M-o open below list, M-O open above list
additional-insert));; override drag directions
(lispyville--define-key 'normal
(kbd "M-j") #'lispyville-drag-backward
(kbd "M-k") #'lispyville-drag-forward))
#+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)
:general
("C-c l" #'org-store-link)
(s-leader-def
"c" #'org-capture
"a" #'org-agenda"o" #'org-clock-out
"l" #'org-clock-in-last
"j" #'org-clock-goto)
(leader-def 'normal 'org-mode-map
"t" #'rasen/org-todo
"s" #'org-schedule
"d" #'org-deadline
"i" #'org-clock-in"T" #'rasen/org-do-today
"w" #'rasen/org-refile-hydra/body
"r" #'org-archive-subtree-default)
(leader-def 'motion 'org-agenda-map
"t" #'rasen/org-agenda-todo)
('normal 'org-mode-map "RET n s" #'org-narrow-to-subtree)
('(insert normal) 'org-mode-map
"C-c ," #'org-time-stamp-inactive)
('org-mode-map
;; tabs
"M-l" nil
"M-h" nil)
:gfhook 'flyspell-mode
:init
<>
:config
<>
)
#+end_srcDo not indent inside tasks
#+name: org-config
#+begin_src emacs-lisp
(setq org-adapt-indentation nil)
#+end_srcDo not indent org-babel blocks.
#+name: org-config
#+begin_src emacs-lisp
(setq org-edit-src-content-indentation 0)
#+end_srcDo not indent tags.
#+name: org-config
#+begin_src emacs-lisp
(setq org-tags-column 0)
#+end_srcWhen entering tags, offer tags from all agenda files. (This is the closes to global tag tracking I could find.))
#+name: org-config
#+begin_src emacs-lisp
(setq org-complete-tags-always-offer-all-agenda-tags t)
#+end_src#+name: org-config
#+begin_src emacs-lisp
(setq org-ellipsis "…")
#+end_srcBy default, show all of org file. (This can be changed on a per-file basis with ~#+STARTUP:~.)
#+name: org-config
#+begin_src emacs-lisp
(setq org-startup-folded nil)
#+end_srcMake the table header float if scrolled out of view.
#+name: org-config
#+begin_src emacs-lisp
(setq org-table-header-line-p t)
#+end_srcHide emphasis markers (asterisks and slashes).
#+name: org-config
#+begin_src emacs-lisp
(setq org-hide-emphasis-markers t)
#+end_srcAllow emphasis marks to follow ndash/mdash, and proper quotes.
#+name: org-config
#+begin_src emacs-lisp
;; 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_srcOpen pdfs in external viewer:
#+name: org-config
#+begin_src emacs-lisp
(add-to-list 'org-file-apps
`("\\.pdf\\'" . ,(if (eq system-type 'darwin)
"open %s"
"zathura %s")))
#+end_srcUse =whitespace-mode= in Org (but don't show too long lines).
#+name: org-config
#+begin_src emacs-lisp
(add-hook 'org-mode-hook (lambda ()
(setq-local whitespace-style '(face
tab-mark
empty
trailing))
(whitespace-mode t)))
#+end_srcMy directory for org files.
#+name: org-config
#+begin_src emacs-lisp
(setq rasen/org-directory "~/org")
#+end_srcMy helper to find all org files in a directory.
#+name: org-config
#+begin_src emacs-lisp
(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_srcPackage for =f-files= and =f-ext?= functions.
#+name: org-init
#+begin_src emacs-lisp
(use-package f
:commands (f-files f-ext? f-no-ext))
#+end_src
** Todo
A special function that marks the task as done yesterday if prefix is supplied (useful for habits).
#+name: org-config
#+begin_src emacs-lisp
(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_srcUse the following states: =TODO= =NEXT= =DONE= =CANCELED= =WAIT=.
#+name: org-config
#+begin_src emacs-lisp
(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_srcWhen repeated task is finished, go back to =TODO= state.
#+name: org-config
#+begin_src emacs-lisp
(setq-default org-todo-repeat-to-state "NEXT")
#+end_srcLog state changes to "LOGBOOK" drawer.
#+name: org-config
#+begin_src emacs-lisp
(setq-default org-log-into-drawer t)
#+end_srcSave =CLOSED= timestamp when task is done.
#+name: org-config
#+begin_src emacs-lisp
(setq org-log-done t)
#+end_srcDisable force-logging repeated tasks because otherwise you’ll get duplicated log lines.
#+name: org-config
#+begin_src emacs-lisp
(setq org-log-repeat nil)
#+end_srcFontify the whole line for done tasks.
#+name: org-config
#+begin_src emacs-lisp
(setq org-fontify-done-headline t)
#+end_srcImport =org-expiry= for =org-expiry-insert-created=---this inserts =CREATED= property.
#+name: org-config
#+begin_src emacs-lisp
(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_srcSchedule task for today and mark it NEXT.
I use this a lot during daily planning.
#+name: org-config
#+begin_src emacs-lisp
(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_srcA command to fold everything except =NEXT= items.
#+name: org-config
#+begin_src emacs-lisp
(defun rasen/org-occur-next ()
(interactive)
(let ((org-highlight-sparse-tree-matches nil))
(org-occur (concat "^" org-outline-regexp " *" "NEXT" "\\>"))))(general-def '(motion normal) 'org-mode-map
"z n" #'rasen/org-occur-next)
#+end_srcDisable highlighting for org-occur.
#+name: org-config
#+begin_src emacs-lisp
(setq org-highlight-sparse-tree-matches nil)
#+end_src
** Highlight projects
Fontify all headlines with the =:PROJECT:= tag.
#+name: org-config
#+begin_src emacs-lisp
(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_srcNote that the face is overridden in [[*Color theme][Color theme]] section.
** Clocking
Remove clocks with 0 duration.
#+name: org-config
#+begin_src emacs-lisp
(setq-default org-clock-out-remove-zero-time-clocks t)
#+end_srcSave more last clocks.
#+name: org-config
#+begin_src emacs-lisp
(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
#+name: org-config
#+begin_src emacs-lisp
(defun rasen/strip-url-from-title (title)
(message "stripping: %s" title)
(replace-regexp-in-string
" @ [^ ]*$"
""
(replace-regexp-in-string " \\[[^]]*\\]\\[[^]]*\\]$" "" title)))
#+end_srcMy capture templates.
#+name: org-config
#+begin_src emacs-lisp
(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_srcEnable org-protocol.
#+name: org-config
#+begin_src emacs-lisp
(require 'org-protocol)
#+end_src=%l= in org-capture fails with multiline context, so use only the first line as a context.
#+name: org-config
#+begin_src emacs-lisp
(setq org-context-in-file-links 1)
#+end_src*** org-capture keybindings
Instanly go into insert mode on capture.
#+name: org-config
#+begin_src emacs-lisp
;; (add-hook 'org-capture-mode-hook 'evil-insert-state)
#+end_src#+name: org-config
#+begin_src emacs-lisp
(general-def
:keymaps 'org-capture-mode-map
:states 'normal
"'" #'org-capture-finalize)
(leader-def 'normal 'org-capture-mode-map
"w" #'org-capture-refile)
#+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
#+name: org-config
#+begin_src emacs-lisp
(defun rasen/org-refile-files ()
(rasen/org-files-in-dir rasen/org-directory));; non-nil values work bad with ivy
(setq-default org-refile-use-outline-path 'file)
(setq-default org-outline-path-complete-in-steps nil);; Allow refiling to projects and honeypots only. The rest of refiling
;; is handled by hydra.
(setq org-refile-targets
'((org-agenda-files . (:tag . "honeypot"))
(org-agenda-files . (:tag . "PROJECT"))))
(add-to-list 'org-tags-exclude-from-inheritance "honeypot")
#+end_srcSetting ~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.
#+name: org-config
#+begin_src emacs-lisp
(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.
#+name: org-config
#+begin_src emacs-lisp
(require 'hydra)(defun rasen/concat (sequence separator)
(mapconcat 'identity sequence separator))(defun rasen/org-refile-exact (file path &optional arg)
"Refile to a specific location.With a `C-u' ARG argument, jump to that location."
(let* ((pos (and path (org-find-olp (cons file path))))
(rfloc (list (rasen/concat path "/") file nil pos)))(if (and (eq major-mode 'org-agenda-mode)
;; Don't use org-agenda-refile if we're just jumping
(not (and arg (listp arg))))
(org-agenda-refile nil rfloc)
(org-refile arg nil rfloc))))(defun rasen/refile (file path &optional arg)
"Refile to PATH in FILE. Clean up org-capture if it's activated.With a `C-u` ARG, just jump to the headline."
(interactive "P")
(let ((is-capturing (and (boundp 'org-capture-mode) org-capture-mode)))
(cond
((and arg (listp arg)) ;Are we jumping?
(rasen/org-refile-exact file path arg));; Are we in org-capture-mode?
(is-capturing
(rasen/org-capture-refile-but-with-args file path arg))(t
(rasen/org-refile-exact file path arg)))(when (or arg is-capturing)
(setq hydra-deactivate t))))(defun rasen/org-capture-refile-but-with-args (file path &optional arg)
"Copied from `org-capture-refile' since it doesn't allow passing arguments. This does."
(unless (eq (org-capture-get :type 'local) 'entry)
(error
"Refiling from a capture buffer makes only sense for `entry'-type templates"))
(let ((pos (point))
(base (buffer-base-buffer (current-buffer)))
(org-capture-is-refiling t)
(kill-buffer (org-capture-get :kill-buffer 'local)))
(org-capture-put :kill-buffer nil)
(org-capture-finalize)
(save-window-excursion
(with-current-buffer (or base (current-buffer))
(org-with-wide-buffer
(goto-char pos)
(rasen/org-refile-exact file path arg))))
(when kill-buffer (kill-buffer base))))(defmacro rasen/make-refile-hydra (hydraname name &rest options)
(declare (indent defun))
`(defhydra ,hydraname (:foreign-keys run :exit t)
,name,@(mapcar (lambda (x)
(let ((key (nth 0 x))
(name (nth 1 x))
(file (nth 2 x))
(path (nthcdr 3 x)))
(if (stringp file)
`(,key (rasen/refile ,file ',path current-prefix-arg) ,name)
`(,key ,file ,name))))
options)("q" nil "cancel")))
(rasen/make-refile-hydra rasen/org-refile-hydra-someday "Someday"
("u" "Uniorg" "~/org/plan.org" "Someday/Maybe" "Uniorg")
("n" "Notes" "~/org/plan.org" "Someday/Maybe" "Notes")
("a" "Writing" "~/org/plan.org" "Someday/Maybe" "Writing / website")
("c" "Computer" "~/org/plan.org" "Someday/Maybe" "Computer")
("r" "Reading" "~/org/plan.org" "Someday/Maybe" "Reading")
("s" "Misc" "~/org/plan.org" "Someday/Maybe" "Misc"))(rasen/make-refile-hydra rasen/org-refile-hydra "Refile"
("p" "Projects" "~/org/plan.org" "Projects")("n" "To do" "~/org/plan.org" "To do")
("s" "Someday" rasen/org-refile-hydra-someday/body)
("t" "Tickler" "~/org/plan.org" "Tickler")
("W" "Waiting" "~/org/plan.org" "Waiting")
("r" "Resources" "~/org/resources.org")
("b" "Books" "~/org/books.org")("w" "select"
(if (eq major-mode 'org-agenda-mode)
(org-agenda-refile current-prefix-arg)
(org-refile current-prefix-arg))))(leader-def 'normal 'org-mode-map "w" #'rasen/org-refile-hydra/body)
(leader-def 'motion 'org-agenda-mode-map "w" #'rasen/org-refile-hydra/body)
(leader-def 'normal 'org-capture-mode-map "w" #'rasen/org-refile-hydra/body)
#+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.
#+name: org-config
#+begin_src emacs-lisp
(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 underMSG 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
#+name: org-config
#+begin_src emacs-lisp
;; (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.
#+name: org-config
#+begin_src emacs-lisp
(setq org-directory "~/org"
org-default-notes-file rasen/org-refile-file
org-agenda-files (cons "~/org/roam/fluxon/index.org" (rasen/org-files-in-dir "~/org")))
#+end_srcShow 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_srcConfigure my agenda view.
#+name: org-config
#+begin_src emacs-lisp
(setq org-agenda-span 6)
#+end_srcConfigure stuck projects.
#+name: org-config
#+begin_src emacs-lisp
(add-to-list 'org-tags-exclude-from-inheritance "PROJECT")
(setq org-stuck-projects
'("+PROJECT/-TODO-DONE-CANCELED-WAIT" ("NEXT" "WAIT") nil ""))
#+end_srcDo not align tags in agenda.
#+name: org-config
#+begin_src emacs-lisp
(setq org-agenda-tags-column 0)
#+end_srcDo not show project tags.
#+name: org-config
#+begin_src emacs-lisp
(setq org-agenda-hide-tags-regexp "PROJECT\\|fc\\|suspended")
#+end_src#+begin_src emacs-lisp
(use-package org-super-agenda
:config
(general-def org-super-agenda-header-map
"k" #'org-agenda-next-line
"j" #'org-agenda-previous-line);; cache agenda, and only rebuild on request
(setq org-agenda-sticky t)(setq org-agenda-block-separator nil
org-agenda-compact-blocks t
org-agenda-time-grid '((daily today require-timed) (1000 1100 1200 1300 1400 1500 1600 1700 1800 1900 2000 2100 2200 2300) "......" "----------------")
;; org-agenda-time-grid '((daily today require-timed) nil "......" "----------------")
)(setq org-agenda-custom-commands
'(("o" "Overview"
((agenda "" (;; start from yesterday
(org-agenda-start-day "-1d");; show 6 days
(org-agenda-span 6);; show closed items
(org-agenda-show-log t)
(org-agenda-log-mode-items '(closed));; Show habits on each day. (Useful if today
;; is closed but you still want to see the
;; habit graph.)
;; (org-habit-show-habits-only-for-today nil)(org-super-agenda-groups
'(;; 1. time-grid
;; 2. scheduled today
;; 3. deadline today
;; 4. habits
;; 5. rest (anniversaries, etc.)
;; 6. deadline reminders
(:name none ;; habits
:habit t
:order 4)
(:name none ;; time-grid
:time-grid t
:order 1)
(:name none ;; don't show closed items—they are still seen in the log
:discard (:todo ("DONE" "CANCELED")))
(:name none ;; scheduled today
:todo ("WAIT")
:scheduled today
:order 2.5)
(:name none ;; scheduled today
:scheduled today
:order 2)
(:name none ;; deadline today
:deadline today
:order 3)
(:name none ;; deadline reminders
:deadline t
:scheduled t
:order 6)
(:name none ;; everything else
:anything t
:order 5)))))
(alltodo "" ((org-agenda-overriding-header "")
(org-super-agenda-groups
'(;; drop scheduled items—they are shown in
;; agenda view
(:discard (:scheduled t));; 8. Next items
;; 9. Active projects (NEXT/WAIT)
;; 11. Active books (NEXT)
;; 12. Waiting for items (WAIT)
(:name "Books"
:and (:category "books"
:todo "NEXT")
:order 11)
(:name "Projects"
:and (:tag "PROJECT"
:todo "NEXT")
:and (:tag "PROJECT"
:todo "WAIT")
:order 9)
(:name "Next"
:todo "NEXT"
:order 8)
(:todo "WAIT"
:order 12)
(:discard (:anything t))))));; inbox items (they must have a “CREATED” property to be considered an item)
(search "+{:CREATED:}" ((org-agenda-files (mapcar (lambda (x) (concat rasen/org-directory "/" x))
'("refile-omicron.org"
"orgzly.org")))
(org-agenda-overriding-header "")
(org-super-agenda-groups
'((:name "Inbox"
:auto-category t
:anything t)))))))("f" "Fluxon"
((agenda "" ((org-agenda-files '("~/org/roam/fluxon/index.org" "~/org/refile-bayraktar.org"))
;; start from yesterday
(org-agenda-start-day "-1d");; show 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-super-agenda-groups
'(;; 1. time-grid
;; 2. scheduled today
;; 3. deadline today
;; 4. habits
;; 5. rest (anniversaries, etc.)
;; 6. deadline reminders
(:name none ;; habits
:habit t
:order 4)
(:name none ;; time-grid
:time-grid t
:order 1)
(:name none ;; don't show closed items—they are still seen in the log
:discard (:todo ("DONE" "CANCELED")))
(:name none ;; scheduled today
:and (:todo ("WAIT")
:scheduled today)
:order 2.5)
(:name none ;; scheduled today
:scheduled today
:order 2)
(:name none ;; deadline today
:deadline today
:order 3)
(:name none ;; deadline reminders
:deadline t
:scheduled t
:order 6)
(:name none ;; everything else
:anything t
:order 5)))))
(alltodo "" ((org-agenda-files '("~/org/roam/fluxon/index.org" "~/org/refile-bayraktar.org"))
(org-agenda-overriding-header "")
(org-super-agenda-groups
'(;; drop scheduled items—they are shown in
;; agenda view
(:discard (:scheduled t));; 8. Next items
;; 9. Active projects (NEXT/WAIT)
;; 11. Active books (NEXT)
;; 12. Waiting for items (WAIT)
(:name "Books"
:and (:category "books"
:todo "NEXT")
:order 11)
(:name "Projects"
:and (:tag "PROJECT"
:todo "NEXT")
:and (:tag "PROJECT"
:todo "WAIT")
:order 9)
(:name "Kiavi"
:and (:category "kiavi"
:todo "NEXT")
:order 8.1)
(:name "Tonic"
:and (:category "tonic"
:todo "NEXT")
:order 8.2)
(:name "Dory"
:and (:category "dory"
:todo "NEXT")
:order 8.3)
(:name "Next"
:todo "NEXT"
:order 8.4)
(:todo "WAIT"
:order 12)
(:discard (:anything t))))));; inbox items (they must have a “CREATED” property to be considered an item)
(search "+{:CREATED:}" ((org-agenda-files '("~/org/refile-bayraktar.org"))
(org-agenda-overriding-header "")
(org-super-agenda-groups
'((:name "Inbox"
:auto-category t
:anything t)))))))("N" tags "+TODO=\"NEXT\"-PROJECT|+TODO=\"WAIT\"-PROJECT")
("n" todo-tree "NEXT")
("p" "active projects" tags "+PROJECT/+NEXT")
("P" "all projects" tags "+PROJECT/-DONE-CANCELED")))
(org-super-agenda-mode))
#+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.
#+name: org-config
#+begin_src emacs-lisp
(setq org-src-fontify-natively t)
#+end_srcDo not confirm evaluation for emacs-lisp.
#+name: org-config
#+begin_src emacs-lisp
(defun rasen/org-confirm-babel-evaluate (lang body)
(not (member lang '("emacs-lisp"))))(setq org-confirm-babel-evaluate 'rasen/org-confirm-babel-evaluate)
#+end_srcLoad more languages:
#+name: org-config
#+begin_src emacs-lisp
(org-babel-do-load-languages 'org-babel-load-languages
'((shell . t)))
#+end_src
** Latex preview
Install imagemagick and latex:
#+name: home-manager-section
#+begin_src nix
{
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#+name: org-config
#+begin_src emacs-lisp
(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
#+name: org-config
#+begin_src emacs-lisp
;; 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.
#+name: org-config
#+begin_src emacs-lisp
(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.
#+name: org-config
#+begin_src emacs-lisp
(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:
#+name: org-config
#+begin_src emacs-lisp
(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.
#+name: org-config
#+begin_src emacs-lisp
(require 'org-checklist)
#+end_src
** Habits
#+name: org-config
#+begin_src emacs-lisp
(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 timestamps as ids.
#+name: org-config
#+begin_src emacs-lisp
(setq org-id-method 'ts)
#+end_srcConfigure ~org-store-link~ to prefer ids for headlines.
#+name: org-config
#+begin_src emacs-lisp
(setq org-id-link-to-org-use-id 'create-if-interactive)
#+end_srcOverride ~org-id-new~ to use ~rasen/tsid~ as ids. (They have less precision but that's enough for me.) By using tsid, I can easily switch headline-nodes to file-nodes—id becomes the filename.
#+name: org-config
#+begin_src emacs-lisp
(el-patch-defun org-id-new (&optional prefix)
"Create a new globally unique ID.An ID consists of two parts separated by a colon:
- a prefix
- a unique part that will be created according to `org-id-method'.PREFIX can specify the prefix, the default is given by the variable
`org-id-prefix'. However, if PREFIX is the symbol `none', don't use any
prefix even if `org-id-prefix' specifies one.So a typical ID could look like \"Org:4nd91V40HI\"."
(let* ((prefix (if (eq prefix 'none)
""
(concat (or prefix org-id-prefix) ":")))
unique)
(if (equal prefix ":") (setq prefix ""))
(cond
((memq org-id-method '(uuidgen uuid))
(setq unique (org-trim (shell-command-to-string org-id-uuid-program)))
(unless (org-uuidgen-p unique)
(setq unique (org-id-uuid))))
((eq org-id-method 'org)
(let* ((etime (org-reverse-string (org-id-time-to-b36)))
(postfix (if org-id-include-domain
(progn
(require 'message)
(concat "@" (message-make-fqdn))))))
(setq unique (concat etime postfix))))
((eq org-id-method 'ts)
(let ((ts (el-patch-swap (format-time-string org-id-ts-format)
(rasen/tsid)))
(postfix (if org-id-include-domain
(progn
(require 'message)
(concat "@" (message-make-fqdn))))))
(setq unique (concat ts postfix))))
(t (error "Invalid `org-id-method'")))
(concat prefix unique)))
#+end_srcHighlight org-id links with different color.
#+name: org-config
#+begin_src emacs-lisp
(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)
:bind
(:map org-mode-map
("C-c i" . org-roam-node-insert))
:general
(s-leader-def
"n r" #'org-roam-buffer-toggle
"n f" #'org-roam-node-find
"n j" #'org-roam-dailies-goto-date
"n t" #'org-roam-dailies-goto-today
"n ." #'org-roam-dailies-goto-today
"n ," #'org-roam-dailies-goto-yesterday)
(:keymaps 'org-mode-map
:states 'normal
"SPC ," #'org-roam-dailies-goto-previous-note
"SPC ." #'org-roam-dailies-goto-next-note)
(:keymaps 'org-mode-map
:states '(insert visual)
"C-c i" #'org-roam-node-insert
;; C-i is interpreted as TAB
"C-c TAB" #'org-roam-node-insert):init
;; yes, I have migrated to v2
(setq org-roam-v2-ack t):config
(setq org-roam-directory (concat rasen/org-directory "/roam")
;; move `org-roam-db-location' off roam directory, so syncthing does not sync it
org-roam-db-location (expand-file-name "cache/org-roam.db" user-emacs-directory))<>
<>
<>
<>
<>
<>
<>
<>
<>
<>
<>
(org-roam-db-autosync-enable))
#+end_src
*** org-roam-exclude-org-fc
Do not treat [[*org-fc][org-fc]] cards as nodes.
#+name: org-roam-exclude-org-fc
#+begin_src emacs-lisp :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.
#+name: org-roam-slip-boxes
#+begin_src emacs-lisp :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}")("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#+PUBLISHED: false");; Literature notes
("b" "bibliograpic" "biblio/" "${citekey}" "#+LAST_MODIFIED: \n#+DATE: %<%Y-%m-%d>\n"))
"My slip boxes. Format is a list of (capture-key name directory filename extra-template).");; one capture template per slip-box
(setq org-roam-capture-templates
(mapcar (lambda (x)
(let ((key (nth 0 x))
(name (nth 1 x))
(dir (nth 2 x))
(filename (nth 3 x))
(extra-template (nth 4 x)))
`(,key ,name plain "%?"
:if-new (file+head
,(concat dir filename ".org")
,(concat "#+TITLE: ${title}\n"
extra-template))
:immediate-finish t
:unnarrowed t)))
rasen/slip-boxes))(defun rasen/capture-tsid (node)
"A hack definition to workaround that org-roam passes a node argument."
(rasen/tsid))
#+end_srcAnd a helper command to move notes between slip boxes.
#+name: org-roam-slip-boxes
#+begin_src emacs-lisp :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_srcAnd some extra refile helpers for dealing with inbox.
#+name: org-roam-slip-boxes
#+begin_src emacs-lisp :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=.
#+name: org-roam-node-display
#+begin_src emacs-lisp :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:
#+name: org-roam-buffer
#+begin_src emacs-lisp :tangle no
(setq org-roam-mode-section-functions
(list #'org-roam-backlinks-section
#'org-roam-reflinks-section
#'org-roam-unlinked-references-section))
#+end_srcMake it prettier—enable word-wrap and variable pitch font.
#+name: org-roam-buffer
#+begin_src emacs-lisp :tangle no
(add-hook 'org-roam-mode-hook #'visual-line-mode)
(add-hook 'org-roam-mode-hook #'variable-pitch-mode)
#+end_src
*** org-roam-dailiesI 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.
#+name: org-roam-dailies
#+begin_src emacs-lisp :tangle no
(require 'org-roam-dailies)
(setq org-roam-dailies-directory "life/journal/")
(setq org-roam-dailies-capture-templates
`(("j" "journal" plain "%U\n%?"
:if-new (file+head "%<%Y-%m-%d>.org"
;; Adding day of week to title makes
;; unlinked references search weaker, so
;; store it in metadata.
,(concat
"#+TITLE: %<%Y-%m-%d>\n"
"- day of week :: %<%A>\n"
"\n"
;; "* Events\n"
;; "* Decisions\n"
;; "* Tomorrow\n"
)))))
#+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_srcFirefox 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.
#+name: org-roam-protocol
#+begin_src emacs-lisp :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_srcWhen 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.)
#+name: org-roam-protocol
#+begin_src emacs-lisp :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:
#+name: home-manager-section
#+begin_src nix
{
home.packages = [ pkgs.graphviz ];
}
#+end_srcEmacs configuration:
#+name: org-roam-graph
#+begin_src emacs-lisp :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)<>
#+end_src
**** org-roam-graph-exclude-node
Patch org-roam to allow a custom function to exclude nodes.
#+name: org-roam-graph-exclude-node
#+begin_src emacs-lisp :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_srcExclude all links that are not nodes (non-id) as well as links from non-permanent directories.
#+name: org-roam-graph-exclude-node
#+begin_src emacs-lisp :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.
#+name: org-roam-kebab-slugs
#+begin_src emacs-lisp :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.
#+name: org-roam-update-ids
#+begin_src emacs-lisp :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.
#+name: org-roam-new-node
#+begin_src emacs-lisp
(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
#+name: org-roam-update-title
#+begin_src emacs-lisp
;; 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)
(general-def 'normal 'bibtex-mode-map
"C-c C-c" #'org-ref-clean-bibtex-entry
"C-c c" #'org-ref-clean-bibtex-entry
"C-c s" #'bibtex-sort-buffer
"C-c n" #'org-ref-open-bibtex-notes;; (a)ttach pdf
"C-c a" #'org-ref-bibtex-assoc-pdf-with-entry
"C-c f" #'org-ref-bibtex-pdf;; (o)nline
"C-c o" #'org-ref-url-html-to-bibtex
"C-c i" #'isbn-to-bibtex)(setq org-ref-completion-library 'org-ref-ivy-cite)
(setq org-ref-insert-cite-function
(lambda ()
(org-cite-insert nil)));; Rules for automatic key generation
(setq bibtex-autokey-year-length 4
bibtex-autokey-name-year-separator ""
bibtex-autokey-year-title-separator "-"
bibtex-autokey-titleword-separator "-"
bibtex-autokey-titlewords 5
bibtex-autokey-titlewords-stretch 1
bibtex-autokey-titleword-length 5)(setq bibtex-dialect 'biblatex)
;; `isbn-to-bibtex' fails with "(wrong-type-argument stringp nil)"
;; error down in `org-ref-isbn-clean-bibtex-entry' functions. This
;; happens because temporary buffer is not in `bibtex-mode', so
;; `bibtex-entry-head' variable is not set.
;;
;; Prepend `bibtex-mode' to the list of processors, so the next ones
;; work correctly.
(add-hook 'org-ref-isbn-clean-bibtex-entry-hook #'bibtex-mode);; Do not fill entries. (It works badly with urls.)
(el-patch-defun bibtex-fill-field-bounds (bounds justify &optional move)
"Fill BibTeX field delimited by BOUNDS.
If JUSTIFY is non-nil justify as well.
If optional arg MOVE is non-nil move point to end of field."
(let ((end-field (copy-marker (bibtex-end-of-field bounds))))
(if (not justify)
(goto-char (bibtex-start-of-text-in-field bounds))
(goto-char (bibtex-start-of-field bounds))
(forward-char) ; leading comma
(bibtex-delete-whitespace)
(insert "\n")
(indent-to-column (+ bibtex-entry-offset
bibtex-field-indentation))
(re-search-forward "[ \t\n]*=" end-field)
(replace-match "=")
(forward-char -1)
(if bibtex-align-at-equal-sign
(indent-to-column
(+ bibtex-entry-offset (- bibtex-text-indentation 2)))
(insert " "))
(forward-char)
(bibtex-delete-whitespace)
(if bibtex-align-at-equal-sign
(insert " ")
(indent-to-column bibtex-text-indentation)))
(el-patch-remove
;; Paragraphs within fields are not preserved. Bother?
(fill-region-as-paragraph (line-beginning-position) end-field
default-justification nil (point)))
(if move (goto-char end-field)))))
#+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
** toc-org
Generate Table of Contents for this file.#+begin_src emacs-lisp
(use-package toc-org
:commands (toc-org-mode toc-org-insert-toc))
#+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:
#+name: home-manager-section
#+begin_src nix
{
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*")))(general-define-key
:definer 'minor-mode
:states 'normal
:keymaps 'org-fc-review-flip-mode
"RET" 'org-fc-review-flip
"n" 'org-fc-review-flip
"s" 'org-fc-review-suspend-card
"q" 'org-fc-review-quit)(general-define-key
:definer 'minor-mode
:states 'normal
:keymaps 'org-fc-review-rate-mode
"a" 'org-fc-review-rate-again
"h" 'org-fc-review-rate-hard
"g" 'org-fc-review-rate-good
;; There seems to be an issue binding "g" and it still behaves as
;; a prefix for other commands in rate mode.
;;
;; Bind `org-fc-review-rate-good' to "n" as well to workaround
;; this.
"n" 'org-fc-review-rate-good
"e" 'org-fc-review-rate-easy
"s" 'org-fc-review-suspend-card
"q" 'org-fc-review-quit)(general-def 'normal 'org-fc-review-edit-mode-map
"'" #'org-fc-review-resume)(general-def 'normal 'org-fc-dashboard-mode-map
"r" #'org-fc-dashboard-review
"g" (defun rasen/org-fc-dashboard ()
(interactive)
(org-fc-dashboard org-fc-context-all))
"h" #'rasen/org-fc-upcoming-histogram
"q" #'quit-window)<>)
#+end_src** COMMENT org-fc review todos
My modifications to support reviewing arbitrary todo lists with spaced repetition.
#+name: org-fc-review-todos
#+begin_src emacs-lisp
(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))(leader-def 'normal 'org-mode-map
"\\" #'rasen/toggle-org-view-mode)
#+end_src
** Evilify org-mode
#+begin_src emacs-lisp
(use-package evil-org
:disabled t
:after org
:diminish
:init
;; https://github.com/Somelauw/evil-org-mode/issues/93
(fset 'evil-redirect-digit-argument 'ignore):custom
;; https://github.com/Somelauw/evil-org-mode/issues/93
(add-to-list 'evil-digit-bound-motions 'evil-org-beginning-of-line)
(evil-define-key 'motion 'evil-org-mode
(kbd "0") 'evil-org-beginning-of-line);; swap j/k
(evil-org-movement-bindings '((up . "j")
(down . "k")
(left . "h")
(right . "l")))
:config
(add-hook 'org-mode-hook 'evil-org-mode)
(add-hook 'evil-org-mode-hook
(lambda ()
(evil-org-set-key-theme)))
(require 'evil-org-agenda)
(evil-org-agenda-set-keys);; when editing code blocks, use current window and do not
;; reorganize my frame
(setq org-src-window-setup 'current-window)(general-def 'normal org-mode-map
"'" #'org-edit-special
"C-c '" (rasen/hard-way "'")
"go" #'org-open-at-point
"C-c C-o" (rasen/hard-way "go"));; open file links in the same window
(push '(file . find-file) org-link-frame-setup)(general-def 'normal org-src-mode-map
"'" #'org-edit-src-exit
"C-c '" (rasen/hard-way "'"))(general-def 'motion org-agenda-mode-map
"k" #'org-agenda-next-line
"j" #'org-agenda-previous-line
"gk" #'org-agenda-next-item
"gj" #'org-agenda-previous-item
"C-k" #'org-agenda-next-item
"C-j" #'org-agenda-previous-item"K" #'org-agenda-priority-down
"J" #'org-agenda-priority-up"M-k" #'org-agenda-drag-line-forward
"M-j" #'org-agenda-drag-line-backward)(general-def 'motion org-agenda-mode-map
"SPC" nil ;; unset prefix
"go" #'org-agenda-open-link
"gl" #'org-agenda-log-mode)
(leader-def 'motion org-agenda-mode-map
"SPC" #'org-save-all-org-buffers
"s" #'org-agenda-schedule
"d" #'org-agenda-deadline
"w" #'rasen/org-refile-hydra/body
"t" #'rasen/org-agenda-todo))
#+end_src
Use emacs-state in org-lint buffers.
#+name: org-config
#+begin_src emacs-lisp
;;(evil-set-initial-state 'org-lint--report-mode 'emacs)
;;(evil-set-initial-state 'epa-key-list-mode 'emacs)
#+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 "^[^:]+: "
(let ((inhibit-field-text-motion t))
(line-beginning-position)))
(org-eval-in-calendar '(calendar-goto-today))
(insert "."))))
#+end_srcI am not sure how but the if-branch causes the issue. (Somehow, it causes mouse cursor to jump.) So… just redefine the binding:
#+name: org-config
#+begin_src emacs-lisp
(org-defkey org-read-date-minibuffer-local-map (kbd ".") #'self-insert-command)
#+end_srcWith ~self-insert-command~, it works just fine. Well, the calendar doesn’t jump to today but who cares.
* Mail setup
My email load is handled by mbsync (to download email), msmtp (to send mail), and notmuch (to tag and browse it).Applications are configured with Home Manager, and notmuch frontend is configured in Emacs.
** Applications
#+name: home-manager-section
#+begin_src nix
{
# Store mails in ~/Mail
accounts.email.maildirBasePath = "Mail";accounts.email.accounts.as = {
realName = "Oleksii Shmalko";
address = "me@alexeyshmalko.com";
flavor = "plain";userName = "me@alexeyshmalko.com";
imap.host = "imap.secureserver.net";
imap.port = 993;
imap.tls.enable = true;
smtp.host = "smtpout.secureserver.net";
smtp.port = 465;
smtp.tls.enable = true;passwordCommand = "pass me@alexeyshmalko.com";
maildir.path = "alexeyshmalko";msmtp.enable = true;
notmuch.enable = true;
mbsync.enable = true;
mbsync.create = "maildir";
};# Use mbsync to fetch email. Configuration is constructed manually
# to keep my current email layout.
programs.mbsync = {
enable = true;
extraConfig = lib.mkBefore ''
MaildirStore local
Path ~/Mail/
Inbox ~/Mail/INBOX
SubFolders Verbatim
'';
};# Notmuch for email browsing, tagging, and searching.
programs.notmuch = {
enable = true;
new.ignore = [
".mbsyncstate"
".mbsyncstate.lock"
".mbsyncstate.new"
".mbsyncstate.journal"
".uidvalidity"
"dovecot-uidlist"
"dovecot-keywords"
"dovecot.index"
"dovecot.index.log"
"dovecot.index.log.2"
"dovecot.index.cache"
"/^archive/"
];
};# msmtp for sending mail
programs.msmtp.enable = true;# My Maildir layout predates home-manager configuration, so I do not
# use mbsync config generation from home-manager, to keep layout
# compatible.
imports =
let
emails = [
{ name = "gmail"; email = "rasen.dubi@gmail.com"; path = "Personal"; primary = true; }
{ name = "ps"; email = "ashmalko@doctoright.org"; path = "protocolstandard"; }
{ name = "egoless"; email = "me@egoless.tech"; path = "egoless"; }
];
mkGmailBox = { name, email, path, ... }@all: {
accounts.email.accounts.${name} = {
realName = "Oleksii Shmalko";
address = email;
flavor = "gmail.com";passwordCommand = "pass imap.gmail.com/${email}";
maildir.path = path;msmtp.enable = true;
notmuch.enable = true;
} // (removeAttrs all ["name" "email" "path"]);programs.mbsync.extraConfig = ''
IMAPAccount ${name}
Host imap.gmail.com
User ${email}
PassCmd "pass imap.gmail.com/${email}"
SSLType IMAPS
CertificateFile /etc/ssl/certs/ca-certificates.crtIMAPStore ${name}-remote
Account ${name}Channel sync-${name}-all
Far :${name}-remote:"[Gmail]/All Mail"
Near :local:${path}/all
Create Both
SyncState *Channel sync-${name}-spam
Far :${name}-remote:"[Gmail]/Spam"
Near :local:${path}/spam
Create Both
SyncState *Channel sync-${name}-sent
Far :${name}-remote:"[Gmail]/Sent Mail"
Near :local:${path}/sent
Create Both
SyncState *Group sync-${name}
Channel sync-${name}-all
Channel sync-${name}-spam
Channel sync-${name}-sent'';
};
in map mkGmailBox emails;
}
#+end_src
** Interface
#+begin_src emacs-lisp
(use-package notmuch
:config
;; (setq mm-text-html-renderer 'shr)(setq notmuch-archive-tags '("-unread"))
(setq notmuch-saved-searches
'(
(:name "unread-inbox" :query "tag:inbox and tag:unread" :key "u")
;; (:name "unread" :query "tag:unread and not tag:nixos and not tag:rust" :key "u")
(:name "unread-egoless" :query "tag:egoless and tag:unread" :key "e")
(:name "unread-nixos" :query "tag:unread and tag:nixos and not tag:nixpkgs" :key "n")
(:name "unread-nixpkgs" :query "tag:unread and tag:nixpkgs" :key "p")
(:name "unread-participating" :query "tag:unread and tag:participating" :key "t")
(:name "unread-doctoright" :query "tag:unread and tag:doctoright" :key "d")
(:name "unread-rss" :query "tag:unread and tag:rss" :key "r")
(:name "unread-other" :query "tag:unread and not tag:nixos and not tag:inbox and not tag:doctoright and not tag:rss" :key "o")
(:name "later" :query "tag:later" :key "l")
(:name "flagged" :query "tag:flagged" :key "F")
(:name "personal" :query "tag:personal" :key "P")
(:name "doctoright" :query "tag:doctoright" :key "D")
(:name "sent" :query "tag:sent" :key "s")
(:name "drafts" :query "tag:draft" :key "f")
(:name "all mail" :query "*" :key "a")))
(setq notmuch-hello-sections
'(;; notmuch-hello-insert-header
notmuch-hello-insert-saved-searches
;; notmuch-hello-insert-search
notmuch-hello-insert-alltags
notmuch-hello-insert-recent-searches
;; notmuch-hello-insert-footer
))
(setq-default notmuch-show-indent-content nil)(defun rasen/mbsync ()
(interactive)
(let ((mbsync-cmd (if (string= (system-name) "omicron")
"mbsync sync-gmail & r2e run & wait; /home/rasen/dotfiles/notmuch.sh"
"mbsync -a && ~/dotfiles/notmuch.sh")))
(async-shell-command mbsync-cmd "*mbsync*")));; bind q in shell to hide buffer
(general-def 'motion shell-mode-map
"q" #'quit-window)(defun rasen/notmuch-search-mute ()
(interactive)
(notmuch-search-tag '("+muted"))
(notmuch-search-archive-thread))(general-def notmuch-hello-mode-map "f" 'rasen/mbsync)
(general-def
:keymaps '(notmuch-hello-mode-map
notmuch-search-mode-map
notmuch-show-mode-map)
"g" 'notmuch-refresh-all-buffers)(general-def 'notmuch-search-mode-map
"k" #'notmuch-search-archive-thread
"m" #'rasen/notmuch-search-mute)
(general-def 'notmuch-show-mode-map
"k" #'notmuch-show-archive-thread-then-next)
;; remap old function
(general-def '(notmuch-search-mode-map
notmuch-show-mode-map)
"K" #'notmuch-tag-jump)(general-def 'notmuch-show-mode-map
"C" #'rasen/org-capture-link)(general-def 'notmuch-show-mode-map
"M-u" (lambda ()
(interactive)
(notmuch-show-tag '("+unread"))));; notmuch-tag-formats
(setq-default notmuch-tagging-keys
'(("a" notmuch-archive-tags "Archive")
("u" notmuch-show-mark-read-tags "Mark read")
("m" ("+muted") "Mute")
("f" ("+flagged") "Flag")
("s" ("+spam" "-inbox") "Mark as spam")
("d" ("+deleted" "-inbox") "Delete")));; support for linking notmuch mails in org-mode
(require 'ol-notmuch))
#+end_srcOpen html links in browser:
#+name: home-manager-section
#+begin_src nix
{
home.file.".mailcap".text = ''
text/html; firefox %s
application/pdf; open %s
'';
}
#+end_src
** Emacs
#+begin_src emacs-lisp
(setq user-full-name "Oleksii Shmalko"
user-mail-address "rasen.dubi@gmail.com")
#+end_srcEmail sending.
#+begin_src emacs-lisp
(use-package message
:ensure nil ; built-in
:config
(setq message-send-mail-function 'message-send-mail-with-sendmail
message-sendmail-f-is-evil t
message-sendmail-envelope-from nil ; 'header
message-sendmail-extra-arguments '("--read-envelope-from"))(setq mml-secure-smime-sign-with-sender t)
(setq mml-secure-openpgp-sign-with-sender t);; Add signature by default
(add-hook 'message-setup-hook 'mml-secure-message-sign-pgpmime)
;; Verify other's signatures
(setq mm-verify-option 'always))(use-package sendmail
:ensure nil ; built-in
:config
(setq mail-specify-envelope-from nil
send-mail-function 'message-send-mail-with-sendmail
sendmail-program "msmtp"))
#+end_src
** rss2email
Install rss2email to deliver RSS feeds to my maildir.
#+name: home-manager-section
#+begin_src nix
{
home.packages = [ pkgs.rss2email ];
}
#+end_src
* Applications
Here go applications (almost) every normal user needs.
** GPG
#+name: nixos-section
#+begin_src nix
{
programs.gnupg.agent = {
enable = true;
enableSSHSupport = true;
pinentryFlavor = "qt";
};## is it no longer needed?
#
# systemd.user.sockets.gpg-agent-ssh = {
# wantedBy = [ "sockets.target" ];
# listenStreams = [ "%t/gnupg/S.gpg-agent.ssh" ];
# socketConfig = {
# FileDescriptorName = "ssh";
# Service = "gpg-agent.service";
# SocketMode = "0600";
# DirectoryMode = "0700";
# };
# };services.pcscd.enable = true;
}
#+end_srcInstall on macOS:
#+name: darwin-section
#+begin_src nix
{
environment.systemPackages = [ pkgs.gnupg ];
programs.gnupg.agent = {
enable = true;
enableSSHSupport = true;
};
}
#+end_srcRequest passwords in Emacs minibuffer. (emacs-lisp)
#+begin_src emacs-lisp
(setq epa-pinentry-mode 'loopback)
#+end_src*** Fix epg--filter-revoked-keys wrongly filtering out my keys
Reported as: bug#46138: 28.0.50; epg--filter-revoked-keys filters out valid keysTemporary workaround:
#+begin_src emacs-lisp
(defun epg--filter-revoked-keys (keys)
keys)
#+end_src
** Yubikey
#+name: nixos-section
#+begin_src nix
{
environment.systemPackages = [
pkgs.yubikey-manager
pkgs.yubikey-personalization
pkgs.yubikey-personalization-gui
];services.udev.packages = [
pkgs.yubikey-personalization
pkgs.libu2f-host
];
}
#+end_src#+name: home-manager-section
#+begin_src nix
{
home.packages = [
pkgs.yubikey-manager
pkgs.yubikey-personalization
];
}
#+end_src
** password-store
Install [[https://www.passwordstore.org/][password-store]] along with [[https://github.com/tadfisher/pass-otp][one-time password extension]].
#+name: home-manager-section
#+begin_src nix
{
home.packages = [
(pkgs.pass.withExtensions (exts: [ exts.pass-otp exts.pass-audit exts.pass-genphrase ]))
pkgs.qrencode
];
}
#+end_srcInstall [[https://github.com/browserpass/browserpass][browserpass]] firefox extension backend.
#+name: home-manager-section
#+begin_src nix
{
programs.browserpass = {
enable = true;
browsers = ["firefox" "chrome"];
};
}
#+end_srcIntegration with Emacs. (emacs-lisp)
#+begin_src emacs-lisp
(use-package ivy-pass
:disabled t
:commands (ivy-pass))(use-package pass
:disabled t
:commands (pass))
#+end_srcEnable auth-source–pass integration.
#+begin_src emacs-lisp
(use-package auth-source
:config
(auth-source-pass-enable))
#+end_src
** AgeUse Age for file encryption.
#+name: home-manager-section
#+begin_src nix
{
home.packages = [
pkgs.age
pkgs.age-plugin-yubikey
];
}
#+end_src** Passage
#+name: home-manager-section
#+begin_src nix
{
home.sessionVariables.PASSAGE_DIR = "${config.home.homeDirectory}/.password-store";
home.sessionVariables.PASSAGE_IDENTITIES_FILE = "${config.home.homeDirectory}/.config/passage/identities";
home.packages = [
(pkgs.passage.overrideAttrs (old: {
patches =
(old.patches or [])
++ [
# Allow using multiple identities.
(pkgs.fetchpatch {
url = "https://github.com/FiloSottile/passage/commit/fba940f9e9ffbad7b746f26b8d6323ef6f746187.patch";
sha256 = "sha256-2w/k6JmcxFq9ThBasM0sL+58fwutF1ioZzwRFXfJgME=";
})
];
}))
];
}
#+end_src** Gopass
#+name: home-manager-section
#+begin_src nix
{
home.packages = [
pkgs.unstable.gopass
pkgs.unstable.gopass-jsonapi
pkgs.pinentry_mac
];
}
#+end_src** KDE apps
I don't use full KDE but some apps are definitely nice.
#+name: home-manager-section
#+begin_src nix
{
home.packages = pkgs.lib.linux-only [
pkgs.gwenview
pkgs.dolphin
# pkgs.kdeFrameworks.kfilemetadata
pkgs.filelight
pkgs.shared-mime-info
];
}
#+end_srcKDE apps might have issues with mime types without this:
#+name: nixos-section
#+begin_src nix
{
environment.pathsToLink = [ "/share" ];
}
#+end_src
** Zathura
[[https://pwmt.org/projects/zathura/][Zathura]] is a cool document viewer with Vim-like bindings.
#+name: home-manager-section
#+begin_src nix
{
programs.zathura = {
enable = true;
options = {
incremental-search = true;
};# Swap j/k (for Workman layout)
extraConfig = ''
map j scroll up
map k scroll down
'';
};
}
#+end_src
** User applications
#+name: home-manager-section
#+begin_src nix
{
home.packages = [
(pkgs.lib.linux-only pkgs.google-play-music-desktop-player)
(pkgs.lib.linux-only pkgs.tdesktop) # Telegram
pkgs.feh# mesa is broken on macOS
(pkgs.lib.linux-only pkgs.mplayer)
(pkgs.lib.linux-only pkgs.smplayer)
];
}
#+end_src
** ActivityWatcher
Emacs integration:
#+begin_src emacs-lisp
(use-package activity-watch-mode
:config
(global-activity-watch-mode))
#+end_src
* Development
** Vim
Install Vim as my backup editor. (~<>~)
#+name: nixos-section
#+begin_src nix
{
environment.systemPackages = [
pkgs.vim_configurable
];
}
#+end_srcFor Home Manager–managed hosts.
#+name: home-manager-section
#+begin_src nix
{
home.packages = [
pkgs.vim_configurable
];
}
#+end_srcLink its configuration.
#+name: home-manager-section
#+begin_src nix
{
home.file.".vim".source = ./.vim;
home.file.".vimrc".source = ./.vim/init.vim;
}
#+end_src
** Terminal / shell
*** rxvt-unicode
I use urxvt as my terminal emulator.
#+name: home-manager-section
#+begin_src nix
{
programs.urxvt = {
enable = true;
iso14755 = false;fonts = [
"-*-terminus-medium-r-normal-*-32-*-*-*-*-*-iso10646-1"
];scroll = {
bar.enable = false;
lines = 65535;
scrollOnOutput = false;
scrollOnKeystroke = true;
};
extraConfig = {
"loginShell" = "true";
"urgentOnBell" = "true";
"secondaryScroll" = "true";# Molokai color theme
"background" = "#101010";
"foreground" = "#d0d0d0";
"color0" = "#101010";
"color1" = "#960050";
"color2" = "#66aa11";
"color3" = "#c47f2c";
"color4" = "#30309b";
"color5" = "#7e40a5";
"color6" = "#3579a8";
"color7" = "#9999aa";
"color8" = "#303030";
"color9" = "#ff0090";
"color10" = "#80ff00";
"color11" = "#ffba68";
"color12" = "#5f5fee";
"color13" = "#bb88dd";
"color14" = "#4eb4fa";
"color15" = "#d0d0d0";
};
};
}
#+end_srcUrxvt gets its setting from =.Xresources= file. If you ever want to reload it on-the-fly, type the following (or press =C-c C-c= if you're reading this document in emacs now):
#+begin_src sh :tangle no
xrdb ~/.Xresources
#+end_src
*** vterm
vterm is a terminal emulator using libvterm. Which allows running it within Emacs.
#+begin_src emacs-lisp
(use-package vterm
:commands vterm
:config
(setq vterm-kill-buffer-on-exit t)(add-hook 'vterm-mode-hook #'rasen/disable-hl-line-mode)
;; (push 'vterm-mode evil-motion-state-modes)
(general-def 'vterm-mode-map
"" nil
"" nil
"" nil
"" nil
"