https://github.com/rasendubi/dotfiles
My dotfiles
https://github.com/rasendubi/dotfiles
dotfiles emacs-configuration nix-dotfiles nixos org-mode-configuration
Last synced: about 1 year ago
JSON representation
My dotfiles
- Host: GitHub
- URL: https://github.com/rasendubi/dotfiles
- Owner: rasendubi
- Created: 2013-10-03T20:38:16.000Z (over 12 years ago)
- Default Branch: master
- Last Pushed: 2025-02-17T02:07:54.000Z (over 1 year ago)
- Last Synced: 2025-03-30T19:38:13.862Z (about 1 year 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_src
Nix 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_src
For nix-darwin systems:
#+name: darwin-section
#+begin_src nix
{
nix = {
extraOptions = ''
experimental-features = nix-command flakes
'';
};
}
#+end_src
For non-NixOS system, put the following into =~/.config/nix/nix.conf=.
#+begin_src conf :tangle no
experimental-features = nix-command flakes
#+end_src
** Unstable packages
For packages that are not available in latest stable, expose the unstable channel as ~pkgs.unstable~.
Add input:
#+name: flake-inputs
#+begin_src nix
nixpkgs-unstable = {
type = "github";
owner = "NixOS";
repo = "nixpkgs";
ref = "nixpkgs-unstable";
};
#+end_src
Add 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_src
Expose 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_src
Integrate 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_src
Integrate 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_src
Add 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_src
And 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_src
This =<>= 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_src
Same 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_src
org-clock.1s.sh
#+begin_src sh
#!/usr/bin/env bash
~/.nix-profile/bin/emacs --batch --eval "(progn (require 'server) (princ (server-eval-at \"server\" '(rasen/xbar \"$0\"${1:+" '$1"}))))"
#+end_src
** Yabai integration
#+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_src
Skhd 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 ; default
default < lcmd - 0x2A ; escaping
escaping < lcmd - 0x2A ; default
# escaping < lcmd - e ~
default < cmd + ctrl - r : yabai -m space --layout $(yabai -m query --spaces --space | jq -r 'if .type == "bsp" then "float" else "bsp" end')
# default < lcmd + ctrl - n : yabai -m window --ratio rel:-0.05
# default < lcmd + ctrl - o : yabai -m window --ratio rel:+0.05
default < lcmd + shift - n : yabai -m window --swap west
default < lcmd + shift - u : yabai -m window --swap north
default < lcmd + shift - e : yabai -m window --swap south
default < lcmd + shift - o : yabai -m window --swap east
default < lcmd - n [
,* : yabai -m window --focus west
"Emacs" ~
]
default < lcmd - u [
,* : yabai -m window --focus north
"Emacs" ~
]
default < lcmd - e [
,* : yabai -m window --focus south
"Emacs" ~
]
default < lcmd - o [
,* : yabai -m window --focus east
"Emacs" ~
]
'';
};
}
#+end_src
Emacs integration for yabai:
#+begin_src emacs-lisp
(defun yabai-move-on-error (direction move-fn)
"Execute `move-fn'. If that function errors, move yabai focus in the specified direction."
(interactive)
(condition-case nil
(funcall move-fn)
(user-error (rasen/yabai-move direction))))
(defun rasen/yabai-move (direction)
"Move yabai focus in the specified `direction'."
(start-process "yabai" "*yabai*" "yabai" "-m" "window" "--focus" direction))
(defun yabai-windmove-left ()
(interactive)
(yabai-move-on-error "west" #'windmove-left))
(defun yabai-windmove-right ()
(interactive)
(yabai-move-on-error "east" #'windmove-right))
(defun yabai-windmove-up ()
(interactive)
(yabai-move-on-error "north" #'windmove-up))
(defun yabai-windmove-down ()
(interactive)
(yabai-move-on-error "south" #'windmove-down))
#+end_src
* 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_src
LVM 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_src
Clickpad:
#+name: machine-omicron
#+begin_src nix
{
services.xserver.libinput = {
enable = true;
touchpad.accelSpeed = "0.7";
};
}
#+end_src
Fix 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-emacs
I 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_src
Use overlay (~<>~).
#+name: flake-overlays
#+begin_src nix
inputs.emacs-overlay.overlay
#+end_src
Expose 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-mode
corfu
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.valign
epkgs.elpaPackages.eglot
(epkgs.trivialBuild rec {
pname = "org-roam-ui";
version = "20210830";
src = pkgs.fetchFromGitHub {
owner = "org-roam";
repo = "org-roam-ui";
rev = "9ad111d2102c24593f6ac012206bb4b2c9c6c4e1";
sha256 = "sha256-x6notv/U+y9Es8m58R/Qh7GEAtRqXqXvr7gy5OiDDUM=";
};
packageRequires = [
epkgs.melpaPackages.f
epkgs.melpaPackages.org-roam
epkgs.melpaPackages.websocket
epkgs.melpaPackages.simple-httpd
];
postInstall = ''
cp -r ./out/ $LISPDIR/
'';
meta = {
description = "A graphical frontend for exploring your org-roam Zettelkasten";
license = pkgs.lib.licenses.gpl3;
};
})
(epkgs.trivialBuild rec {
pname = "org-fc";
version = "20201121";
src = pkgs.fetchFromGitHub {
# owner = "rasendubi";
# repo = "org-fc";
# rev = "35ec13fd0412cd17cbf0adba7533ddf0998d1a90";
# sha256 = "sha256-2h1dIR7WHYFsLZ/0D4HgkoNDxKQy+v3OaiiCwToynvU=";
owner = "l3kn";
repo = "org-fc";
rev = "cc191458a991138bdba53328690a569b8b563502";
sha256 = "sha256-wzMSgS4iZfpKOICqQQuQYNPb2h7i4tTWsMs7mVmgBt8=";
};
packageRequires = [
epkgs.elpaPackages.org
epkgs.melpaPackages.hydra
];
propagatedUserEnvPkgs = [ pkgs.findutils pkgs.gawk ];
postInstall = ''
cp -r ./awk/ $LISPDIR/
'';
meta = {
description = "Spaced Repetition System for Emacs org-mode";
license = pkgs.lib.licenses.gpl3;
};
})
# required for org-roam/emacsql-sqlite3
pkgs.sqlite
pkgs.notmuch
pkgs.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_src
Install 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_src
Another important file that needs to be tangled is [[./.emacs.d/early-init.el]]. For now, just add a header to it.
#+begin_src emacs-lisp :tangle .emacs.d/early-init.el
;;;
;;; 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_src
Don't clutter the current directory with backups. Save them in a separate directory.
#+begin_src emacs-lisp
(setq backup-directory-alist '(("." . "~/.emacs.d/backups")))
#+end_src
Don't clutter the current directory with auto-save files.
#+begin_src emacs-lisp
(setq auto-save-file-name-transforms '((".*" "~/.emacs.d/backups/" t)))
#+end_src
Do not create lockfiles either. (I am the only user in the system and only use emacs through daemon, so that should be ok.)
#+begin_src emacs-lisp
(setq create-lockfiles nil)
#+end_src
** Helpers
Emacs lisp helper functions.
Timestamp-ids are used to uniquely identify things.
#+begin_src emacs-lisp
(defun rasen/tsid (&optional time)
"Return timestamp-id."
(format-time-string "%Y%m%d%H%M%S" time "UTC"))
(defun rasen/insert-tsid ()
"Insert timestamp-id at point."
(interactive)
(insert (rasen/tsid)))
#+end_src
Insert current date in =yyyy-mm-dd= format. Useful when creating dated notes or dumb commits.
#+begin_src emacs-lisp
(defun rasen/insert-date (arg)
"Insert current date. With prefix ARG, insert time in ISO 8601 format as well. With double-prefix, insert time in UTC timezone."
(interactive "p")
(insert (format-time-string
(if (> arg 1)
"%FT%T%z"
"%F")
nil
(if (equal arg 16) t nil))))
(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_src
Shamelessly stolen from https://github.com/purcell/emacs.d.
#+begin_src emacs-lisp
(defun rename-this-file-and-buffer (new-name)
"Renames both current buffer and file it's visiting to NEW-NAME."
(interactive "FNew name: ")
(let ((name (buffer-name))
(filename (buffer-file-name)))
(unless filename
(error "Buffer '%s' is not visiting file!" name))
(if (get-buffer new-name)
(message "A buffer named '%s' already exists!" new-name)
(progn
(make-directory (file-name-directory new-name) t)
(when (file-exists-p filename)
(rename-file filename new-name 1))
(rename-buffer new-name)
(set-visited-file-name new-name)))))
(defun delete-this-file-and-buffer ()
"Delete the current file, and kill the buffer."
(interactive)
(or (buffer-file-name) (error "No file is currently being edited"))
(when (yes-or-no-p (format "Really delete '%s'?"
(file-name-nondirectory buffer-file-name)))
(delete-file (buffer-file-name))
(kill-buffer)))
#+end_src
#+begin_src emacs-lisp
(defun add-to-path (str)
"Add an STR to the PATH environment variable."
(setenv "PATH" (concat str ":" (getenv "PATH"))))
#+end_src
** 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_src
Do 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_src
Do not show ~./~ and ~../~ during file name completion.
#+name: ivy-config
#+begin_src emacs-lisp :tangle no
(setq-default ivy-extra-directories nil)
#+end_src
The 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_src
Evilify 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_src
Install 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_src
Automatically add a final newline in files.
#+begin_src emacs-lisp
(setq-default require-final-newline t)
#+end_src
* Environment
** EXWM
Emacs is my Window Manager, thanks to [[https://github.com/ch11ng/exwm][EXWM]].
NixOS has an EXWM module, but my feeling is that it's too limiting. (~<>~)
#+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_src
Initialize EXWM configuration (emacs-lisp)
#+begin_src emacs-lisp
(use-package exwm
:init
;; these must be set before exwm is loaded
(setq mouse-autoselect-window t
focus-follows-mouse t)
:config
;; the next two make all buffers available on all workspaces
(setq exwm-workspace-show-all-buffers t)
(setq exwm-layout-show-all-buffers t)
;; Make class name the buffer name
(add-hook 'exwm-update-class-hook
(lambda ()
(exwm-workspace-rename-buffer exwm-class-name)))
(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_src
Add a couple of helpers functions. (emacs-lisp)
#+begin_src emacs-lisp
(defun rasen/autostart (cmd)
"Start CMD unless already running."
(let ((buf-name (concat "*" cmd "*")))
(unless (process-live-p (get-buffer-process buf-name))
(start-process-shell-command cmd buf-name cmd))))
(defun rasen/start-command (command &optional buffer)
"Start shell COMMAND in the background. If BUFFER is provided, log process output to that buffer."
(interactive (list (read-shell-command "Run: ")))
(start-process-shell-command command buffer command))
(defun rasen/switch-start (buffer cmd)
"Switch to buffer with name BUFFER or start one with CMD."
(if-let (b (get-buffer buffer))
(switch-to-buffer b)
(rasen/start-command cmd)))
(defun rasen/exwm-input-set-key (key command)
"Similar to `exwm-input-set-key', but always refreshes prefix
keys. This allows defining keys from any place in config."
(exwm-input-set-key key command)
;; 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_src
Bind ~s-M-l~ to lock screen immediately.
#+begin_src emacs-lisp
(defun rasen/blank-screen ()
"Blank screen after 1 second. The delay is introduced so the user
could get their hands away from the keyboard. Otherwise, the screen
would lit again immediately."
(interactive)
(run-at-time "1 sec" nil
(lambda ()
(rasen/start-command "xset dpms force off"))))
(defun rasen/lock-screen ()
"Lock and blank screen."
(interactive)
(rasen/start-command "slock")
(rasen/blank-screen))
(rasen/exwm-input-set-key (kbd "s-M-l") #'rasen/lock-screen)
#+end_src
** System tray
Use built-in EXWM system tray (emacs-lisp)
#+begin_src emacs-lisp
(use-package exwm-systemtray
:after exwm
:config
(exwm-systemtray-mode))
#+end_src
** Screenshots
I use [[https://github.com/Roger/escrotum][Escrotum]] for screenshots.
Install it. (~<>~)
#+name: home-manager-section
#+begin_src nix
{
home.packages = pkgs.lib.linux-only [ pkgs.escrotum ];
}
#+end_src
Bind it to Print Screen button. (emacs-lisp)
#+begin_src emacs-lisp
(defun rasen/screenshot ()
(interactive)
;; -sC — choose selection + save to clipboard
(rasen/start-command "escrotum -sC"))
(rasen/exwm-input-set-key (kbd "") #'rasen/screenshot)
#+end_src
** Misc
I definitely use X server:
#+name: nixos-section
#+begin_src nix
{
services.xserver.enable = true;
}
#+end_src
Use English as my only supported locale:
#+name: nixos-section
#+begin_src nix
{
i18n.supportedLocales = [ "en_US.UTF-8/UTF-8" ];
}
#+end_src
Setup 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_src
Same setting but for Home Manager (~<>~)
#+name: home-manager-section
#+begin_src nix
{
home.keyboard = {
layout = "us,ua";
variant = "workman,";
};
}
#+end_src
Map left Caps Lock to Ctrl, and left Ctrl to switch between layout. (Shift-Ctrl triggers Caps Lock function.)
I never use Caps Lock–the feature, so it's nice to have Caps LED indicate alternate layouts.
~<>~:
#+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_src
On macOS, right option acts as AltGr. Make Emacs ignore it, so it can work correctly as a Meta modifier:
#+begin_src emacs-lisp
(setq ns-right-alternate-modifier 'none)
#+end_src
*** Xkeymap
I have a slightly customized Workman+Ukrainian layout at [[./Xkeymap]] (more keys on 3rd level). It's quite big and isn't particularly fun to explain, so I keep it off my main config.
Activate it on session start (~<>~).
#+name: home-manager-section
#+begin_src nix
{
xsession.initExtra = ''
xkbcomp ${./Xkeymap} $DISPLAY
'';
}
#+end_src
One caveat is that it's dropped when I activate (update) new system version, or when unplug keyboard and plug it again.
Add a small Emacs function to re-apply this configuration (emacs-lisp).
#+begin_src emacs-lisp
(defun rasen/set-xkb-layout ()
(interactive)
(rasen/autostart "xkbcomp ~/dotfiles/Xkeymap $DISPLAY"))
(rasen/set-xkb-layout)
#+end_src
Install 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_src
Install network manager applet for user. (~<>~)
#+name: home-manager-section
#+begin_src nix
{
home.packages = pkgs.lib.linux-only [ pkgs.networkmanagerapplet ];
}
#+end_src
Auto-start ~nm-applet~ (emacs-lisp)
#+begin_src emacs-lisp
(rasen/autostart "nm-applet")
#+end_src
** SSH
(~<>~)
#+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=1000
no-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_src
On 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/"
# Mail
"/home/rasen/Mail/"
"/home/rasen/.mbsync/"
];
exclude = [
# Scanning notmuch takes too much time and doesn't make much
# sense as it is easily replicable
"/home/rasen/Mail/.notmuch"
];
};
# Start backup on boot if missed one while laptop was off
systemd.timers.borgbackup-job-all.timerConfig = {
Persistent = lib.mkForce true;
};
})
#+end_src
Mount external drive when needed.
#+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_src
Better (?) 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_src
For Home Manager–managed hosts.
#+name: home-manager-section
#+begin_src nix
{
home.packages = pkgs.lib.linux-only [ pkgs.acpilight ];
}
#+end_src
Bind it to keys (emacs-lisp).
#+begin_src emacs-lisp
(rasen/exwm-input-set-key (kbd "")
(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_src
Bind 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_src
Execute the following command to update firmware.
#+begin_src sh :tangle no
fwupdmgr get-updates
#+end_src
* Browsers
Firefox is default, Chrome for backup.
#+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 tridactylsync
bind J scrollline -10
bind K scrollline 10
bind j scrollline -2
bind k scrollline 2
#+end_src
Link tridactyl config to the place the tridactyl can find it. (~<>~)
#+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_src
Swap =.= 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_src
Close other window.
#+begin_src emacs-lisp
(defun rasen/quit-other ()
(interactive)
(other-window 1)
(quit-window))
(s-leader-def
"q" #'rasen/quit-other)
#+end_src
Move 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_src
Save 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_src
Now, 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-m