Initial release of my NixOS configuration

This commit is contained in:
Konstantin Nazarov 2023-07-30 17:18:49 +01:00
commit 5f271a7194
Signed by: knazarov
GPG key ID: 4CFE0A42FA409C22
20 changed files with 2341 additions and 0 deletions

20
.sops.yaml Normal file
View file

@ -0,0 +1,20 @@
keys:
- &admin_knazarov DDB4423999505236CF585F9B0560020C9C577C1B
- &server_mira age1le98v5v0xnlnc4y0ydgj9kwfftt8g5wduws8zsadgc97pj0fzecs55tjvz
- &server_framework age1rkmhgep2jhdnma24x7ufzr686cwq6p3nk7mmedykan0d7c36xaus2y58sw
- &server_knazarovcom age1esdg28lplhhvrj6vmqu9x0adyxj5trp2dp7my3k57kjhkstkk9cqkg5qkj
creation_rules:
- path_regex: secrets\.yaml$
key_groups:
- pgp:
- *admin_knazarov
age:
- *server_mira
- *server_framework
- path_regex: secrets-knazarovcom\.yaml$
key_groups:
- pgp:
- *admin_knazarov
age:
- *server_knazarovcom

19
README.md Normal file
View file

@ -0,0 +1,19 @@
# My NixOS configuration
This configuration is used to provision both of my "desktop" machines, and a personal website.
Most important features this configuration provides:
- PGP, commit signing, U2F
- Secret management with [SOPS](https://github.com/getsops/sops)
- Email / mbsync configuration
- VPN with [Mullvad](https://mullvad.net)
# Updating machine configuration
There are solutions that allow to push configuration to remote hosts, but I find them a bit heavy,
so a simple shell script does the trick for me:
```
./switch.sh <machine name>
```

572
configuration.nix Normal file
View file

@ -0,0 +1,572 @@
# Edit this configuration file to define what should be installed on
# your system. Help is available in the configuration.nix(5) man page
# and in the NixOS manual (accessible by running nixos-help).
{ config, lib, nixpkgs, pkgs, home-manager, ... }:
let
in
{
imports =
[
#./gnupg.nix
];
nix.settings.experimental-features = [ "nix-command" "flakes" ];
nix.extraOptions = ''
!include ${config.sops.secrets.github_token.path}
bash-prompt = (nix:$name)\040\[\033[1;32m\][\u@\h:\w]\$\[\033[0m\]\040
'';
sops = {
environment.SOPS_GPG_EXEC = "${pkgs.gnupg}/bin/gpg";
defaultSopsFile = ./secrets.yaml;
secrets = {
fastmail_password = {
owner = config.users.users.knazarov.name;
group = config.users.users.knazarov.group;
};
github_token = {
owner = config.users.users.knazarov.name;
group = config.users.users.knazarov.group;
};
mullvad_account = {};
};
};
# networking.wireless.enable = true; # Enables wireless support via wpa_supplicant.
# Configure network proxy if necessary
# networking.proxy.default = "http://user:password@proxy:port/";
# networking.proxy.noProxy = "127.0.0.1,localhost,internal.domain";
# Enable networking
networking.networkmanager.enable = true;
# Set your time zone.
time.timeZone = "Europe/London";
# Select internationalisation properties.
i18n.defaultLocale = "en_US.UTF-8";
i18n.extraLocaleSettings = {
LC_ADDRESS = "en_US.UTF-8";
LC_IDENTIFICATION = "en_US.UTF-8";
LC_MEASUREMENT = "en_US.UTF-8";
LC_MONETARY = "en_US.UTF-8";
LC_NAME = "en_US.UTF-8";
LC_NUMERIC = "en_US.UTF-8";
LC_PAPER = "en_US.UTF-8";
LC_TELEPHONE = "en_US.UTF-8";
LC_TIME = "en_US.UTF-8";
};
# Mainly to access SMB shares on local network
services.gvfs = {
enable = true;
package = lib.mkForce pkgs.gnome3.gvfs;
};
# Configure keymap in X11
services.xserver = {
layout = "us";
xkbVariant = "";
};
# Define a user account. Don't forget to set a password with passwd.
users.users.knazarov = {
isNormalUser = true;
description = "Konstantin Nazarov";
extraGroups = [
"networkmanager"
"wheel"
config.users.groups.keys.name
];
packages = with pkgs; [];
openssh.authorizedKeys.keys = [
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDGxebDydOcs7URJjXFHMU++ruaZOJpXbK4ixH19pWTsX7WtxxriZxD4+RQ3oyllGG/8sEFzEe0NoTHUPU6YrBpfwT/ekGDmCJHtvZ+rZs+cRQd6tObfAUip1B1Mcvhuaj0prnrbfohOuHpvQ/L8TogIKuHgczDmud4KGUu0mxCsUHbD5tlKpsgN+dJXkvjxsO7JhhF9JpFTrYAU0gTuBPTt3ynpnZKrE1NgnE0iy+CEr/v41dLqxw3fUjT3nOFUQ1l/VKTw5mLt5Iw7XmBLuFGLRAVrwzXxeBCfYqKGYgY4QV8HCcVpcqC8zWmRskiRetzQ/5HwRagm4yZr0I+LZ305nGB0cSJzLWXXOUF6SDg2cqAXFpF/o2LoFCmaV5h3jmCGOUrowF7oV4mYwBMWfabrbZx21z/R56GkAOOEKc2h+Qh5wIj4yayX081SkqJK3J9+3vGG4VvXnwGnPnWQFqrzeedyV74maffGBGFYm0UOcD+oG6EwM+7MEUBpJm9m4c= knazarov"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHkDvP2BO1uV0AwEjABYFEiA2BbGo1IsSht4emYMRLgi root@mira"
];
};
nixpkgs.config.allowUnfree = true;
environment.systemPackages = with pkgs; [
# needed to request polkit access (for instance, for SMB shares)
lxqt.lxqt-policykit
vim
waybar
foot
wayland
xdg-utils
glib
dracula-theme
gnome3.adwaita-icon-theme
swaylock
swayidle
wl-clipboard
qutebrowser
keyd
tdesktop
git
source-code-pro
pavucontrol
brightnessctl
sops
age
ssh-to-age
syncthing
pass
pkgs.gnupg
pciutils
slack
q-sh
transmission-gtk
mpv
imv
okular
yt-dlp
evince # document viewer
firefox
gthumb
unzip
somafm-cli
yubikey-manager
yubikey-manager-qt
gnome.gedit # temporary
ripgrep
file
zoom-us
obs-studio
gnome.nautilus
xfce.thunar
zig
morph
gomuks
nheko
mullvad-vpn
mullvad
clang-tools # mainly for clang-format
(emacsWithPackagesFromUsePackage {
config = ./emacs.el;
defaultInitFile = true;
package = emacs-unstable-pgtk.overrideAttrs (old: {
withTreeSitter = true;
});
alwaysEnsure = true;
extraEmacsPackages = epkgs: [
pkgs.mu
epkgs.treesit-grammars.with-all-grammars
];
})
# wget
];
services.mullvad-vpn = {
enable = true;
};
systemd.services."mullvad-daemon".postStart = let
mullvad = config.services.mullvad-vpn.package;
in ''
while ! ${mullvad}/bin/mullvad status >/dev/null; do sleep 1; done
${mullvad}/bin/mullvad account login `cat /var/run/secrets/mullvad_account`
${mullvad}/bin/mullvad auto-connect set on
${mullvad}/bin/mullvad tunnel ipv6 set on
'';
services.gnome.gnome-keyring.enable = true;
services.emacs.package = nixpkgs.emacsUnstablePgtk;
# Enables wayland support in electron apps (e.g. slack)
environment.sessionVariables.NIXOS_OZONE_WL = "1";
# Set default browser to qutebrowser in electron apps
environment.sessionVariables.DEFAULT_BROWSER = "${pkgs.qutebrowser}/bin/qutebrowser";
# Set default browser to qutebrowser everywhere else
xdg.mime.defaultApplications = {
"text/html" = "org.qutebrowser.qutebrowser.desktop";
"x-scheme-handler/http" = "org.qutebrowser.qutebrowser.desktop";
"x-scheme-handler/https" = "org.qutebrowser.qutebrowser.desktop";
"x-scheme-handler/about" = "org.qutebrowser.qutebrowser.desktop";
"x-scheme-handler/unknown" = "org.qutebrowser.qutebrowser.desktop";
};
# Enable screen sharing on Wayland
xdg = {
portal = {
enable = true;
extraPortals = with pkgs; [
xdg-desktop-portal-wlr
xdg-desktop-portal-gtk
];
};
};
# Some programs need SUID wrappers, can be configured further or are
# started in user sessions.
# programs.mtr.enable = true;
programs.gnupg.package = pkgs.gnupg;
programs.gnupg.agent = {
enable = true;
enableSSHSupport = true;
};
programs.sway = {
enable = true;
wrapperFeatures.gtk = true;
};
# List services that you want to enable:
services.pipewire = {
enable = true;
alsa.enable = true;
alsa.support32Bit = true;
pulse.enable = true;
};
services.dbus.enable = true;
services.greetd = {
enable = true;
settings = rec {
initial_session = {
command = "${pkgs.sway}/bin/sway";
user = "knazarov";
};
default_session = initial_session;
};
};
services.keyd = {
enable = true;
keyboards = {
default = {
ids = [ "*" ];
settings = {
main = {
capslock = "overload(control, esc)";
leftalt = "layer(meta_mac)";
leftmeta = "layer(alt)";
};
"meta_mac:M" = {
c = "C-insert";
v = "S-insert";
};
};
};
};
};
# Enable the OpenSSH daemon.
services.openssh = {
enable = true;
# settings = {
# passwordAuthentication = false;
# kbdInteractiveAuthentication = false;
# };
};
networking.firewall.allowedTCPPorts = [
# Syncthing
8384 22000
];
networking.firewall.allowedUDPPorts = [
# Syncthing
22000 21027
];
# This value determines the NixOS release from which the default
# settings for stateful data, like file locations and database versions
# on your system were taken. Its perfectly fine and recommended to leave
# this value at the release version of the first install of this system.
# Before changing this value read the documentation for this option
# (e.g. man configuration.nix or on https://nixos.org/nixos/options.html).
system.stateVersion = "23.05"; # Did you read the comment?
# needed for sway
security.polkit.enable = true;
# needed for pipewire
security.rtkit.enable = true;
# allow remote rebuilds
nix.settings.trusted-users = [ "@wheel" ];
# needed for YubiKey smartcard support
services.pcscd.enable = true;
fonts.fontDir.enable = true;
fonts.packages = with pkgs; [
dejavu_fonts
source-code-pro
font-awesome # for waybar indicators
];
home-manager.users.knazarov = {
/* The home.stateVersion option does not have a default and must be set */
home.stateVersion = "23.05";
programs.bash = {
enable = true;
bashrcExtra = ''
if [[ "$INSIDE_EMACS" = 'vterm' ]] \
&& [[ -n ''${EMACS_VTERM_PATH} ]] \
&& [[ -f ''${EMACS_VTERM_PATH}/etc/emacs-vterm-bash.sh ]]; then
source ''${EMACS_VTERM_PATH}/etc/emacs-vterm-bash.sh
fi
'';
};
programs.direnv = {
enable = true;
nix-direnv = {
enable = true;
};
};
programs.gpg = {
enable = true;
package = pkgs.gnupg;
publicKeys = [{source = ./gpg_public_key.asc; trust="ultimate"; }];
settings = {
default-key = "0x0560020C9C577C1B";
};
mutableKeys = false;
mutableTrust = false;
};
programs.git = {
enable = true;
userName = "Konstantin Nazarov";
userEmail = "mail@knazarov.com";
signing = {
gpgPath = "${pkgs.gnupg}/bin/gpg2";
key = "0x0560020C9C577C1B";
signByDefault = true;
};
};
accounts.email = {
maildirBasePath = "${config.users.users.knazarov.home}/Maildir";
accounts = {
personal = let account = "mail@knazarov.com"; in {
primary = true;
flavor = "fastmail.com";
address = account;
userName = account;
realName = "Konstantin Nazarov";
passwordCommand = "cat /run/secrets/fastmail_password";
gpg = {
key = "0x0560020C9C577C1B";
signByDefault = true;
};
mu.enable = true;
msmtp.enable = true;
mbsync = {
enable = true;
# Folders existing on the server, but not locally, will be created.
create = "maildir";
};
};
};
};
programs.mu = {
enable = true;
};
programs.msmtp.enable = true;
programs.mbsync = {
enable = true;
};
#services.easyeffects = {
# enable = true;
#};
services.swayidle = {
enable = true;
timeouts = [
{ timeout = 300; command = "${pkgs.swaylock}/bin/swaylock -f -c 000000";}
{ timeout = 600;
command = "${pkgs.sway}/bin/swaymsg \"output * dpms off\"";
resumeCommand = "${pkgs.sway}/bin/swaymsg \"output * dpms on\"";}
];
events = [
{ event = "before-sleep"; command = "${pkgs.swaylock}/bin/swaylock -f -c 000000"; }
];
};
wayland.windowManager.sway = {
enable = true;
xwayland = true;
config = rec {
modifier = "Mod4";
terminal = "foot";
bars = [{
"command" = "waybar";
}];
keybindings = pkgs.lib.mkOptionDefault {
"XF86AudioRaiseVolume" = "exec wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+";
"XF86AudioLowerVolume" = "exec wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-";
"XF86AudioMute" = "exec wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle";
"XF86AudioMicMute" = "exec pactl set-source-mute @DEFAULT_SOURCE@ toggle";
"XF86MonBrightnessUp" = "exec brightnessctl s +5%";
"XF86MonBrightnessDown" = "exec brightnessctl s 5%-";
"Mod4+Return" = "exec emacs --eval '(progn (setq confirm-kill-processes nil) (vterm))'";
"Mod4+space" = "exec ${pkgs.foot}/bin/foot -T mylauncher -a mylauncher ${pkgs.q-sh}/bin/q";
"Mod4+p" = "exec '${pkgs.grim}/bin/grim -g \"$$(${pkgs.slurp}/bin/slurp)\" - | ${pkgs.wl-clipboard}/bin/wl-copy -t image/png'";
};
output = {
"*" = { bg = "${./wallpaper.jpg} fill";};
"Lenovo Group Limited LEN T32p-20 VNA4VRNY" = { scale = "1.5"; };
};
input = {
"type:keyboard" = {
xkb_layout = "us,ru";
xkb_options = "grp:alt_space_toggle";
};
};
gaps = {
inner = 10;
outer = 5;
};
colors.unfocused = {border = "#dddddd";
background = "#dddddd";
text = "#888888";
indicator = "#888888";
childBorder = "#888888";};
};
extraConfig = ''
for_window [title="mylauncher"] floating enable
default_border pixel 3
'';
};
programs.foot = {
enable = true;
settings = {
main = {
font="monospace:size=10";
};
colors = {
background="feffff";
foreground="333333";
regular0="333333";
regular1="bd4436";
regular2="008700";
regular3="cdcc42";
regular4="0017f5";
regular5="b92cc6";
regular6="5bbdf9";
regular7="c7c7c7";
bright0="333333";
bright1="bd4436";
bright2="008700";
bright3="cdcc42";
bright4="0017f5";
bright5="b92cc6";
bright6="5bbdf9";
bright7="feffff";
};
key-bindings = {
clipboard-copy="Control+Insert";
clipboard-paste="Shift+Insert";
primary-paste="Control+Shift+v";
};
};
};
programs.qutebrowser = {
enable = true;
searchEngines = {
DEFAULT = "https://kagi.com/search?q={}";
g = "https://www.google.com/search?hl=en&q={}";
};
#config.set('content.media.video_capture', True, 'https://www.meet.google.com')
settings = {
tabs.position = "left";
content.cookies.accept = "no-3rdparty";
};
extraConfig = ''
config.set('content.javascript.can_access_clipboard', True, 'amazon.com')
config.set('content.javascript.can_access_clipboard', True, 'awsapps.com')
config.set('content.media.audio_capture', True, 'https://meet.google.com')
config.set('content.media.video_capture', True, 'https://meet.google.com')
config.set('content.media.audio_video_capture', True, 'https://meet.google.com')
config.set('content.notifications.enabled', False, 'https://meet.google.com')
config.set('content.register_protocol_handler', False, 'https://calendar.google.com?cid=%25s')
'';
keyBindings = {
insert = {
"<Shift+Ins>" = "insert-text -- {clipboard}";
};
};
};
programs.waybar = {
enable = true;
settings = [{
layer = "top";
position = "top";
height = 24;
modules-left = ["sway/workspaces" "sway/mode"];
modules-center = ["sway/window"];
modules-right =
[ "idle_inhibitor" "battery" "clock" "tray" ];
clock.format = "{:%Y-%m-%d %H:%M}";
battery = {
states = {
# good = 95;
warning = 30;
critical = 15;
};
format = "{capacity}% {icon}";
format-charging = "{capacity}% ";
format-plugged = "{capacity}% ";
format-alt = "{time} {icon}";
format-icons = ["" "" "" "" ""];
};
idle_inhibitor = {
format = "{icon}";
format-icons = {
activated = "";
deactivated = "";
};
};
}];
};
services.syncthing = {
enable = true;
};
home.pointerCursor = {
name = "Adwaita";
package = pkgs.gnome.adwaita-icon-theme;
size = 24;
x11 = {
enable = true;
defaultCursor = "Adwaita";
};
};
};
virtualisation = {
podman = {
enable = true;
dockerCompat = true;
defaultNetwork.settings = {
dns_enabled = true;
};
};
};
services.udev.packages = [
pkgs.android-udev-rules
];
}

929
emacs.el Executable file
View file

@ -0,0 +1,929 @@
;; -------- Speed up load time -------
;; I don't use emacs-server, so startup times are very important to me.
;; Garbage collection is triggered very often during start up, and it
;; slows the whole thing down. It is safe to increase threshold
;; temporarily to prevent aggressive GC, and then re-enable it at the
;; end.
(setq gc-cons-threshold 402653184
gc-cons-percentage 0.6)
;; There are special ways to handle files (via SSH or in archives),
;; but this is not necessary during startup, and it also slows down
;; the load significantly, as emacs is going through lots of files.
(defvar saved--file-name-handler-alist file-name-handler-alist)
(setq file-name-handler-alist nil)
;; Restore defaults after initialization has completed
(add-hook 'after-init-hook #'(lambda ()
(setq gc-cons-threshold 16777216
gc-cons-percentage 0.1)
(setq file-name-handler-alist saved--file-name-handler-alist)))
;; -------- Default directories --------
(setq default-directory "~/")
(setq command-line-default-directory "~/")
;; -------- State files --------
;; By default emacs leaves lots of trash around your filesystem while
;; you are editing. This section cleans up the basics.
;; Don't leave =yourfile~= temporary files nearby, and put them to a
;; separate directory instead.
(setq backup-directory-alist '(("." . "~/.emacs.d/backups")))
(setq auto-save-file-name-transforms
'((".*" "~/.emacs.d/backups" t)))
;; -------- Command history --------
;; Save command history so that when emacs is restarted, the history
;; is preserved.
(setq savehist-file "~/.emacs.d/savehist")
(savehist-mode +1)
(setq savehist-save-minibuffer-history +1)
(setq savehist-additional-variables
'(kill-ring
search-ring
regexp-search-ring))
;; -------- Recent files --------
;; Recent files are convenient to record because you can use them to
;; quickly jump to what you've been editing recently.
(setq recentf-save-file "~/.emacs.d/recentf"
recentf-max-menu-items 0
recentf-max-saved-items 300
recentf-filename-handlers '(file-truename)
recentf-exclude
(list "^/tmp/" "^/ssh:" "\\.?ido\\.last$" "\\.revive$" "/TAGS$"
"^/var/folders/.+$"
))
(recentf-mode 1)
;; -------- De-clutter --------
;; Toolbar and scrollbars are only useful to novices. The same for
;; startup screen and menu bar.
(tool-bar-mode -1)
(scroll-bar-mode -1)
(setq inhibit-startup-screen t)
(if (not (eq window-system 'mac))
(menu-bar-mode -1))
;; More reliable inter-window border
;; The native border "consumes" a pixel of the fringe on righter-most splits,
;; ~window-divider~ does not. Available since Emacs 25.1.
(setq-default window-divider-default-places t
window-divider-default-bottom-width 0
window-divider-default-right-width 1)
(window-divider-mode +1)
;; Remove continuation arrow on right fringe
(setq fringe-indicator-alist (delq (assq 'continuation fringe-indicator-alist)
fringe-indicator-alist))
;; No more typing the whole yes or no. Just y or n will do.
(fset 'yes-or-no-p 'y-or-n-p)
;; Makes *scratch* empty.
(setq initial-scratch-message "")
;; Hide modeline in the vterm mode
(use-package hide-mode-line)
(add-hook 'vterm-mode-hook #'hide-mode-line-mode)
;; -------- Cursor and movement --------
;; On emacs mac port use Alt as meta key
(if (eq window-system 'mac)
(progn
(setq mac-option-modifier 'meta)
(setq mac-command-modifier nil)
(setq mac-pass-command-to-system 't)))
;; Blinking cursor is inconvenient
(blink-cursor-mode -1)
;; Disable bell ring when moving outside of available area
(setq ring-bell-function 'ignore)
;; Disable annoying blink-matching-paren
(setq blink-matching-paren nil)
;; -------- Window decoration --------
;; This makes the header transparent on Emacs 26.1+ under OS X
(add-to-list 'default-frame-alist '(ns-transparent-titlebar . t))
(add-to-list 'default-frame-alist '(ns-appearance . dark))
(setq ns-use-proxy-icon nil)
;; -------- Minor modes --------
;; Hide some miror modes from sight to not clutter the modeline
(use-package diminish)
(require 'diminish)
(diminish 'company-mode)
(diminish 'projectile-mode)
(diminish 'editorconfig-mode)
(diminish 'eldoc-mode)
(diminish 'flycheck-mode)
(diminish 'which-key-mode)
;; -------- Theme --------
;; - I don't like that fringes are visible, so I set them to regular
;; background color
;; - Panels look better without outset/inset shadows
(use-package modus-themes)
(load-theme 'modus-operandi t)
(set-face-attribute 'fringe nil
:foreground (face-foreground 'default)
:background (face-background 'default))
;; On many OSs the modeline has an outset border (lighter on top and
;; darker on the bottom). This doesn't look pretty on a flat theme.
(set-face-attribute 'mode-line nil :box nil)
(set-face-attribute 'mode-line-inactive nil :box nil)
;; -------- Font --------
;; Some time ago I've purchased a great font called Pragmata Pro,
;; which is easy on the eyes and tailored for programmers. It may
;; not be available everywhere though, hence conditional load.
(when window-system
(if (not (null (x-list-fonts "PragmataPro")))
(add-to-list 'default-frame-alist
'(font . "PragmataPro-15"))
(add-to-list 'default-frame-alist
'(font . "Source Code Pro-11"))
))
;; -------- Packages --------
(diminish 'company-mode)
(diminish 'projectile-mode)
(diminish 'editorconfig-mode)
(diminish 'eldoc-mode)
(diminish 'flycheck-mode)
(diminish 'which-key-mode)
;; -------- Navigation --------
;; Quickly find my way around emacs
;; Default scheme for uniquifying buffer names is not convenient.
;; It's better to have a regular path-like structure.
;;(require 'uniquify)
(setq uniquify-buffer-name-style 'forward)
(setq uniquify-separator "/")
(setq uniquify-after-kill-buffer-p t) ; rename after killing uniquified
(setq uniquify-ignore-buffers-re "^\\*") ; don't muck with special buffers
;; If you stop after typing a part of keybinding, shows available
;; options in minibuffer.
(use-package which-key)
(add-hook 'after-init-hook 'which-key-mode)
(with-eval-after-load 'which-key
(which-key-setup-side-window-bottom))
;; persp-mode allows you to have tagged workspaces akin to
;; Linux tiled-window managers.
(use-package persp-mode)
;; persp-mode clashes with corfu
(setq persp-auto-resume-time 0)
(add-hook 'after-init-hook 'persp-mode)
(global-set-key (kbd "C-x x s") 'persp-switch)
;; vertico-mode allows for easy navigation between buffers and files
(use-package vertico)
(add-hook 'after-init-hook 'vertico-mode)
(setq completion-ignore-case t)
(setq completion-styles '(basic substring partial-completion flex))
(use-package marginalia)
(add-hook 'after-init-hook 'marginalia-mode)
;; ripgrep search with consult
(use-package consult)
(global-set-key (kbd "M-s r") 'consult-ripgrep)
;; Navigation when in russian layout
(cl-loop
for from across "йцукенгшщзхъфывапролджэячсмитьбюЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЯЧСМИТЬБЮ№"
for to across "qwertyuiop[]asdfghjkl;'zxcvbnm,.QWERTYUIOP{}ASDFGHJKL:\"ZXCVBNM<>#"
do
(eval `(define-key key-translation-map (kbd ,(concat "C-" (string from))) (kbd ,(concat "C-" (string to)))))
(eval `(define-key key-translation-map (kbd ,(concat "M-" (string from))) (kbd ,(concat "M-" (string to))))))
;; -------- Editor basics --------
;; Use 4 spaces to indent by default
(setq-default indent-tabs-mode nil)
(setq-default tab-width 4)
;; Clean up trailing whitespace on file save
(add-hook 'before-save-hook 'whitespace-cleanup)
;; But use editorconfig to guess proper project-wide indentation rules
(use-package editorconfig)
(add-hook 'prog-mode-hook #'editorconfig-mode)
;; Speed up comint buffers by disabling bidirectional language support
(setq-default bidi-display-reordering nil)
;; -------- Tools and environment --------
;; By default, Emacs doesn't add system path to its search places
(use-package exec-path-from-shell)
(require 'exec-path-from-shell)
(setenv "PATH" (concat "/usr/local/bin:" (getenv "PATH")))
;; On a mac, this will set up PATH and MANPATH from your environment
(when (memq window-system '(mac ns x pgtk))
(exec-path-from-shell-initialize))
;; -------- Org roam --------
(use-package org)
(use-package org-contrib)
(use-package org-roam)
(use-package consult-org-roam)
(setq org-roam-completion-everywhere t)
(setq org-roam-directory "~/notes")
(setq org-roam-node-default-sort 'file-mtime)
(setq consult-org-roam-grep-func #'consult-ripgrep)
(global-set-key (kbd "C-c n l") 'org-roam-buffer-toggle)
(global-set-key (kbd "C-c n i") 'org-roam-node-insert)
(global-set-key (kbd "C-c n f") 'org-roam-node-find)
(global-set-key (kbd "C-c n r") 'consult-org-roam-search)
(autoload 'org-roam-buffer-toggle "org-roam" "\
Enable org-roam
" t nil)
(autoload 'org-roam-setup "org-roam" "\
Enable org-roam
" t nil)
(setq org-roam-capture-templates
`(("d" "default" plain "%?"
:target (file+head "%<%Y%m%d%H%M%S>-${slug}.org"
"#+title: ${title}\n")
:unnarrowed t)
("i" "interview" plain "%?"
:target (file+head "%<%Y%m%d%H%M%S>-${slug}.org"
,(concat "#+title: ${title}\n"
"#+filetags: Interview\n"
"\n"))
:unnarrowed t)
))
(setq org-roam-node-display-template
(concat "${title:80} " (propertize "${tags:20}" 'face 'org-tag))
org-roam-node-annotation-function
(lambda (node) (marginalia--time (org-roam-node-file-mtime node))))
(add-hook 'after-init-hook 'org-roam-setup)
(defun roam-extra:get-filetags ()
(split-string (or (org-roam-get-keyword "filetags") "")))
(defun roam-extra:add-filetag (tag)
(let* ((new-tags (cons tag (roam-extra:get-filetags)))
(new-tags-str (combine-and-quote-strings new-tags)))
(org-roam-set-keyword "filetags" new-tags-str)))
(defun roam-extra:del-filetag (tag)
(let* ((new-tags (seq-difference (roam-extra:get-filetags) `(,tag)))
(new-tags-str (combine-and-quote-strings new-tags)))
(org-roam-set-keyword "filetags" new-tags-str)))
(defun roam-extra:todo-p ()
"Return non-nil if current buffer has any TODO entry.
TODO entries marked as done are ignored, meaning the this
function returns nil if current buffer contains only completed
tasks."
(org-element-map
(org-element-parse-buffer 'headline)
'headline
(lambda (h)
(eq (org-element-property :todo-type h)
'todo))
nil 'first-match))
(defun roam-extra:update-todo-tag ()
"Update TODO tag in the current buffer."
(when (and (not (active-minibuffer-window))
(org-roam-file-p))
(org-with-point-at 1
(let* ((tags (roam-extra:get-filetags))
(is-todo (roam-extra:todo-p)))
(cond ((and is-todo (not (seq-contains-p tags "todo")))
(roam-extra:add-filetag "todo"))
((and (not is-todo) (seq-contains-p tags "todo"))
(roam-extra:del-filetag "todo")))))))
(defun roam-extra:todo-files ()
"Return a list of roam files containing todo tag."
(org-roam-db-sync)
(let ((todo-nodes (seq-filter (lambda (n)
(seq-contains-p (org-roam-node-tags n) "todo"))
(org-roam-node-list))))
(seq-uniq (seq-map #'org-roam-node-file todo-nodes))))
(defun roam-extra:update-todo-files (&rest _)
"Update the value of `org-agenda-files'."
(setq org-agenda-files (roam-extra:todo-files)))
(add-hook 'find-file-hook #'roam-extra:update-todo-tag)
(add-hook 'before-save-hook #'roam-extra:update-todo-tag)
(advice-add 'org-agenda :before #'roam-extra:update-todo-files)
(defun org-roam-week (&optional other-window)
"Opens or creates a weekly note."
(interactive)
(let* ((title (format-time-string "Week %U %Y"))
(nodes (org-roam-node-list))
(node (seq-find (lambda (n) (string-equal (org-roam-node-title n) title)) nodes)))
(if (and node (org-roam-node-file node))
(org-roam-node-visit node other-window)
(org-roam-capture-
:node (org-roam-node-create :title title)
:templates
`(("w" "week" plain "* Goals\n\n%?\n\n* Tasks\n\n"
:if-new (file+head "%<%Y%m%d%H%M%S>-${slug}.org" "#+title: ${title}\n#+category: ${title}\n#+filetags: Week")
:unnarrowed t))
:props '(:finalize find-file))
)
)
)
(global-set-key (kbd "C-c n w") 'org-roam-week)
;; -------- Org mode ---------
(use-package org-modern)
(with-eval-after-load 'org (global-org-modern-mode))
(setq org-modules '(org-w3m org-bbdb org-bibtex org-docview
org-gnus org-info org-irc org-mhe
org-rmail org-checklist org-mu4e))
;; Sometimes I sit at night until 4 AM, and I still want org to treat it
;; as "today"
(setq org-extend-today-until 4)
;; navigate with (org-goto) by offering the full list of targets in ido-mode
(setq org-goto-interface 'outline-path-completion) ;; don't search incrementally
(setq org-outline-path-complete-in-steps nil) ;; see whole path at once
;; avoid inadvertently editing hidden text
(setq org-catch-invisible-edits 'show-and-error)
;; hide empty spaces between folded subtrees
(setq org-cycle-separator-lines 0)
;; use hours in clocktable instead of days and hours
(setq org-time-clocksum-format "%d:%02d")
;; After a recurring task is marked as done, reset it to TODO. This
;; is important because I have the "INBOX" state first in the sequence
;; of states.
(setq org-todo-repeat-to-state "TODO")
;; Default format for column view
(setq org-columns-default-format "%38ITEM(Details) %TAGS(Context) %7TODO(To Do) %5Effort(Time){:} %6CLOCKSUM(Total){:}")
;; Better source code editing
(setq org-src-fontify-natively t
org-src-window-setup 'current-window
org-src-strip-leading-and-trailing-blank-lines t
org-src-preserve-indentation t
org-src-tab-acts-natively t)
;; Default tags
(setq org-tag-alist '((:startgroup . nil)
("WORK" . ?w) ("HOME" . ?h)
(:endgroup . nil)
("PROJECT" . ?p)
("PHONE" . ?n)
("MEETING" . ?m)
("DOC" . ?d)
("GOOGLE" . ?g)
("QUICK" . ?q)))
;; Default todo sequence
(setq org-todo-keywords
'((sequence "INBOX(i)" "TODO(t)" "ERRAND(k)"
"SOMEDAY(s)" "WAITING(w@/!)" "APPT(a)" "|"
"DONE(d!)" "CANCELLED(c!)")))
;; Babel and code block embedding
(setq org-confirm-babel-evaluate nil)
(org-babel-do-load-languages
'org-babel-load-languages
'((emacs-lisp . t)
(python . t)))
;; Log TODO state changes and clock-ins into the LOGBOOK drawer
(setq org-clock-into-drawer t)
(setq org-log-into-drawer t)
;; Quickly creating new tasks
(global-set-key (kbd "\C-c r") 'org-capture)
(setq org-capture-templates
`(("t" "Todo" entry (file+headline "~/org/gtd.org" "Tasks")
"* TODO %?\n %U\n %a")
("i" "Inbox" entry (file+headline "~/org/gtd.org" "Tasks")
"* INBOX %?\n %U")
("f" "Follow-up" entry (file+headline "~/org/gtd.org" "Tasks")
,(concat "* TODO %? :EMAIL:\n"
" %U\n"
" %a"))
("c" "Contacts" entry (file "~/org/contacts.org")
(concat "* %(org-contacts-template-name)\n"
":PROPERTIES:\n"
":EMAIL: %(org-contacts-template-email)\n"
":END:"))
("q"
"Org capture template"
entry
(file+headline "~/org/capture.org" "Notes")
"* %:description\n\n Source: %u, %:link\n\n %i"
:empty-lines 1)
)
)
;; org-protocol allows you to capture stuff into your system from web
;; browsers
(require 'org-protocol)
;; Refiling allows you to quickly move an element with its children to
;; another location.
;; By default, refile works up to 2-level sections, which is not very
;; convenient if you have project-based organization
;; (/Projects/ProjectName).
(setq org-refile-targets '((org-agenda-files :maxlevel . 3)))
;; Then, it's nice to have a full path to the target element appear in
;; completion
(setq org-refile-use-outline-path 'file)
;; But, when using helm, we also need to tell org mode to present the
;; whole list of possible completions right away, and not use
;; incremental search:
(setq org-outline-path-complete-in-steps nil)
;; It may also be useful to be able to create elements, if the refile
;; target doesn't already exist.
(setq org-refile-allow-creating-parent-nodes 'confirm)
;; Agenda
(global-set-key "\C-ca" 'org-agenda)
(setq org-agenda-files '("~/org/gtd.org"
"~/org/weeklyreview.org"
))
(setq org-agenda-custom-commands nil)
(add-to-list 'org-agenda-custom-commands
'("h" "Work todos" tags-todo
"-personal-doat={.+}-dowith={.+}/!-ERRAND"
((org-agenda-todo-ignore-scheduled t))))
(add-to-list 'org-agenda-custom-commands
'("H" "All work todos" tags-todo "-personal/!-ERRAND-MAYBE"
((org-agenda-todo-ignore-scheduled nil))))
(add-to-list 'org-agenda-custom-commands
'("A" "Work todos with doat or dowith" tags-todo
"-personal+doat={.+}|dowith={.+}/!-ERRAND"
((org-agenda-todo-ignore-scheduled nil))))
(add-to-list 'org-agenda-custom-commands
'("P" "Projects"
tags "+PROJECT-TODO=\"SOMEDAY\""))
(add-to-list 'org-agenda-custom-commands
'("i" "Inbox"
todo "INBOX"))
(add-to-list 'org-agenda-custom-commands
'("o" "Someday"
todo "SOMEDAY"))
(add-to-list 'org-agenda-custom-commands
'("c" "Simple agenda view"
(
(agenda ""
)
(todo ""
(
(org-agenda-overriding-header "\nUnscheduled TODO")
(org-agenda-skip-function '(org-agenda-skip-entry-if
'timestamp 'todo '("SOMEDAY" "ERRAND")))
(org-agenda-sorting-strategy
(quote ((agenda time-up priority-down tag-up))))
))
)
((org-agenda-overriding-columns-format
"%38ITEM(Details) %TAGS(Context) %7TODO(To Do) %5Effort(Time){:} %6CLOCKSUM_T(Total){:}")
(org-agenda-view-columns-initially t))
)
)
(setq org-todo-keyword-faces
'(("ERRAND" . (:foreground "light sea green" :weight bold))
("INBOX" . (:foreground "DarkGoldenrod" :weight bold))))
;;(set-face-foreground 'org-scheduled-previously "DarkGoldenrod")
(setq org-tags-exclude-from-inheritance '("PROJECT")
org-stuck-projects '("+PROJECT/-MAYBE-DONE-SOMEDAY"
("TODO" "ERRAND" "WAITING") () ()))
;; -------- Email --------
;;(use-package mu4e)
;;(add-to-list 'load-path "/usr/share/emacs/site-lisp/mu4e")
(setq sendmail-program (executable-find "msmtp"))
(autoload 'mu4e "mu4e" "\
If mu4e is not running yet, start it. Then, show the main
window, unless BACKGROUND (prefix-argument) is non-nil.
" t nil)
(setq mu4e-update-interval 600) ;; refresh every X seconds
(setq message-citation-line-format "On %d %b %Y at %R, %f wrote:\n")
(setq message-citation-line-function 'message-insert-formatted-citation-line)
(setq mu4e-attachment-dir "~/Downloads")
(setq mu4e-html2text-command 'mu4e-shr2text)
(setq mu4e-user-mail-address-list '("mail@knazarov.com" "k.nazarov@corp.mail.ru"))
;; exlude myself from the email replies
(setq mu4e-compose-dont-reply-to-self t)
;; set mu4e as a default mail agent
(setq mail-user-agent 'mu4e-user-agent)
(setq mu4e-maildir "/home/knazarov/Maildir")
(setq
mu4e-view-show-images t
mu4e-image-max-width 800
mu4e-view-prefer-html t
mu4e-change-filenames-when-moving t ;; prevent duplicate UIDs
mu4e-get-mail-command "mbsync -a -q"
mu4e-headers-include-related nil)
(setq mu4e-sent-folder "/personal/Sent"
mu4e-drafts-folder "/personal/Drafts"
mu4e-trash-folder "/personal/Trash"
mu4e-refile-folder "/personal/Archive"
user-full-name "Konstantin Nazarov"
user-mail-address "mail@knazarov.com"
smtpmail-default-smtp-server "smtp.fastmail.com"
smtpmail-local-domain "knazarov.com"
smtpmail-smtp-server "smtp.fastmail.com"
smtpmail-stream-type 'starttls
smtpmail-smtp-service 587
message-send-mail-function 'message-send-mail-with-sendmail
;;message-sendmail-extra-arguments '("--read-envelope-from")
)
(setq mu4e-compose-signature
"<#part type=text/html><html><body><p>Hello ! I am the html signature which can contains anything in html !</p></body></html><#/part>" )
(defvar my-mu4e-account-alist
`(("personal"
(mu4e-sent-folder "/personal/Sent")
(mu4e-drafts-folder "/personal/Drafts")
(mu4e-trash-folder "/personal/Trash")
(mu4e-refile-folder "/personal/Archive")
(user-mail-address "mail@knazarov.com")
(message-sendmail-envelope-from "mail@knazarov.com")
;;(mu4e-compose-signature-auto-include nil)
(mu4e-compose-signature ,(if (file-exists-p "~/.mail-sig.txt")
(with-temp-buffer
(insert-file-contents "~/.mail-sig.txt")
(buffer-string))
""))
(message-signature-file "~/.mail-sig.txt")
(message-cite-reply-position above)
(message-cite-style message-cite-style-outlook))
("work"
(mu4e-sent-folder "/work/Sent")
(mu4e-drafts-folder "/work/Drafts")
(mu4e-trash-folder "/work/Trash")
(mu4e-refile-folder "/work/Archive")
(user-mail-address "k.nazarov@corp.mail.ru")
(message-sendmail-envelope-from "k.nazarov@corp.mail.ru")
(mu4e-compose-signature-auto-include nil)
(message-signature-file "~/.mail-sig.txt")
(mu4e-compose-signature ,(if (file-exists-p "~/.mail-sig.txt")
(with-temp-buffer
(insert-file-contents "~/.mail-sig.txt")
(buffer-string))
""))
(message-cite-reply-position above)
(message-cite-style message-cite-style-outlook))
))
(defun my-mu4e-set-account ()
"Set the account for composing a message."
(let* ((account
(if mu4e-compose-parent-message
(let ((maildir (mu4e-message-field mu4e-compose-parent-message :maildir)))
(string-match "/\\(.*?\\)/" maildir)
(match-string 1 maildir))
(completing-read (format "Compose with account: (%s) "
(mapconcat #'(lambda (var) (car var))
my-mu4e-account-alist "/"))
(mapcar #'(lambda (var) (car var)) my-mu4e-account-alist)
nil t nil nil (caar my-mu4e-account-alist))))
(account-vars (cdr (assoc account my-mu4e-account-alist))))
(if account-vars
(mapc #'(lambda (var)
(set (car var) (cadr var)))
account-vars)
(error "No email account found"))))
(defun my-mu4e-refile-folder-function (msg)
(let ((mu4e-accounts my-mu4e-account-alist)
(current-message msg)
(account))
(setq account (catch 'found
(dolist (candidate mu4e-accounts)
(if (string-match (car candidate)
(mu4e-message-field current-message :maildir))
(throw 'found candidate)
))))
(if account
(cadr (assoc 'mu4e-refile-folder account))
(throw 'account_not_found (mu4e-message-field current-message :maildir))
)
)
)
(setq mu4e-refile-folder 'my-mu4e-refile-folder-function)
(add-hook 'mu4e-compose-pre-hook 'my-mu4e-set-account)
;; Be smart about inserting signature for either cite-reply-position used
(defun insert-signature ()
"Insert signature where you are replying"
;; Do not insert if already done - needed when switching modes back/forth
(unless (save-excursion (message-goto-signature))
(save-excursion
(if (eq message-cite-reply-position 'below)
(goto-char (point-max))
(message-goto-body))
(insert-file-contents message-signature-file)
(save-excursion (insert "\n-- \n")))))
(add-hook 'mu4e-compose-mode-hook 'insert-signature)
;;(add-to-list 'mu4e-bookmarks
;; '("maildir:/work/INBOX" "work inbox" ?w))
;;(add-to-list 'mu4e-bookmarks
;; '("maildir:/knazarov/INBOX" "personal inbox" ?p))
(setq mu4e-bookmarks
'(("maildir:/personal/INBOX OR maildir:/work/INBOX" "inbox" ?i)))
;; -------- Programming --------
;; Enable direnv-mode to automatically load Nix flake dependencies for projects
(use-package direnv
:config
(direnv-mode))
;; Rainbow delimeters highlight matching pairs of braces in different colors
(use-package rainbow-delimiters)
(add-hook 'prog-mode-hook #'rainbow-delimiters-mode)
;; Flycheck is an on-the-fly syntax checker for emacs with pluggable backengs.
(use-package flycheck)
;; Not usable for every single mode, so need to be selective
;;(add-hook 'prog-mode-hook #'flycheck-mode)
;; Scroll compilation buffer with the output
(setq compilation-scroll-output t)
;; Projectile auto-detects projects and allows to run project-wide commands
(use-package projectile)
(setq projectile-cache-file "~/.emacs.d/projectile.cache")
(projectile-mode +1)
(define-key projectile-mode-map (kbd "C-c p") 'projectile-command-map)
;; Corfu mode is a simple auto completion framework with
;; pluggable backends
;;(setq company-backends '(company-capf (company-dabbrev-code) company-dabbrev))
;;(add-hook 'prog-mode-hook #'company-mode)
;;(add-hook 'mu4e-compose-mode-hook #'company-mode)
(use-package corfu)
(add-hook 'after-init-hook 'global-corfu-mode)
(add-hook 'after-init-hook 'corfu-popupinfo-mode)
(setq corfu-auto t)
(setq tab-always-indent 'complete)
(setq corfu-quit-no-match 'separator)
(use-package cape)
(add-to-list 'completion-at-point-functions #'cape-dabbrev)
(add-to-list 'completion-at-point-functions #'cape-file)
;; Can't live without magit. It makes working with git sooo much easie
(use-package magit)
(global-set-key (kbd "C-x g") 'magit-status)
;;(setq magit-completing-read-function 'magit-ido-completing-read)
;; LSP
;; Maybe switch to EGLOT, which is shipped in Emacs 29
(add-hook 'lua-mode-hook #'lsp)
;; Tree-sitter
(setq major-mode-remap-alist
'((yaml-mode . yaml-ts-mode)
(bash-mode . bash-ts-mode)
(js2-mode . js-ts-mode)
(typescript-mode . typescript-ts-mode)
(json-mode . json-ts-mode)
(css-mode . css-ts-mode)
(python-mode . python-ts-mode)
(cpp-mode . cpp-ts-mode)
(c-mode . c-ts-mode)
))
(setq treesit-font-lock-level 4)
;; vterm
;; Vterm is a fully featured terminal emulator, that works inside
;; emacs buffers. It is miles ahead of "term" and "eshell" both in
;; speed and features.
(setq vterm-always-compile-module t)
(use-package vterm)
;;(add-to-list 'load-path (expand-file-name "~/dev/emacs-libvterm/"))
(defun vterm-less (content)
(let ((less-buffer (get-buffer-create (make-temp-name "vterm-less-"))))
(with-current-buffer less-buffer
(switch-to-buffer less-buffer)
(special-mode)
(insert (base64-decode-string content)))
)
)
(use-package with-editor)
(defun my-vterm-mode-hook ()
(add-to-list 'vterm-eval-cmds (list "less" #'vterm-less))
(with-editor-export-editor)
)
(add-hook 'vterm-mode-hook #'my-vterm-mode-hook)
(put 'upcase-region 'disabled nil)
(autoload 'vterm "vterm" "\
If vterm is not running yet, start it. Then, show the main
window, unless BACKGROUND (prefix-argument) is non-nil.
" t nil)
;; lua
(use-package lua-mode)
(setq lua-indent-level 4)
;; common lisp
(use-package sly)
(use-package sly-asdf)
(use-package aggressive-indent)
(add-to-list 'sly-contribs 'sly-asdf 'append)
(autoload 'sly "sly"