aboutsummaryrefslogtreecommitdiff
path: root/home-manager
diff options
context:
space:
mode:
Diffstat (limited to 'home-manager')
-rw-r--r--home-manager/.gitignore1
-rw-r--r--home-manager/.gitlab-ci.yml41
-rw-r--r--home-manager/.travis.yml13
-rw-r--r--home-manager/CONTRIBUTING.md164
-rw-r--r--home-manager/FAQ.md151
-rw-r--r--home-manager/LICENSE21
-rw-r--r--home-manager/README.md317
-rw-r--r--home-manager/default.nix10
-rw-r--r--home-manager/doc/default.nix67
-rw-r--r--home-manager/doc/installation.xml310
-rw-r--r--home-manager/doc/man-configuration.xml40
-rw-r--r--home-manager/doc/man-home-manager.xml513
-rw-r--r--home-manager/doc/man-pages.xml12
-rw-r--r--home-manager/doc/manual.xml38
-rw-r--r--home-manager/doc/release-notes/release-notes.xml15
-rw-r--r--home-manager/doc/release-notes/rl-1809.adoc4
-rw-r--r--home-manager/doc/release-notes/rl-1903.adoc59
-rw-r--r--home-manager/doc/release-notes/rl-1909.adoc31
-rw-r--r--home-manager/doc/release-notes/rl-2003.adoc83
-rwxr-xr-xhome-manager/format70
-rw-r--r--home-manager/home-manager/completion.bash356
-rw-r--r--home-manager/home-manager/default.nix40
-rw-r--r--home-manager/home-manager/home-manager579
-rw-r--r--home-manager/home-manager/home-manager.nix88
-rw-r--r--home-manager/home-manager/install.nix67
-rw-r--r--home-manager/modules/accounts/email.nix423
-rw-r--r--home-manager/modules/default.nix62
-rw-r--r--home-manager/modules/files.nix302
-rw-r--r--home-manager/modules/home-environment.nix470
-rwxr-xr-xhome-manager/modules/lib-bash/activation-init.sh83
-rw-r--r--home-manager/modules/lib-bash/color-echo.sh37
-rw-r--r--home-manager/modules/lib/dag.nix117
-rw-r--r--home-manager/modules/lib/default.nix24
-rw-r--r--home-manager/modules/lib/file-type.nix96
-rw-r--r--home-manager/modules/lib/shell.nix11
-rw-r--r--home-manager/modules/lib/stdlib-extended.nix7
-rw-r--r--home-manager/modules/lib/strings.nix22
-rw-r--r--home-manager/modules/lib/types-dag.nix84
-rw-r--r--home-manager/modules/lib/types.nix36
-rw-r--r--home-manager/modules/lib/zsh.nix30
-rw-r--r--home-manager/modules/manual.nix68
-rw-r--r--home-manager/modules/misc/dconf.nix88
-rw-r--r--home-manager/modules/misc/fontconfig.nix105
-rw-r--r--home-manager/modules/misc/gtk.nix188
-rw-r--r--home-manager/modules/misc/lib.nix14
-rw-r--r--home-manager/modules/misc/news.nix1323
-rw-r--r--home-manager/modules/misc/nixpkgs.nix152
-rw-r--r--home-manager/modules/misc/numlock.nix29
-rw-r--r--home-manager/modules/misc/pam.nix32
-rw-r--r--home-manager/modules/misc/qt.nix68
-rw-r--r--home-manager/modules/misc/submodule-support.nix32
-rw-r--r--home-manager/modules/misc/version.nix24
-rw-r--r--home-manager/modules/misc/xdg-mime-apps.nix88
-rw-r--r--home-manager/modules/misc/xdg-mime.nix47
-rw-r--r--home-manager/modules/misc/xdg-user-dirs.nix103
-rw-r--r--home-manager/modules/misc/xdg.nix104
-rw-r--r--home-manager/modules/modules.nix181
-rw-r--r--home-manager/modules/programs/afew.nix52
-rw-r--r--home-manager/modules/programs/alacritty.nix52
-rw-r--r--home-manager/modules/programs/alot-accounts.nix58
-rw-r--r--home-manager/modules/programs/alot.nix173
-rw-r--r--home-manager/modules/programs/astroid-accounts.nix32
-rw-r--r--home-manager/modules/programs/astroid-config-template.json113
-rw-r--r--home-manager/modules/programs/astroid.nix123
-rw-r--r--home-manager/modules/programs/autorandr.nix355
-rw-r--r--home-manager/modules/programs/bash.nix219
-rw-r--r--home-manager/modules/programs/bat.nix37
-rw-r--r--home-manager/modules/programs/beets.nix58
-rw-r--r--home-manager/modules/programs/broot.nix256
-rw-r--r--home-manager/modules/programs/browserpass.nix76
-rw-r--r--home-manager/modules/programs/chromium.nix92
-rw-r--r--home-manager/modules/programs/command-not-found/command-not-found.nix59
-rw-r--r--home-manager/modules/programs/command-not-found/command-not-found.pl44
-rw-r--r--home-manager/modules/programs/direnv.nix99
-rw-r--r--home-manager/modules/programs/eclipse.nix50
-rw-r--r--home-manager/modules/programs/emacs.nix77
-rw-r--r--home-manager/modules/programs/feh.nix70
-rw-r--r--home-manager/modules/programs/firefox.nix305
-rw-r--r--home-manager/modules/programs/fish.nix191
-rw-r--r--home-manager/modules/programs/fzf.nix134
-rw-r--r--home-manager/modules/programs/getmail-accounts.nix49
-rw-r--r--home-manager/modules/programs/getmail.nix57
-rw-r--r--home-manager/modules/programs/git.nix303
-rw-r--r--home-manager/modules/programs/gnome-terminal.nix216
-rw-r--r--home-manager/modules/programs/go.nix89
-rw-r--r--home-manager/modules/programs/gpg.nix61
-rw-r--r--home-manager/modules/programs/home-manager.nix37
-rw-r--r--home-manager/modules/programs/htop.nix370
-rw-r--r--home-manager/modules/programs/info.nix75
-rw-r--r--home-manager/modules/programs/irssi.nix211
-rw-r--r--home-manager/modules/programs/jq.nix76
-rw-r--r--home-manager/modules/programs/kakoune.nix615
-rw-r--r--home-manager/modules/programs/keychain.nix115
-rw-r--r--home-manager/modules/programs/lesspipe.nix19
-rw-r--r--home-manager/modules/programs/lsd.nix41
-rw-r--r--home-manager/modules/programs/man.nix22
-rw-r--r--home-manager/modules/programs/matplotlib.nix59
-rw-r--r--home-manager/modules/programs/mbsync-accounts.nix105
-rw-r--r--home-manager/modules/programs/mbsync.nix164
-rw-r--r--home-manager/modules/programs/mercurial.nix100
-rw-r--r--home-manager/modules/programs/mpv.nix143
-rw-r--r--home-manager/modules/programs/msmtp-accounts.nix48
-rw-r--r--home-manager/modules/programs/msmtp.nix71
-rw-r--r--home-manager/modules/programs/neomutt-accounts.nix34
-rw-r--r--home-manager/modules/programs/neomutt.nix302
-rw-r--r--home-manager/modules/programs/neovim.nix207
-rw-r--r--home-manager/modules/programs/newsboat.nix119
-rw-r--r--home-manager/modules/programs/noti.nix50
-rw-r--r--home-manager/modules/programs/notmuch-accounts.nix5
-rw-r--r--home-manager/modules/programs/notmuch.nix191
-rw-r--r--home-manager/modules/programs/obs-studio.nix47
-rw-r--r--home-manager/modules/programs/offlineimap-accounts.nix51
-rw-r--r--home-manager/modules/programs/offlineimap.nix173
-rw-r--r--home-manager/modules/programs/opam.nix50
-rw-r--r--home-manager/modules/programs/password-store.nix62
-rw-r--r--home-manager/modules/programs/pazi.nix55
-rw-r--r--home-manager/modules/programs/pidgin.nix34
-rw-r--r--home-manager/modules/programs/readline.nix77
-rw-r--r--home-manager/modules/programs/rofi.nix327
-rw-r--r--home-manager/modules/programs/rtorrent.nix34
-rw-r--r--home-manager/modules/programs/skim.nix124
-rw-r--r--home-manager/modules/programs/ssh.nix461
-rw-r--r--home-manager/modules/programs/starship.nix94
-rw-r--r--home-manager/modules/programs/taskwarrior.nix112
-rw-r--r--home-manager/modules/programs/termite.nix387
-rw-r--r--home-manager/modules/programs/texlive.nix46
-rw-r--r--home-manager/modules/programs/tmux.nix320
-rw-r--r--home-manager/modules/programs/urxvt.nix156
-rw-r--r--home-manager/modules/programs/vim.nix171
-rw-r--r--home-manager/modules/programs/vscode.nix101
-rw-r--r--home-manager/modules/programs/vscode/haskell.nix62
-rw-r--r--home-manager/modules/programs/z-lua.nix90
-rw-r--r--home-manager/modules/programs/zathura.nix58
-rw-r--r--home-manager/modules/programs/zsh.nix456
-rw-r--r--home-manager/modules/services/blueman-applet.nix36
-rw-r--r--home-manager/modules/services/cbatticon.nix118
-rw-r--r--home-manager/modules/services/compton.nix291
-rw-r--r--home-manager/modules/services/dunst.nix159
-rw-r--r--home-manager/modules/services/dwm-status.nix65
-rw-r--r--home-manager/modules/services/emacs.nix42
-rw-r--r--home-manager/modules/services/flameshot.nix39
-rw-r--r--home-manager/modules/services/getmail.nix55
-rw-r--r--home-manager/modules/services/gnome-keyring.nix46
-rw-r--r--home-manager/modules/services/gpg-agent.nix281
-rw-r--r--home-manager/modules/services/grobi.nix97
-rw-r--r--home-manager/modules/services/hound.nix74
-rw-r--r--home-manager/modules/services/imapnotify-accounts.nix33
-rw-r--r--home-manager/modules/services/imapnotify.nix90
-rw-r--r--home-manager/modules/services/kbfs.nix67
-rw-r--r--home-manager/modules/services/kdeconnect.nix72
-rw-r--r--home-manager/modules/services/keepassx.nix27
-rw-r--r--home-manager/modules/services/keybase.nix37
-rw-r--r--home-manager/modules/services/lorri.nix51
-rw-r--r--home-manager/modules/services/mbsync.nix104
-rw-r--r--home-manager/modules/services/mpd.nix150
-rw-r--r--home-manager/modules/services/mpdris2.nix101
-rw-r--r--home-manager/modules/services/muchsync.nix203
-rw-r--r--home-manager/modules/services/network-manager-applet.nix36
-rw-r--r--home-manager/modules/services/nextcloud-client.nix26
-rw-r--r--home-manager/modules/services/owncloud-client.nix26
-rw-r--r--home-manager/modules/services/parcellite.nix35
-rw-r--r--home-manager/modules/services/password-store-sync.nix71
-rw-r--r--home-manager/modules/services/pasystray.nix30
-rw-r--r--home-manager/modules/services/polybar.nix135
-rw-r--r--home-manager/modules/services/random-background.nix97
-rw-r--r--home-manager/modules/services/redshift.nix164
-rw-r--r--home-manager/modules/services/rsibreak.nix32
-rw-r--r--home-manager/modules/services/screen-locker.nix85
-rw-r--r--home-manager/modules/services/spotifyd.nix52
-rw-r--r--home-manager/modules/services/stalonetray.nix90
-rw-r--r--home-manager/modules/services/status-notifier-watcher.nix44
-rw-r--r--home-manager/modules/services/sxhkd.nix86
-rw-r--r--home-manager/modules/services/syncthing.nix68
-rw-r--r--home-manager/modules/services/taffybar.nix44
-rw-r--r--home-manager/modules/services/tahoe-lafs.nix19
-rw-r--r--home-manager/modules/services/taskwarrior-sync.nix50
-rw-r--r--home-manager/modules/services/udiskie.nix91
-rw-r--r--home-manager/modules/services/unclutter.nix61
-rw-r--r--home-manager/modules/services/unison.nix121
-rw-r--r--home-manager/modules/services/window-managers/awesome.nix52
-rw-r--r--home-manager/modules/services/window-managers/bspwm/default.nix74
-rw-r--r--home-manager/modules/services/window-managers/bspwm/options.nix214
-rw-r--r--home-manager/modules/services/window-managers/i3.nix856
-rw-r--r--home-manager/modules/services/window-managers/xmonad.nix101
-rw-r--r--home-manager/modules/services/xcape.nix76
-rw-r--r--home-manager/modules/services/xembed-sni-proxy.nix45
-rw-r--r--home-manager/modules/services/xscreensaver.nix52
-rw-r--r--home-manager/modules/services/xsuspender.nix192
-rw-r--r--home-manager/modules/systemd-activate.rb203
-rw-r--r--home-manager/modules/systemd-activate.sh114
-rw-r--r--home-manager/modules/systemd.nix268
-rw-r--r--home-manager/modules/xcursor.nix83
-rw-r--r--home-manager/modules/xresources.nix87
-rw-r--r--home-manager/modules/xsession.nix168
-rw-r--r--home-manager/nix-darwin/default.nix81
-rw-r--r--home-manager/nixos/default.nix110
-rw-r--r--home-manager/overlay.nix3
-rw-r--r--home-manager/tests/default.nix54
-rw-r--r--home-manager/tests/lib/types/dag-merge-result.txt3
-rw-r--r--home-manager/tests/lib/types/dag-merge.nix32
-rw-r--r--home-manager/tests/lib/types/default.nix4
-rw-r--r--home-manager/tests/lib/types/list-or-dag-merge-result.txt15
-rw-r--r--home-manager/tests/lib/types/list-or-dag-merge.nix34
-rw-r--r--home-manager/tests/modules/accounts/email-test-accounts.nix27
-rw-r--r--home-manager/tests/modules/files/.hidden1
-rw-r--r--home-manager/tests/modules/files/default.nix6
-rw-r--r--home-manager/tests/modules/files/executable.nix17
-rw-r--r--home-manager/tests/modules/files/hidden-source.nix19
-rw-r--r--home-manager/tests/modules/files/source with spaces!1
-rw-r--r--home-manager/tests/modules/files/source-with-spaces.nix20
-rw-r--r--home-manager/tests/modules/files/text-expected.txt2
-rw-r--r--home-manager/tests/modules/files/text.nix18
-rw-r--r--home-manager/tests/modules/home-environment/default.nix3
-rw-r--r--home-manager/tests/modules/home-environment/session-variables-expected.txt6
-rw-r--r--home-manager/tests/modules/home-environment/session-variables.nix19
-rw-r--r--home-manager/tests/modules/misc/fontconfig/default.nix5
-rw-r--r--home-manager/tests/modules/misc/fontconfig/multiple-font-packages.nix15
-rw-r--r--home-manager/tests/modules/misc/fontconfig/no-font-package.nix17
-rw-r--r--home-manager/tests/modules/misc/fontconfig/single-font-package.nix15
-rw-r--r--home-manager/tests/modules/misc/pam/default.nix1
-rw-r--r--home-manager/tests/modules/misc/pam/session-variables-expected.txt2
-rw-r--r--home-manager/tests/modules/misc/pam/session-variables.nix19
-rw-r--r--home-manager/tests/modules/misc/xdg/default.nix1
-rw-r--r--home-manager/tests/modules/misc/xdg/mime-apps-basics-expected.ini9
-rw-r--r--home-manager/tests/modules/misc/xdg/mime-apps-basics.nix28
-rw-r--r--home-manager/tests/modules/misc/xsession/basic-setxkbmap-expected.service12
-rw-r--r--home-manager/tests/modules/misc/xsession/basic-xprofile-expected.txt16
-rw-r--r--home-manager/tests/modules/misc/xsession/basic-xsession-expected.txt18
-rw-r--r--home-manager/tests/modules/misc/xsession/basic.nix42
-rw-r--r--home-manager/tests/modules/misc/xsession/default.nix4
-rw-r--r--home-manager/tests/modules/misc/xsession/keyboard-without-layout-expected.service12
-rw-r--r--home-manager/tests/modules/misc/xsession/keyboard-without-layout.nix36
-rw-r--r--home-manager/tests/modules/programs/alacritty/default.nix4
-rw-r--r--home-manager/tests/modules/programs/alacritty/empty-settings.nix17
-rw-r--r--home-manager/tests/modules/programs/alacritty/example-settings-expected.yml1
-rw-r--r--home-manager/tests/modules/programs/alacritty/example-settings.nix34
-rw-r--r--home-manager/tests/modules/programs/bash/default.nix4
-rw-r--r--home-manager/tests/modules/programs/bash/logout-expected.txt4
-rw-r--r--home-manager/tests/modules/programs/bash/logout.nix22
-rw-r--r--home-manager/tests/modules/programs/bash/session-variables-expected.txt8
-rw-r--r--home-manager/tests/modules/programs/bash/session-variables.nix25
-rw-r--r--home-manager/tests/modules/programs/browserpass/browserpass.nix29
-rw-r--r--home-manager/tests/modules/programs/browserpass/default.nix1
-rw-r--r--home-manager/tests/modules/programs/firefox/default.nix4
-rw-r--r--home-manager/tests/modules/programs/firefox/profile-settings-expected-user.js6
-rw-r--r--home-manager/tests/modules/programs/firefox/profile-settings.nix36
-rw-r--r--home-manager/tests/modules/programs/firefox/state-version-19_09.nix31
-rw-r--r--home-manager/tests/modules/programs/getmail/default.nix1
-rw-r--r--home-manager/tests/modules/programs/getmail/getmail-expected.conf16
-rw-r--r--home-manager/tests/modules/programs/getmail/getmail.nix31
-rw-r--r--home-manager/tests/modules/programs/git/default.nix5
-rw-r--r--home-manager/tests/modules/programs/git/git-expected.conf42
-rw-r--r--home-manager/tests/modules/programs/git/git-with-email-expected.conf15
-rw-r--r--home-manager/tests/modules/programs/git/git-with-email.nix36
-rw-r--r--home-manager/tests/modules/programs/git/git-with-str-extra-config-expected.conf5
-rw-r--r--home-manager/tests/modules/programs/git/git-with-str-extra-config.nix23
-rw-r--r--home-manager/tests/modules/programs/git/git.nix77
-rw-r--r--home-manager/tests/modules/programs/gpg/default.nix1
-rw-r--r--home-manager/tests/modules/programs/gpg/override-defaults-expected.conf19
-rw-r--r--home-manager/tests/modules/programs/gpg/override-defaults.nix22
-rw-r--r--home-manager/tests/modules/programs/mbsync/default.nix1
-rw-r--r--home-manager/tests/modules/programs/mbsync/mbsync-expected.conf55
-rw-r--r--home-manager/tests/modules/programs/mbsync/mbsync.nix31
-rw-r--r--home-manager/tests/modules/programs/neomutt/default.nix1
-rw-r--r--home-manager/tests/modules/programs/neomutt/hm-example.com-expected37
-rw-r--r--home-manager/tests/modules/programs/neomutt/neomutt-expected.conf27
-rw-r--r--home-manager/tests/modules/programs/neomutt/neomutt.nix47
-rw-r--r--home-manager/tests/modules/programs/newsboat/default.nix1
-rw-r--r--home-manager/tests/modules/programs/newsboat/newsboat-basics-urls.txt3
-rw-r--r--home-manager/tests/modules/programs/newsboat/newsboat-basics.nix33
-rw-r--r--home-manager/tests/modules/programs/readline/default.nix1
-rw-r--r--home-manager/tests/modules/programs/readline/using-all-options.nix31
-rw-r--r--home-manager/tests/modules/programs/readline/using-all-options.txt11
-rw-r--r--home-manager/tests/modules/programs/rofi/assert-on-both-theme-and-colors-expected.json1
-rw-r--r--home-manager/tests/modules/programs/rofi/assert-on-both-theme-and-colors.nix29
-rw-r--r--home-manager/tests/modules/programs/rofi/default.nix3
-rw-r--r--home-manager/tests/modules/programs/ssh/default-config-expected.conf15
-rw-r--r--home-manager/tests/modules/programs/ssh/default-config.nix18
-rw-r--r--home-manager/tests/modules/programs/ssh/default.nix17
-rw-r--r--home-manager/tests/modules/programs/ssh/forwards-dynamic-bind-path-with-port-asserts.nix29
-rw-r--r--home-manager/tests/modules/programs/ssh/forwards-dynamic-valid-bind-no-asserts-expected.conf19
-rw-r--r--home-manager/tests/modules/programs/ssh/forwards-dynamic-valid-bind-no-asserts.nix38
-rw-r--r--home-manager/tests/modules/programs/ssh/forwards-local-bind-path-with-port-asserts.nix33
-rw-r--r--home-manager/tests/modules/programs/ssh/forwards-local-host-path-with-port-asserts.nix33
-rw-r--r--home-manager/tests/modules/programs/ssh/forwards-paths-with-ports-error.json1
-rw-r--r--home-manager/tests/modules/programs/ssh/forwards-remote-bind-path-with-port-asserts.nix33
-rw-r--r--home-manager/tests/modules/programs/ssh/forwards-remote-host-path-with-port-asserts.nix33
-rw-r--r--home-manager/tests/modules/programs/ssh/match-blocks-attrs-expected.conf29
-rw-r--r--home-manager/tests/modules/programs/ssh/match-blocks-attrs.nix55
-rw-r--r--home-manager/tests/modules/programs/ssh/no-assertions.json1
-rw-r--r--home-manager/tests/modules/programs/texlive/default.nix1
-rw-r--r--home-manager/tests/modules/programs/texlive/texlive-minimal.nix13
-rw-r--r--home-manager/tests/modules/programs/tmux/default.nix7
-rw-r--r--home-manager/tests/modules/programs/tmux/disable-confirmation-prompt.conf31
-rw-r--r--home-manager/tests/modules/programs/tmux/disable-confirmation-prompt.nix28
-rw-r--r--home-manager/tests/modules/programs/tmux/emacs-with-plugins.conf54
-rw-r--r--home-manager/tests/modules/programs/tmux/emacs-with-plugins.nix49
-rw-r--r--home-manager/tests/modules/programs/tmux/hm-session-vars.sh5
-rw-r--r--home-manager/tests/modules/programs/tmux/not-enabled.nix13
-rw-r--r--home-manager/tests/modules/programs/tmux/secure-socket-enabled.nix17
-rw-r--r--home-manager/tests/modules/programs/tmux/vi-all-true.conf31
-rw-r--r--home-manager/tests/modules/programs/tmux/vi-all-true.nix29
-rw-r--r--home-manager/tests/modules/programs/zsh/default.nix7
-rw-r--r--home-manager/tests/modules/programs/zsh/history-path-new-custom.nix20
-rw-r--r--home-manager/tests/modules/programs/zsh/history-path-new-default.nix17
-rw-r--r--home-manager/tests/modules/programs/zsh/history-path-old-custom.nix20
-rw-r--r--home-manager/tests/modules/programs/zsh/history-path-old-default.nix17
-rw-r--r--home-manager/tests/modules/programs/zsh/session-variables.nix28
-rw-r--r--home-manager/tests/modules/services/sxhkd/configuration.nix30
-rw-r--r--home-manager/tests/modules/services/sxhkd/default.nix4
-rw-r--r--home-manager/tests/modules/services/sxhkd/service.nix20
-rw-r--r--home-manager/tests/modules/services/sxhkd/sxhkdrc13
-rw-r--r--home-manager/tests/modules/services/window-managers/i3/default.nix1
-rw-r--r--home-manager/tests/modules/services/window-managers/i3/i3-keybindings-expected.conf106
-rw-r--r--home-manager/tests/modules/services/window-managers/i3/i3-keybindings.nix35
-rw-r--r--home-manager/tests/modules/systemd/default.nix5
-rw-r--r--home-manager/tests/modules/systemd/services-expected.conf5
-rw-r--r--home-manager/tests/modules/systemd/services.nix23
-rw-r--r--home-manager/tests/modules/systemd/session-variables-expected.conf2
-rw-r--r--home-manager/tests/modules/systemd/session-variables.nix18
-rw-r--r--home-manager/tests/modules/systemd/timers-expected.conf8
-rw-r--r--home-manager/tests/modules/systemd/timers.nix25
-rw-r--r--home-manager/tests/modules/xresources/default.nix1
-rw-r--r--home-manager/tests/modules/xresources/xresources-expected.conf5
-rw-r--r--home-manager/tests/modules/xresources/xresources.nix22
325 files changed, 26675 insertions, 0 deletions
diff --git a/home-manager/.gitignore b/home-manager/.gitignore
new file mode 100644
index 00000000000..d6944e3ddc1
--- /dev/null
+++ b/home-manager/.gitignore
@@ -0,0 +1 @@
+/result*
diff --git a/home-manager/.gitlab-ci.yml b/home-manager/.gitlab-ci.yml
new file mode 100644
index 00000000000..f7c14abb69e
--- /dev/null
+++ b/home-manager/.gitlab-ci.yml
@@ -0,0 +1,41 @@
+image: nixos/nix:latest
+
+variables:
+ # Pinned 2020-01-01.
+ NIX_PATH: "nixpkgs=https://github.com/NixOS/nixpkgs/archive/b0bbacb52134a7e731e549f4c0a7a2a39ca6b481.tar.gz"
+
+stages:
+ - test
+ - deploy
+
+Run tests:
+ stage: test
+ script:
+ - nix-shell tests -A run.files-text
+ only:
+ - master
+
+pages:
+ stage: deploy
+ script:
+ - mkdir -p ~/.config/nixpkgs
+ - echo '{ manual.html.enable = true; }' > ~/.config/nixpkgs/home.nix
+ - nix-shell . -A install
+ - mkdir public
+ - cp -r ~/.nix-profile/share/doc/home-manager/* public/
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
+
+Deploy NUR:
+ stage: deploy
+ variables:
+ HM_BRANCH: $CI_COMMIT_REF_NAME
+ HM_COMMIT_SHA: $CI_COMMIT_SHA
+ trigger:
+ project: rycee/nur-expressions
+ branch: master
+ only:
+ - master
diff --git a/home-manager/.travis.yml b/home-manager/.travis.yml
new file mode 100644
index 00000000000..e0712e1e41f
--- /dev/null
+++ b/home-manager/.travis.yml
@@ -0,0 +1,13 @@
+language: nix
+
+os:
+ - linux
+ - osx
+
+before_script:
+ - mkdir -m 0755 -p /nix/var/nix/{profiles,gcroots}/per-user/$USER
+
+script:
+ - ./format -c
+ - nix-shell . -A install
+ - nix-shell tests -A run.all
diff --git a/home-manager/CONTRIBUTING.md b/home-manager/CONTRIBUTING.md
new file mode 100644
index 00000000000..8e64b3b45f9
--- /dev/null
+++ b/home-manager/CONTRIBUTING.md
@@ -0,0 +1,164 @@
+Contributing
+============
+
+Thanks for wanting to contribute to Home Manager! These are some
+guidelines to make the process as smooth as possible for both you and
+the Home Manager developers.
+
+If you are only looking to report a problem then it is sufficient to
+read through the following section on reporting issues. If you instead
+want to directly contribute improvements and additions then please
+have a look at everything here.
+
+Reporting an issue
+------------------
+
+If you notice a problem with Home Manager and want to report it then
+have a look among the already [open issues][], if you find one
+matching yours then feel free to comment on it to add any additional
+information you may have.
+
+If no matching issue exists then go to the [new issue][] page and
+write a description of your problem. Include as much information as
+you can, ideally also include relevant excerpts from your Home Manager
+configuration.
+
+Contributing code
+-----------------
+
+If you want to contribute code to improve Home Manager then please
+follow these guidelines.
+
+### Fork and create a pull request ###
+
+If you have not previously forked Home Manager then you need to do
+that first. Have a look at GitHub's "[Fork A Repo][]" for instructions
+on how to do this.
+
+Once you have a fork of Home Manager you should create a branch
+starting at the most recent `master`. Give your branch a reasonably
+descriptive name. Perform your changes on this branch and when you are
+happy with the result push the branch to GitHub and
+[create a pull request][].
+
+Assuming your clone is at `$HOME/devel/home-manager` then you can make
+the `home-manager` command use it by either
+
+1. overriding the default path by using the `-I` command line option:
+
+ home-manager -I home-manager=$HOME/devel/home-manager
+
+ or
+
+2. changing the default path by ensuring your configuration includes
+
+ programs.home-manager.enable = true;
+ programs.home-manager.path = "$HOME/devel/home-manager";
+
+ and running `home-manager switch` to activate the change.
+ Afterwards, `home-manager build` and `home-manager switch` will
+ use your cloned repository.
+
+The first option is good if you only temporarily want to use your
+clone.
+
+### Commits ###
+
+The commits in your pull request should be reasonably self-contained,
+that is, each commit should make sense in isolation. In particular,
+you will be asked to amend any commit that introduces syntax errors or
+similar problems even if they are fixed in a later commit.
+
+The commit messages should follow the format
+
+ {component}: {description}
+
+ {long description}
+
+where `{component}` refers to the code component (or module) your
+change affects, `{description}` is a brief description of your change,
+and `{long description}` is an optional clarifying description. Note,
+`{description}` should start with a lower case letter. As a rare
+exception, if there is no clear component, or your change affects many
+components, then the `{component}` part is optional.
+
+When adding a new module, say `foo.nix`, we use the fixed commit
+format `foo: add module`. You can, of course, still include a long
+description if you wish.
+
+In addition to the above commit message guidelines, try to follow the
+[seven rules][] as much as possible.
+
+### Style guidelines ###
+
+The code in Home Manager is formatted by the [nixfmt][] tool and the
+formatting is checked in the pull request tests. Run the `format` tool
+inside the project repository before submitting your pull request.
+
+Note, we prefer `lowerCamelCase` for variable and attribute names with
+the accepted exception of variables directly referencing packages in
+Nixpkgs which use a hyphenated style. For example, the Home Manager
+option `services.gpg-agent.enableSshSupport` references the
+`gpg-agent` package in Nixpkgs.
+
+### News ###
+
+Home Manager includes a system for presenting news to the user. When
+making a change you, therefore, have the option to also include an
+associated news entry. In general, a news entry should only be added
+for truly noteworthy news. For example, a bug fix or new option does
+generally not need a news entry.
+
+If you do have a change worthy of a news entry then please add one in
+[`news.nix`][] but you should follow some basic guidelines:
+
+- The entry timestamp should be in ISO-8601 format having "+00:00" as
+ time zone. For example, "2017-09-13T17:10:14+00:00". A suitable
+ timestamp can be produced by the command
+
+ date --iso-8601=second --universal
+
+- The entry condition should be as specific as possible. For example,
+ if you are changing or deprecating a specific option then you could
+ restrict the news to those users who actually use this option.
+
+- Wrap the news message so that it will fit in the typical terminal,
+ that is, at most 80 characters wide. Ideally a bit less.
+
+- Unlike commit messages, news will be read without any connection to
+ the Home Manager source code. It is therefore important to make the
+ message understandable in isolation and to those who do not have
+ knowledge of the Home Manager internals. To this end it should be
+ written in more descriptive, prose like way.
+
+- If you refer to an option then write its full attribute path. That
+ is, instead of writing
+
+ > The option 'foo' has been deprecated, please use 'bar' instead.
+
+ it should read
+
+ > The option 'services.myservice.foo' has been deprecated, please
+ > use 'services.myservice.bar' instead.
+
+- A new module, say `foo.nix`, should always include a news entry
+ that has a message along the lines of
+
+ > A new module is available: 'services.foo'.
+
+ If the module is platform specific, e.g., a service module using
+ systemd, then a condition like
+
+ ```
+ condition = hostPlatform.isLinux;
+ ```
+
+ should be added.
+
+[open issues]: https://github.com/rycee/home-manager/issues
+[new issue]: https://github.com/rycee/home-manager/issues/new
+[Fork A Repo]: https://help.github.com/articles/fork-a-repo/
+[create a pull request]: https://help.github.com/articles/creating-a-pull-request/
+[seven rules]: https://chris.beams.io/posts/git-commit/#seven-rules
+[`news.nix`]: https://github.com/rycee/home-manager/blob/master/modules/misc/news.nix
+[nixfmt]: https://github.com/serokell/nixfmt/
diff --git a/home-manager/FAQ.md b/home-manager/FAQ.md
new file mode 100644
index 00000000000..a83a90f6cab
--- /dev/null
+++ b/home-manager/FAQ.md
@@ -0,0 +1,151 @@
+Frequently Asked Questions (FAQ)
+================================
+
+Why is there a collision error when switching generation?
+---------------------------------------------------------
+
+Home Manager currently installs packages into the user environment,
+precisely as if the packages were installed through
+`nix-env --install`. This means that you will get a collision error if
+your Home Manager configuration attempts to install a package that you
+already have installed manually, that is, packages that shows up when
+you run `nix-env --query`.
+
+For example, imagine you have the `hello` package installed in your
+environment
+
+```console
+$ nix-env --query
+hello-2.10
+```
+
+and your Home Manager configuration contains
+
+ home.packages = [ pkgs.hello ];
+
+Then attempting to switch to this configuration will result in an
+error similar to
+
+```console
+$ home-manager switch
+these derivations will be built:
+ /nix/store/xg69wsnd1rp8xgs9qfsjal017nf0ldhm-home-manager-path.drv
+[…]
+Activating installPackages
+replacing old ‘home-manager-path’
+installing ‘home-manager-path’
+building path(s) ‘/nix/store/b5c0asjz9f06l52l9812w6k39ifr49jj-user-environment’
+Wide character in die at /nix/store/64jc9gd2rkbgdb4yjx3nrgc91bpjj5ky-buildenv.pl line 79.
+collision between ‘/nix/store/fmwa4axzghz11cnln5absh31nbhs9lq1-home-manager-path/bin/hello’ and ‘/nix/store/c2wyl8b9p4afivpcz8jplc9kis8rj36d-hello-2.10/bin/hello’; use ‘nix-env --set-flag priority NUMBER PKGNAME’ to change the priority of one of the conflicting packages
+builder for ‘/nix/store/b37x3s7pzxbasfqhaca5dqbf3pjjw0ip-user-environment.drv’ failed with exit code 2
+error: build of ‘/nix/store/b37x3s7pzxbasfqhaca5dqbf3pjjw0ip-user-environment.drv’ failed
+```
+
+The solution is typically to uninstall the package from the
+environment using `nix-env --uninstall` and reattempt the Home Manager
+generation switch.
+
+Why are the session variables not set?
+--------------------------------------
+
+Home Manager is only able to set session variables automatically if it
+manages your Bash or Z shell configuration. If you don't want to let
+Home Manager manage your shell then you will have to manually source
+the
+
+ ~/.nix-profile/etc/profile.d/hm-session-vars.sh
+
+file in an appropriate way. In Bash and Z shell this can be done by
+adding
+
+```sh
+. "$HOME/.nix-profile/etc/profile.d/hm-session-vars.sh"
+```
+
+to your `.profile` and `.zshrc` files, respectively. The
+`hm-session-vars.sh` file should work in most Bourne-like shells.
+
+How do set up a configuration for multiple users/machines?
+----------------------------------------------------------
+
+A typical way to prepare a repository of configurations for multiple
+logins and machines is to prepare one "top-level" file for each unique
+combination.
+
+For example, if you have two machines, called "kronos" and "rhea" on
+which you want to configure your user "jane" then you could create the
+files
+
+- `kronos-jane.nix`,
+- `rhea-jane.nix`, and
+- `common.nix`
+
+in your repository. On the kronos and rhea machines you can then make
+`~jane/.config/nixpkgs/home.nix` be a symbolic link to the
+corresponding file in your configuration repository.
+
+The `kronos-jane.nix` and `rhea-jane.nix` files follow the format
+
+```nix
+{ ... }:
+
+{
+ imports = [ ./common.nix ];
+
+ # Various options that are specific for this machine/user.
+}
+```
+
+while the `common.nix` file contains configuration shared across the
+two logins. Of course, instead of just a single `common.nix` file you
+can have multiple ones, even one per program or service.
+
+You can get some inspiration from the [Post your home-manager home.nix
+file!][1] Reddit thread.
+
+[1]: https://www.reddit.com/r/NixOS/comments/9bb9h9/post_your_homemanager_homenix_file/
+
+Why do I get an error message about `ca.desrt.dconf`?
+-----------------------------------------------------
+
+You are most likely trying to configure the GTK or Gnome Terminal but
+the DBus session is not aware of the dconf service. The full error you
+might get is
+
+ error: GDBus.Error:org.freedesktop.DBus.Error.ServiceUnknown: The name ca.desrt.dconf was not provided by any .service files
+
+The solution on NixOS is to add
+
+ services.dbus.packages = with pkgs; [ gnome3.dconf ];
+
+to your system configuration.
+
+How do I install packages from Nixpkgs unstable?
+------------------------------------------------
+
+If you are using a stable version of Nixpkgs but would like to install
+some particular packages from Nixpkgs unstable then you can import the
+unstable Nixpkgs and refer to its packages within your configuration.
+Something like
+
+```nix
+{ pkgs, config, ... }:
+
+let
+
+ pkgsUnstable = import <nixpkgs-unstable> {};
+
+in
+
+{
+ home.packages = [
+ pkgsUnstable.foo
+ ];
+
+ # …
+}
+```
+
+should work provided you have a Nix channel called `nixpkgs-unstable`.
+Note, the package will not be affected by any package overrides,
+overlays, etc.
diff --git a/home-manager/LICENSE b/home-manager/LICENSE
new file mode 100644
index 00000000000..a7566dbf194
--- /dev/null
+++ b/home-manager/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017-2019 Robert Helgesson and Home Manager contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/home-manager/README.md b/home-manager/README.md
new file mode 100644
index 00000000000..5171ff657f8
--- /dev/null
+++ b/home-manager/README.md
@@ -0,0 +1,317 @@
+Home Manager using Nix
+======================
+
+This project provides a basic system for managing a user environment
+using the [Nix][] package manager together with the Nix libraries
+found in [Nixpkgs][]. Before attempting to use Home Manager please
+read the warning below.
+
+For a more systematic overview of all the options Home Manager
+provides please see the [Home Manager manual][configuration options].
+
+Words of warning
+----------------
+
+This project is under development. I personally use it to manage
+several user configurations but it may fail catastrophically for you.
+So beware!
+
+In some cases Home Manager cannot detect whether it will overwrite a
+previous manual configuration. For example, the Gnome Terminal module
+will write to your dconf store and cannot tell whether a configuration
+that it is about to be overwrite was from a previous Home Manager
+generation or from manual configuration.
+
+Home Manager targets [NixOS][] unstable and NixOS version 19.09 (the
+current stable version), it may or may not work on other Linux
+distributions and NixOS versions.
+
+Also, the `home-manager` tool does not explicitly support rollbacks at
+the moment so if your home directory gets messed up you'll have to fix
+it yourself. See the [rollbacks](#rollbacks) section for instructions
+on how to manually perform a rollback.
+
+Now when your expectations have been built up and you are eager to try
+all this out you can go ahead and read the rest of this text.
+
+Contact
+-------
+
+You can chat with us on IRC in the channel [#home-manager][] on
+[freenode][]. The [channel logs][] are hosted courtesy of
+[samueldr][].
+
+Installation
+------------
+
+Currently the easiest way to install Home Manager is as follows:
+
+1. Make sure you have a working Nix installation. If you are not
+ using NixOS then you may here have to run
+
+ ```console
+ $ mkdir -m 0755 -p /nix/var/nix/{profiles,gcroots}/per-user/$USER
+ ```
+
+ since Home Manager uses these directories to manage your profile
+ generations. On NixOS these should already be available.
+
+ Also make sure that your user is able to build and install Nix
+ packages. For example, you should be able to successfully run a
+ command like `nix-instantiate '<nixpkgs>' -A hello` without having
+ to switch to the root user. For a multi-user install of Nix this
+ means that your user must be covered by the
+ [`allowed-users`][nixAllowedUsers] Nix option. On NixOS you can
+ control this option using the
+ [`nix.allowedUsers`][nixosAllowedUsers] system option.
+
+2. Add the appropriate Home Manager channel. Typically this is
+
+ ```console
+ $ nix-channel --add https://github.com/rycee/home-manager/archive/master.tar.gz home-manager
+ $ nix-channel --update
+ ```
+
+ if you are following Nixpkgs master or an unstable channel and
+
+ ```console
+ $ nix-channel --add https://github.com/rycee/home-manager/archive/release-19.09.tar.gz home-manager
+ $ nix-channel --update
+ ```
+
+ if you follow a Nixpkgs version 19.09 channel.
+
+ On NixOS you may need to log out and back in for the channel to
+ become available. On non-NixOS you may have to add
+
+ ```shell
+ export NIX_PATH=$HOME/.nix-defexpr/channels${NIX_PATH:+:}$NIX_PATH
+ ```
+
+ to your shell (see [nix#2033](https://github.com/NixOS/nix/issues/2033)).
+
+3. Install Home Manager and create the first Home Manager generation:
+
+ ```console
+ $ nix-shell '<home-manager>' -A install
+ ```
+
+ Once finished, Home Manager should be active and available in your
+ user environment.
+
+3. If you do not plan on having Home Manager manage your shell
+ configuration then you must source the
+
+ ```
+ $HOME/.nix-profile/etc/profile.d/hm-session-vars.sh
+ ```
+
+ file in your shell configuration. Unfortunately, in this specific
+ case we currently only support POSIX.2-like shells such as
+ [Bash][] or [Z shell][].
+
+ For example, if you use Bash then add
+
+ ```bash
+ . "$HOME/.nix-profile/etc/profile.d/hm-session-vars.sh"
+ ```
+
+ to your `~/.profile` file.
+
+If instead of using channels you want to run Home Manager from a Git
+checkout of the repository then you can use the
+`programs.home-manager.path` option to specify the absolute path to
+the repository.
+
+Usage
+-----
+
+Home Manager is typically managed through the `home-manager` tool.
+This tool can, for example, apply configurations to your home
+directory, list user packages installed by the tool, and list the
+configuration generations.
+
+As an example, let us expand the initial configuration file from the
+installation above to install the htop and fortune packages, install
+Emacs with a few extra packages enabled, install Firefox with the
+IcedTea plugin enabled, and enable the user gpg-agent service.
+
+To satisfy the above setup we should elaborate the
+`~/.config/nixpkgs/home.nix` file as follows:
+
+```nix
+{ pkgs, ... }:
+
+{
+ home.packages = [
+ pkgs.htop
+ pkgs.fortune
+ ];
+
+ programs.emacs = {
+ enable = true;
+ extraPackages = epkgs: [
+ epkgs.nix-mode
+ epkgs.magit
+ ];
+ };
+
+ programs.firefox = {
+ enable = true;
+ enableIcedTea = true;
+ };
+
+ services.gpg-agent = {
+ enable = true;
+ defaultCacheTtl = 1800;
+ enableSshSupport = true;
+ };
+
+ programs.home-manager = {
+ enable = true;
+ path = "…";
+ };
+}
+```
+
+To activate this configuration you can then run
+
+```console
+$ home-manager switch
+```
+
+or if you are not feeling so lucky,
+
+```console
+$ home-manager build
+```
+
+which will create a `result` link to a directory containing an
+activation script and the generated home directory files.
+
+Documentation of available configuration options, including
+descriptions and usage examples, is available in the [Home Manager
+manual][configuration options] or offline by running
+
+```console
+$ man home-configuration.nix
+```
+
+Rollbacks
+---------
+
+While the `home-manager` tool does not explicitly support rollbacks at
+the moment it is relatively easy to perform one manually. The steps to
+do so are
+
+1. Run `home-manager generations` to determine which generation you
+ wish to rollback to:
+
+ ```console
+ $ home-manager generations
+ 2018-01-04 11:56 : id 765 -> /nix/store/kahm1rxk77mnvd2l8pfvd4jkkffk5ijk-home-manager-generation
+ 2018-01-03 10:29 : id 764 -> /nix/store/2wsmsliqr5yynqkdyjzb1y57pr5q2lsj-home-manager-generation
+ 2018-01-01 12:21 : id 763 -> /nix/store/mv960kl9chn2lal5q8lnqdp1ygxngcd1-home-manager-generation
+ 2017-12-29 21:03 : id 762 -> /nix/store/6c0k1r03fxckql4vgqcn9ccb616ynb94-home-manager-generation
+ 2017-12-25 18:51 : id 761 -> /nix/store/czc5y6vi1rvnkfv83cs3rn84jarcgsgh-home-manager-generation
+ …
+ ```
+
+2. Copy the Nix store path of the generation you chose, e.g.,
+
+ /nix/store/mv960kl9chn2lal5q8lnqdp1ygxngcd1-home-manager-generation
+
+ for generation 763.
+
+3. Run the `activate` script inside the copied store path:
+
+ ```console
+ $ /nix/store/mv960kl9chn2lal5q8lnqdp1ygxngcd1-home-manager-generation/activate
+ Starting home manager activation
+ …
+ ```
+
+Keeping your ~ safe from harm
+-----------------------------
+
+To configure programs and services Home Manager must write various
+things to your home directory. To prevent overwriting any existing
+files when switching to a new generation, Home Manager will attempt to
+detect collisions between existing files and generated files. If any
+such collision is detected the activation will terminate before
+changing anything on your computer.
+
+For example, suppose you have a wonderful, painstakingly created
+`~/.config/git/config` and add
+
+```nix
+{
+ # …
+
+ programs.git = {
+ enable = true;
+ userName = "Jane Doe";
+ userEmail = "jane.doe@example.org";
+ };
+
+ # …
+}
+```
+
+to your configuration. Attempting to switch to the generation will
+then result in
+
+```console
+$ home-manager switch
+…
+Activating checkLinkTargets
+Existing file '/home/jdoe/.gitconfig' is in the way
+Please move the above files and try again
+```
+
+Graphical services
+------------------
+
+Home Manager includes a number of services intended to run in a
+graphical session, for example `xscreensaver` and `dunst`.
+Unfortunately, such services will not be started automatically unless
+you let Home Manager start your X session. That is, you have something
+like
+
+```nix
+{
+ # …
+
+ services.xserver.enable = true;
+
+ # …
+}
+```
+
+in your system configuration and
+
+```nix
+{
+ # …
+
+ xsession.enable = true;
+ xsession.windowManager.command = "…";
+
+ # …
+}
+```
+
+in your Home Manager configuration.
+
+[Bash]: https://www.gnu.org/software/bash/
+[Nix]: https://nixos.org/nix/
+[NixOS]: https://nixos.org/
+[Nixpkgs]: https://nixos.org/nixpkgs/
+[nixAllowedUsers]: https://nixos.org/nix/manual/#conf-allowed-users
+[nixosAllowedUsers]: https://nixos.org/nixos/manual/options.html#opt-nix.allowedUsers
+[Z shell]: http://zsh.sourceforge.net/
+[configuration options]: https://rycee.gitlab.io/home-manager/options.html
+[#home-manager]: https://webchat.freenode.net/?url=irc%3A%2F%2Firc.freenode.net%2Fhome-manager
+[freenode]: https://freenode.net/
+[channel logs]: https://logs.nix.samueldr.com/home-manager/
+[samueldr]: https://github.com/samueldr/
diff --git a/home-manager/default.nix b/home-manager/default.nix
new file mode 100644
index 00000000000..545b0114191
--- /dev/null
+++ b/home-manager/default.nix
@@ -0,0 +1,10 @@
+{ pkgs ? import <nixpkgs> { } }:
+
+rec {
+ home-manager = pkgs.callPackage ./home-manager { path = toString ./.; };
+
+ install =
+ pkgs.callPackage ./home-manager/install.nix { inherit home-manager; };
+
+ nixos = import ./nixos;
+}
diff --git a/home-manager/doc/default.nix b/home-manager/doc/default.nix
new file mode 100644
index 00000000000..638027b0a78
--- /dev/null
+++ b/home-manager/doc/default.nix
@@ -0,0 +1,67 @@
+{
+# Note, this should be "the standard library" + HM extensions.
+lib, pkgs }:
+
+let
+
+ nmdSrc = pkgs.fetchFromGitLab {
+ name = "nmd";
+ owner = "rycee";
+ repo = "nmd";
+ rev = "b437898c2b137c39d9c5f9a1cf62ec630f14d9fc";
+ sha256 = "18j1nh53cfpjpdiwn99x9kqpvr0s7hwngyc0a93xf4sg88ww93lq";
+ };
+
+ nmd = import nmdSrc { inherit lib pkgs; };
+
+ # Make sure the used package is scrubbed to avoid actually
+ # instantiating derivations.
+ scrubbedPkgsModule = {
+ imports = [{
+ _module.args = {
+ pkgs = lib.mkForce (nmd.scrubDerivations "pkgs" pkgs);
+ pkgs_i686 = lib.mkForce { };
+ };
+ }];
+ };
+
+ hmModulesDocs = nmd.buildModulesDocs {
+ modules = import ../modules/modules.nix {
+ inherit lib pkgs;
+ check = false;
+ } ++ [ scrubbedPkgsModule ];
+ moduleRootPaths = [ ./.. ];
+ mkModuleUrl = path:
+ "https://github.com/rycee/home-manager/blob/master/${path}#blob-path";
+ channelName = "home-manager";
+ docBook.id = "home-manager-options";
+ };
+
+ docs = nmd.buildDocBookDocs {
+ pathName = "home-manager";
+ modulesDocs = [ hmModulesDocs ];
+ documentsDirectory = ./.;
+ chunkToc = ''
+ <toc>
+ <d:tocentry xmlns:d="http://docbook.org/ns/docbook" linkend="book-home-manager-manual"><?dbhtml filename="index.html"?>
+ <d:tocentry linkend="ch-options"><?dbhtml filename="options.html"?></d:tocentry>
+ <d:tocentry linkend="ch-tools"><?dbhtml filename="tools.html"?></d:tocentry>
+ <d:tocentry linkend="ch-release-notes"><?dbhtml filename="release-notes.html"?></d:tocentry>
+ </d:tocentry>
+ </toc>
+ '';
+ };
+
+in {
+ inherit nmdSrc;
+
+ options = {
+ json = hmModulesDocs.json.override {
+ path = "share/doc/home-manager/options.json";
+ };
+ };
+
+ manPages = docs.manPages;
+
+ manual = { inherit (docs) html htmlOpenTool; };
+}
diff --git a/home-manager/doc/installation.xml b/home-manager/doc/installation.xml
new file mode 100644
index 00000000000..52119886800
--- /dev/null
+++ b/home-manager/doc/installation.xml
@@ -0,0 +1,310 @@
+<chapter xmlns="http://docbook.org/ns/docbook"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:xi="http://www.w3.org/2001/XInclude"
+ version="5.0"
+ xml:id="ch-installation">
+ <title>Installing Home Manager</title>
+ <para>
+ Home Manager can be used in three primary ways:
+ <orderedlist>
+ <listitem>
+ <para>
+ Using the standalone <command>home-manager</command> tool. For platforms
+ other than NixOS and Darwin, this is the only available choice. It is also
+ recommended for people on NixOS or Darwin that want to manage their home
+ directory independent of the system as a whole. See
+ <xref linkend="sec-install-standalone"/> for instructions on how to
+ perform this installation.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ As a module within a NixOS system configuration. This allows the user
+ profiles to be built together with the system when running
+ <command>nixos-rebuild</command>. See
+ <xref linkend="sec-install-nixos-module"/> for a description of this
+ setup.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ As a module within a
+ <link xlink:href="https://github.com/LnL7/nix-darwin/">nix-darwin</link>
+ system configuration. This allows the user profiles to be built together
+ with the system when running <command>darwin-rebuild</command>. See
+ <xref linkend="sec-install-nix-darwin-module"/> for a description of this
+ setup.
+ </para>
+ </listitem>
+ </orderedlist>
+ </para>
+ <section xml:id="sec-install-standalone">
+ <title>Standalone installation</title>
+
+ <orderedlist>
+ <listitem>
+ <para>
+ Make sure you have a working Nix installation. If you are not using NixOS
+ then it may be necessary to run
+ </para>
+<screen>
+<prompt>$</prompt> <userinput>mkdir -m 0755 -p /nix/var/nix/{profiles,gcroots}/per-user/$USER</userinput>
+</screen>
+ <para>
+ since Home Manager uses these directories to manage your profile
+ generations. On NixOS these should already be available.
+ </para>
+ <para>
+ Also make sure that your user is able to build and install Nix packages.
+ For example, you should be able to successfully run a command like
+ <literal>nix-instantiate '&lt;nixpkgs&gt;' -A hello</literal> without
+ having to switch to the root user. For a multi-user install of Nix this
+ means that your user must be covered by the
+ <link xlink:href="https://nixos.org/nix/manual/#conf-allowed-users"><literal>allowed-users</literal></link>
+ Nix option. On NixOS you can control this option using the
+ <link xlink:href="https://nixos.org/nixos/manual/options.html#opt-nix.allowedUsers"><literal>nix.allowedUsers</literal></link>
+ system option.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ Add the Home Manager channel that you wish to follow. This is done by
+ running
+ </para>
+<screen>
+<prompt>$</prompt> <userinput>nix-channel --add https://github.com/rycee/home-manager/archive/master.tar.gz home-manager</userinput>
+<prompt>$</prompt> <userinput>nix-channel --update</userinput>
+</screen>
+ <para>
+ if you are following Nixpkgs master or an unstable channel and
+ </para>
+<screen>
+<prompt>$</prompt> <userinput>nix-channel --add https://github.com/rycee/home-manager/archive/release-19.09.tar.gz home-manager</userinput>
+<prompt>$</prompt> <userinput>nix-channel --update</userinput>
+</screen>
+ <para>
+ if you follow a Nixpkgs version 19.09 channel.
+ </para>
+ <para>
+ On NixOS you may need to log out and back in for the channel to become
+ available. On non-NixOS you may have to add
+<programlisting language="bash">
+export NIX_PATH=$HOME/.nix-defexpr/channels${NIX_PATH:+:}$NIX_PATH
+</programlisting>
+ to your shell (see
+ <link xlink:href="https://github.com/NixOS/nix/issues/2033">nix#2033</link>).
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ Run the Home Manager installation command and create the first Home
+ Manager generation:
+ </para>
+<screen>
+<prompt>$</prompt> <userinput>nix-shell '&lt;home-manager&gt;' -A install</userinput>
+</screen>
+ <para>
+ Once finished, Home Manager should be active and available in your user
+ environment.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ If you do not plan on having Home Manager manage your shell configuration
+ then you must source the
+ </para>
+<programlisting language="bash">
+$HOME/.nix-profile/etc/profile.d/hm-session-vars.sh
+</programlisting>
+ <para>
+ file in your shell configuration. Unfortunately, we currently only support
+ POSIX.2-like shells such as
+ <link xlink:href="https://www.gnu.org/software/bash/">Bash</link> or
+ <link xlink:href="http://zsh.sourceforge.net/">Z shell</link>.
+ </para>
+ <para>
+ For example, if you use Bash then add
+ </para>
+<programlisting language="bash">
+. &quot;$HOME/.nix-profile/etc/profile.d/hm-session-vars.sh&quot;
+</programlisting>
+ <para>
+ to your <literal>~/.profile</literal> file.
+ </para>
+ </listitem>
+ </orderedlist>
+
+ <para>
+ If instead of using channels you want to run Home Manager from a Git
+ checkout of the repository then you can use the
+ <literal>programs.home-manager.path</literal> option to specify the absolute
+ path to the repository.
+ </para>
+ </section>
+ <section xml:id="sec-install-nixos-module">
+ <title>NixOS module</title>
+
+ <para>
+ Home Manager provides a NixOS module that allows you to prepare user
+ environments directly from the system configuration file, which often is
+ more convenient than using the <command>home-manager</command> tool. It also
+ opens up additional possibilities, for example, to automatically configure
+ user environments in NixOS declarative containers or on systems deployed
+ through NixOps.
+ </para>
+
+ <para>
+ To make the NixOS module available for use you must <option>import</option>
+ it into your system configuration. This is most conveniently done by adding
+ a Home Manager channel, for example
+ </para>
+
+<screen>
+<prompt>#</prompt> <userinput>nix-channel --add https://github.com/rycee/home-manager/archive/master.tar.gz home-manager</userinput>
+<prompt>#</prompt> <userinput>nix-channel --update</userinput>
+</screen>
+
+ <para>
+ if you are following Nixpkgs master or an unstable channel and
+ </para>
+
+<screen>
+<prompt>#</prompt> <userinput>nix-channel --add https://github.com/rycee/home-manager/archive/release-19.09.tar.gz home-manager</userinput>
+<prompt>#</prompt> <userinput>nix-channel --update</userinput>
+</screen>
+
+ <para>
+ if you follow a Nixpkgs version 19.09 channel.
+ </para>
+
+ <para>
+ It is then possible to add
+ </para>
+
+<programlisting language="nix">
+imports = [ &lt;home-manager/nixos&gt; ];
+</programlisting>
+
+ <para>
+ to your system <filename>configuration.nix</filename> file, which will
+ introduce a new NixOS option called <option>home-manager.users</option>
+ whose type is an attribute set that maps user names to Home Manager
+ configurations.
+ </para>
+
+ <para>
+ For example, a NixOS configuration may include the lines
+ </para>
+
+<programlisting language="nix">
+users.users.eve.isNormalUser = true;
+home-manager.users.eve = { pkgs, ... }: {
+ home.packages = [ pkgs.atool pkgs.httpie ];
+ programs.bash.enable = true;
+};
+</programlisting>
+
+ <para>
+ and after a <command>nixos-rebuild switch</command> the user eve's
+ environment should include a basic Bash configuration and the packages atool
+ and httpie.
+ </para>
+
+ <note>
+ <para>
+ By default packages will be installed to
+ <filename>$HOME/.nix-profile</filename> but they can be installed to
+ <filename>/etc/profiles</filename> if
+ </para>
+<programlisting language="nix">
+home-manager.useUserPackages = true;
+</programlisting>
+ <para>
+ is added to the system configuration. This is necessary if, for example,
+ you wish to use <command>nixos-rebuild build-vm</command>. This option may
+ become the default value in the future.
+ </para>
+ </note>
+ </section>
+ <section xml:id="sec-install-nix-darwin-module">
+ <title>nix-darwin module</title>
+
+ <para>
+ Home Manager provides a module that allows you to prepare user
+ environments directly from the nix-darwin configuration file, which often is
+ more convenient than using the <command>home-manager</command> tool.
+ </para>
+
+ <para>
+ To make the NixOS module available for use you must <option>import</option>
+ it into your system configuration. This is most conveniently done by adding
+ a Home Manager channel, for example
+ </para>
+
+<screen>
+<prompt>#</prompt> <userinput>nix-channel --add https://github.com/rycee/home-manager/archive/master.tar.gz home-manager</userinput>
+<prompt>#</prompt> <userinput>nix-channel --update</userinput>
+</screen>
+
+ <para>
+ if you are following Nixpkgs master or an unstable channel and
+ </para>
+
+<screen>
+<prompt>#</prompt> <userinput>nix-channel --add https://github.com/rycee/home-manager/archive/release-19.09.tar.gz home-manager</userinput>
+<prompt>#</prompt> <userinput>nix-channel --update</userinput>
+</screen>
+
+ <para>
+ if you follow a Nixpkgs version 19.09 channel.
+ </para>
+
+ <para>
+ It is then possible to add
+ </para>
+
+<programlisting language="nix">
+imports = [ &lt;home-manager/nix-darwin&gt; ];
+</programlisting>
+
+ <para>
+ to your nix-darwin <filename>configuration.nix</filename> file, which will
+ introduce a new NixOS option called <option>home-manager</option> whose type
+ is an attribute set that maps user names to Home Manager configurations.
+ </para>
+
+ <para>
+ For example, a nix-darwin configuration may include the lines
+ </para>
+
+<programlisting language="nix">
+home-manager.users.eve = { pkgs, ... }: {
+ home.packages = [ pkgs.atool pkgs.httpie ];
+ programs.bash.enable = true;
+};
+</programlisting>
+
+ <para>
+ and after a <command>darwin-rebuild --switch</command> the user eve's
+ environment should include a basic Bash configuration and the packages atool
+ and httpie.
+ </para>
+
+ <note>
+ <para>
+ By default user packages will not be ignored in favor of
+ <option>environment.systemPackages</option>, but they will be intalled to
+ <option>/etc/profiles/per-user/$USERNAME</option> if
+ </para>
+
+<programlisting language="nix">
+home-manager.useUserPackages = true;
+</programlisting>
+
+ <para>
+ is added to the nix-darwin configuration. This option may become the default
+ value in the future.
+ </para>
+ </note>
+ </section>
+</chapter>
diff --git a/home-manager/doc/man-configuration.xml b/home-manager/doc/man-configuration.xml
new file mode 100644
index 00000000000..42962a75f3c
--- /dev/null
+++ b/home-manager/doc/man-configuration.xml
@@ -0,0 +1,40 @@
+<refentry xmlns="http://docbook.org/ns/docbook"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:xi="http://www.w3.org/2001/XInclude">
+ <refmeta>
+ <refentrytitle><filename>home-configuration.nix</filename></refentrytitle>
+ <manvolnum>5</manvolnum>
+ <refmiscinfo class="source">Home Manager</refmiscinfo>
+<!-- <refmiscinfo class="version"><xi:include href="version.txt" parse="text"/></refmiscinfo> -->
+ </refmeta>
+ <refnamediv>
+ <refname><filename>home-configuration.nix</filename></refname>
+ <refpurpose>Home Manager configuration specification</refpurpose>
+ </refnamediv>
+ <refsection>
+ <title>Description</title>
+ <para>
+ The file <filename>~/.config/nixpkgs/home.nix</filename> contains the
+ declarative specification of your Home Manager configuration. The command
+ <command>home-manager</command> takes this file and realises the user
+ environment configuration specified therein.
+ </para>
+ </refsection>
+ <refsection>
+ <title>Options</title>
+ <para>
+ You can use the following options in
+ <filename>home-configuration.nix</filename>:
+ </para>
+ <xi:include href="./nmd-result/home-manager-options.xml" />
+ </refsection>
+ <refsection>
+ <title>See also</title>
+ <para>
+ <citerefentry>
+ <refentrytitle>home-manager</refentrytitle>
+ <manvolnum>1</manvolnum>
+ </citerefentry>
+ </para>
+ </refsection>
+</refentry>
diff --git a/home-manager/doc/man-home-manager.xml b/home-manager/doc/man-home-manager.xml
new file mode 100644
index 00000000000..117bc494687
--- /dev/null
+++ b/home-manager/doc/man-home-manager.xml
@@ -0,0 +1,513 @@
+<refentry xmlns="http://docbook.org/ns/docbook"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:xi="http://www.w3.org/2001/XInclude">
+ <refmeta>
+ <refentrytitle><command>home-manager</command>
+ </refentrytitle><manvolnum>1</manvolnum>
+ <refmiscinfo class="source">Home Manager</refmiscinfo>
+ </refmeta>
+ <refnamediv>
+ <refname><command>home-manager</command>
+ </refname><refpurpose>reconfigure a user environment</refpurpose>
+ </refnamediv>
+ <refsynopsisdiv>
+ <cmdsynopsis>
+ <command>home-manager</command> <group choice="req">
+ <arg choice="plain">
+ build
+ </arg>
+
+ <arg choice="plain">
+ edit
+ </arg>
+
+ <arg choice="plain">
+ expire-generations <replaceable>timestamp</replaceable>
+ </arg>
+
+ <arg choice="plain">
+ generations
+ </arg>
+
+ <arg choice="plain">
+ help
+ </arg>
+
+ <arg choice="plain">
+ news
+ </arg>
+
+ <arg choice="plain">
+ packages
+ </arg>
+
+ <arg choice="plain">
+ remove-generations <replaceable>ID …</replaceable>
+ </arg>
+
+ <arg choice="plain">
+ switch
+ </arg>
+
+ <arg choice="plain">
+ uninstall
+ </arg>
+ </group>
+ <sbr />
+ <arg>
+ -A <replaceable>attrPath</replaceable>
+ </arg>
+
+ <arg>
+ -I <replaceable>path</replaceable>
+ </arg>
+
+ <arg>
+ -b <replaceable>ext</replaceable>
+ </arg>
+
+ <arg>
+ <group choice="req">
+ <arg choice="plain">
+ -f
+ </arg>
+
+ <arg choice="plain">
+ --file
+ </arg>
+ </group> <replaceable>path</replaceable>
+ </arg>
+
+ <arg>
+ <group choice="req">
+ <arg choice="plain">
+ -h
+ </arg>
+
+ <arg choice="plain">
+ --help
+ </arg>
+ </group>
+ </arg>
+
+ <arg>
+ <group choice="req">
+ <arg choice="plain">
+ -n
+ </arg>
+
+ <arg choice="plain">
+ --dry-run
+ </arg>
+ </group>
+ </arg>
+
+ <arg>
+ --option <replaceable>name</replaceable> <replaceable>value</replaceable>
+ </arg>
+
+ <arg>
+ --cores <replaceable>number</replaceable>
+ </arg>
+
+ <arg>
+ --max-jobs <replaceable>number</replaceable>
+ </arg>
+
+ <arg>
+ --keep-failed
+ </arg>
+
+ <arg>
+ --keep-going
+ </arg>
+
+ <arg>
+ --show-trace
+ </arg>
+
+ <arg>
+ --(no-)substitute
+ </arg>
+
+ <arg>
+ <group choice="req">
+ <arg choice="plain">
+ -v
+ </arg>
+
+ <arg choice="plain">
+ --verbose
+ </arg>
+ </group>
+ </arg>
+ </cmdsynopsis>
+ </refsynopsisdiv>
+ <refsection>
+ <title>Description</title>
+ <para>
+ This command updates the user environment so that it corresponds to the
+ configuration specified in <filename>~/.config/nixpkgs/home.nix</filename>.
+ </para>
+ <para>
+ All operations using this tool expects a sub-command that indicates the
+ operation to perform. It must be one of
+ <variablelist>
+ <varlistentry>
+ <term>
+ <option>build</option>
+ </term>
+ <listitem>
+ <para>
+ Build configuration into a <filename>result</filename> directory.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>
+ <option>edit</option>
+ </term>
+ <listitem>
+ <para>
+ Open the home configuration using the editor indicated by
+ <envar>EDITOR</envar>.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>
+ <option>expire-generations <replaceable>timestamp</replaceable></option>
+ </term>
+ <listitem>
+ <para>
+ Remove generations older than <replaceable>timestamp</replaceable> where
+ <replaceable>timestamp</replaceable> is interpreted as in the
+ <option>-d</option> argument of the <citerefentry>
+ <refentrytitle>date</refentrytitle>
+ <manvolnum>1</manvolnum> </citerefentry> tool. For example <literal>-30
+ days</literal> or <literal>2018-01-01</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>
+ <option>generations</option>
+ </term>
+ <listitem>
+ <para>
+ List all home environment generations.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>
+ <option>help</option>
+ </term>
+ <listitem>
+ <para>
+ Print tool help.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>
+ <option>news</option>
+ </term>
+ <listitem>
+ <para>
+ Show news entries in a pager.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>
+ <option>packages</option>
+ </term>
+ <listitem>
+ <para>
+ List all packages installed in <varname>home-manager-path</varname>.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>
+ <option>remove-generations <replaceable>ID …</replaceable></option>
+ </term>
+ <listitem>
+ <para>
+ Remove indicated generations. Use the <option>generations</option>
+ sub-command to find suitable generation numbers.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>
+ <option>switch</option>
+ </term>
+ <listitem>
+ <para>
+ Build and activate the configuration.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>
+ <option>uninstall</option>
+ </term>
+ <listitem>
+ <para>
+ Remove Home Manager from the user environment. This will
+ <itemizedlist>
+ <listitem>
+ <para>
+ remove all managed files from the home directory,
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ remove packages installed through Home Manager from the user profile,
+ and
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ optionally remove all Home Manager generations and make them
+ available for immediate garbage collection.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </para>
+ </refsection>
+ <refsection>
+ <title>Options</title>
+ <para>
+ The tool accepts the options
+ </para>
+ <variablelist>
+ <varlistentry>
+ <term>
+ <option>-A <replaceable>attrPath</replaceable></option>
+ </term>
+ <listitem>
+ <para>
+ Optional attribute that selects a configuration expression in the
+ configuration file. That is, if <filename>home.nix</filename> contains
+<programlisting language="nix">
+{
+ joe-at-work = {pkgs, ...}: { home.packages = [ pkgs.fortune ]; };
+ joe-at-home = {pkgs, ...}: { home.packages = [ pkgs.cowsay ]; };
+}
+</programlisting>
+ then the command <command>home-manager switch -A joe-at-work</command>
+ will activate the profile containing the fortune program.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>
+ <option>-I <replaceable>path</replaceable></option>
+ </term>
+ <listitem>
+ <para>
+ Add a path to the Nix expression search path. For example, to build a
+ Home Manager profile using a specific Nixpkgs run <command>home-manager
+ -I nixpkgs=/absolute/path/to/nixpkgs build</command>. By default
+ <literal>&lt;nixpkgs&gt;</literal> is used.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>
+ <option>-b <replaceable>extension</replaceable></option>
+ </term>
+ <listitem>
+ <para>
+ Enable automatic resolution of collisions between unmanaged and managed
+ files. The name of the original file will be suffixed by the given
+ extension. For example,
+<screen>
+<prompt>$</prompt> <userinput>home-manager -b bck switch</userinput>
+</screen>
+ will cause a colliding file <filename>~/.config/foo.conf</filename> to be
+ moved to <filename>~/.config/foo.conf.bck</filename>.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>
+ <option>-f <replaceable>path</replaceable></option>
+ </term>
+ <term>
+ <option>--file <replaceable>path</replaceable></option>
+ </term>
+ <listitem>
+ <para>
+ Indicates the path to the Home Manager configuration file. If not given,
+ <filename>~/.config/nixpkgs/home.nix</filename> is used.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>
+ <option>-h</option>
+ </term>
+ <term>
+ <option>--help</option>
+ </term>
+ <listitem>
+ <para>
+ Prints usage information for the <command>home-manager</command> tool.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>
+ <option>-n</option>
+ </term>
+ <term>
+ <option>--dry-run</option>
+ </term>
+ <listitem>
+ <para>
+ Perform a dry-run of the given operation, only prints what actions would
+ be taken.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>
+ <option>--option <replaceable>name</replaceable> <replaceable>value</replaceable></option>
+ </term>
+ <listitem>
+ <para>
+ Passed on to <citerefentry>
+ <refentrytitle>nix-build</refentrytitle>
+ <manvolnum>1</manvolnum> </citerefentry>.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>
+ <option>--cores <replaceable>number</replaceable></option>
+ </term>
+ <listitem>
+ <para>
+ Passed on to <citerefentry>
+ <refentrytitle>nix-build</refentrytitle>
+ <manvolnum>1</manvolnum> </citerefentry>.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>
+ <option>--max-jobs <replaceable>number</replaceable></option>
+ </term>
+ <listitem>
+ <para>
+ Passed on to <citerefentry>
+ <refentrytitle>nix-build</refentrytitle>
+ <manvolnum>1</manvolnum> </citerefentry>.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>
+ <option>--keep-failed</option>
+ </term>
+ <listitem>
+ <para>
+ Passed on to <citerefentry>
+ <refentrytitle>nix-build</refentrytitle>
+ <manvolnum>1</manvolnum> </citerefentry>.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>
+ <option>--keep-going</option>
+ </term>
+ <listitem>
+ <para>
+ Passed on to <citerefentry>
+ <refentrytitle>nix-build</refentrytitle>
+ <manvolnum>1</manvolnum> </citerefentry>.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>
+ <option>--show-trace</option>
+ </term>
+ <listitem>
+ <para>
+ Passed on to <citerefentry>
+ <refentrytitle>nix-build</refentrytitle>
+ <manvolnum>1</manvolnum> </citerefentry>.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>
+ <option>--(no-)substitute</option>
+ </term>
+ <listitem>
+ <para>
+ Passed on to <citerefentry>
+ <refentrytitle>nix-build</refentrytitle>
+ <manvolnum>1</manvolnum> </citerefentry>.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>
+ <option>-v</option>
+ </term>
+ <term>
+ <option>--verbose</option>
+ </term>
+ <listitem>
+ <para>
+ Activates verbose output.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </refsection>
+ <refsection>
+ <title>Files</title>
+ <variablelist>
+ <varlistentry>
+ <term>
+ <filename>~/.local/share/home-manager/news-read-ids</filename>
+ </term>
+ <listitem>
+ <para>
+ Identifiers of news items that have been shown. Can be deleted to reset
+ the read news indicator.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </refsection>
+ <refsection>
+ <title>Bugs</title>
+ <para>
+ Please report any bugs on the
+ <link
+ xlink:href="https://github.com/rycee/home-manager/issues">project
+ issue tracker</link>.
+ </para>
+ </refsection>
+ <refsection>
+ <title>See also</title>
+ <para>
+ <citerefentry>
+ <refentrytitle>home-configuration.nix</refentrytitle>
+ <manvolnum>5</manvolnum> </citerefentry>
+ </para>
+ </refsection>
+</refentry>
diff --git a/home-manager/doc/man-pages.xml b/home-manager/doc/man-pages.xml
new file mode 100644
index 00000000000..616c2ef291b
--- /dev/null
+++ b/home-manager/doc/man-pages.xml
@@ -0,0 +1,12 @@
+<reference xmlns="http://docbook.org/ns/docbook"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:xi="http://www.w3.org/2001/XInclude">
+ <title>Home Manager Reference Pages</title>
+ <info>
+ <author><personname>Home Manager contributors</personname></author>
+ <copyright><year>2017–2019</year><holder>Home Manager contributors</holder>
+ </copyright>
+ </info>
+ <xi:include href="man-configuration.xml" />
+ <xi:include href="man-home-manager.xml" />
+</reference>
diff --git a/home-manager/doc/manual.xml b/home-manager/doc/manual.xml
new file mode 100644
index 00000000000..8ff81308b2c
--- /dev/null
+++ b/home-manager/doc/manual.xml
@@ -0,0 +1,38 @@
+<book xmlns="http://docbook.org/ns/docbook"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:xi="http://www.w3.org/2001/XInclude"
+ version="5.0"
+ xml:id="book-home-manager-manual">
+ <info>
+ <title>Home Manager Manual</title>
+ </info>
+ <preface>
+ <title>Preface</title>
+ <para>
+ This manual will eventually describes how to install, use, and extend Home
+ Manager.
+ </para>
+ <para>
+ If you encounter problems or bugs then please report them on the
+ <link xlink:href="https://github.com/rycee/home-manager/issues">Home Manager
+ issue tracker</link>.
+ </para>
+ <note>
+ <para>
+ Commands prefixed with <literal>#</literal> have to be run as root, either
+ requiring to login as root user or temporarily switching to it using
+ <literal>sudo</literal> for example.
+ </para>
+ </note>
+ </preface>
+ <xi:include href="installation.xml" />
+ <appendix xml:id="ch-options">
+ <title>Configuration Options</title>
+ <xi:include href="./nmd-result/home-manager-options.xml" />
+ </appendix>
+ <appendix xml:id="ch-tools">
+ <title>Tools</title>
+ <xi:include href="./man-home-manager.xml" />
+ </appendix>
+ <xi:include href="./release-notes/release-notes.xml" />
+</book>
diff --git a/home-manager/doc/release-notes/release-notes.xml b/home-manager/doc/release-notes/release-notes.xml
new file mode 100644
index 00000000000..2c1f5fcde3b
--- /dev/null
+++ b/home-manager/doc/release-notes/release-notes.xml
@@ -0,0 +1,15 @@
+<appendix xmlns="http://docbook.org/ns/docbook"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:xi="http://www.w3.org/2001/XInclude"
+ version="5.0"
+ xml:id="ch-release-notes">
+ <title>Release Notes</title>
+ <para>
+ This section lists the release notes for stable versions of Home Manager and
+ the current unstable version.
+ </para>
+ <xi:include href="rl-2003.xml" />
+ <xi:include href="rl-1909.xml" />
+ <xi:include href="rl-1903.xml" />
+ <xi:include href="rl-1809.xml" />
+</appendix>
diff --git a/home-manager/doc/release-notes/rl-1809.adoc b/home-manager/doc/release-notes/rl-1809.adoc
new file mode 100644
index 00000000000..b363704e241
--- /dev/null
+++ b/home-manager/doc/release-notes/rl-1809.adoc
@@ -0,0 +1,4 @@
+[[sec-release-18.09]]
+== Release 18.09
+
+The 18.09 release branch became the stable branch in September, 2018.
diff --git a/home-manager/doc/release-notes/rl-1903.adoc b/home-manager/doc/release-notes/rl-1903.adoc
new file mode 100644
index 00000000000..6dfdc67f5bf
--- /dev/null
+++ b/home-manager/doc/release-notes/rl-1903.adoc
@@ -0,0 +1,59 @@
+[[sec-release-19.03]]
+== Release 19.03
+
+The 19.03 release branch became the stable branch in April, 2019.
+
+[[sec-release-19.03-highlights]]
+=== Highlights
+:opt-home-file-source: opt-home.file._name__.source
+
+This release has the following notable changes:
+
+* The <<{opt-home-file-source}>> option now allows source files to be
+hidden, that is, having a name starting with the `.` character. It
+also allows the source file name to contain characters not typically
+allowed for Nix store paths. For example, your configuration can now
+contain things such as
++
+[source,nix]
+----
+home.file."my file".source = ./. + "/file with spaces!";
+----
+
+* The type used for the systemd unit options under
+<<opt-systemd.user.services>>, <<opt-systemd.user.sockets>>, etc. has
+been changed to offer more robust merging of configurations. If you
+don't override values within systemd units then you are not affected
+by this change. Unfortunately, if you do override unit values you may
+encounter errors.
++
+In particular, if you get an error saying that a ``unique option'' is
+``defined multiple times'' then you need to use the
+https://nixos.org/nixos/manual/#sec-option-definitions-setting-priorities[`mkForce`]
+function. For example,
++
+[source,nix]
+----
+systemd.user.services.foo.Service.ExecStart = "/foo/bar";
+----
++
+becomes
++
+[source,nix]
+----
+systemd.user.services.foo.Service.ExecStart = lib.mkForce "/foo/bar";
+----
++
+We had to make this change because the old merging was causing too
+many confusing situations for people.
+
+[[sec-release-19.03-state-version-changes]]
+=== State Version Changes
+
+The state version in this release includes the changes below. These
+changes are only active if the <<opt-home.stateVersion>> option is set
+to ``19.03'' or later.
+
+* There is now an option <<opt-programs.beets.enable>> that defaults
+to `false`. Before the module would be active if the
+<<opt-programs.beets.settings>> option was non-empty.
diff --git a/home-manager/doc/release-notes/rl-1909.adoc b/home-manager/doc/release-notes/rl-1909.adoc
new file mode 100644
index 00000000000..2cc90c164de
--- /dev/null
+++ b/home-manager/doc/release-notes/rl-1909.adoc
@@ -0,0 +1,31 @@
+[[sec-release-19.09]]
+== Release 19.09
+
+The 19.09 release branch became the stable branch in October, 2019.
+
+[[sec-release-19.09-highlights]]
+=== Highlights
+
+This release has the following notable changes:
+
+* The <<opt-programs.firefox.enableGoogleTalk>> and
+ <<opt-programs.firefox.enableIcedTea>> options are now deprecated
+ and will only work if Firefox ESR 52.x is used.
+
+* The `home-manager` tool now provides an `uninstall` sub-command that
+ can be used to uninstall Home Manager, if used in the standalone
+ mode. That is, not as a NixOS module.
+
+[[sec-release-19.09-state-version-changes]]
+=== State Version Changes
+
+The state version in this release includes the changes below. These
+changes are only active if the `home.stateVersion` option is set to
+"19.09" or later.
+
+* The <<opt-programs.firefox.package>> option now expects a wrapped
+ Firefox package and defaults to `pkgs.firefox`.
+
+* The options <<opt-home.keyboard.layout>> and
+ <<opt-home.keyboard.variant>> now default to `null`, which indicates
+ that the system value should be used.
diff --git a/home-manager/doc/release-notes/rl-2003.adoc b/home-manager/doc/release-notes/rl-2003.adoc
new file mode 100644
index 00000000000..fc1dcd7cfe2
--- /dev/null
+++ b/home-manager/doc/release-notes/rl-2003.adoc
@@ -0,0 +1,83 @@
+[[sec-release-20.03]]
+== Release 20.03 (unreleased)
+
+This is the current unstable branch and the information in this
+section is therefore not final.
+
+[[sec-release-20.03-highlights]]
+=== Highlights
+
+This release has the following notable changes:
+
+* Assigning a list to the <<opt-home.file>>, <<opt-xdg.configFile>>,
+and <<opt-xdg.dataFile>> options is now deprecated and will produce a
+warning message if used. Specifically, if your configuration currently
+contains something like
++
+[source,nix]
+----
+home.file = [
+ {
+ target = ".config/foo.txt";
+ text = "bar";
+ }
+]
+----
++
+then it should be updated to instead use the equivalent attribute set form
++
+[source,nix]
+----
+home.file = {
+ ".config/foo.txt".text = "bar";
+}
+----
++
+Support for the list form will be removed in Home Manager version
+20.09.
+
+* The `lib` function attribute given to modules is now enriched with
+an attribute `hm` containing extra library functions specific for Home
+Manager. More specifically, `lib.hm` is now the same as `config.lib`
+and should be the preferred choice since it is more robust.
++
+Therefore, if your configuration makes use of, for example,
+`config.lib.dag` to create activation script blocks, it is recommended
+to change to `lib.hm.dag`.
++
+Note, in the unlikely case that you are
++
+** using Home Manager's NixOS or nix-darwin module,
+** have made your own Home Manager module containing an top-level
+ option named `config` or `options`, and
+** assign to this option in your system configuration inside a plain
+ attribute set, i.e., without a function argument,
+
++
+then you must update your configuration to perform the option
+assignment inside a `config` attribute. For example, instead of
++
+[source,nix]
+----
+home-manager.users.jane = { config = "foo"; };
+----
++
+use
++
+[source,nix]
+----
+home-manager.users.jane = { config.config = "foo"; };
+----
+
+[[sec-release-20.03-state-version-changes]]
+=== State Version Changes
+
+The state version in this release includes the changes below. These
+changes are only active if the `home.stateVersion` option is set to
+"20.03" or later.
+
+* The <<opt-programs.zsh.history.path>> option is no longer prepended
+ by `$HOME`, which allows specifying absolute paths, for example,
+ using the xdg module. Also, the default value is fixed to
+ `$HOME/.zsh_history` and `dotDir` path is not prepended to it
+ anymore.
diff --git a/home-manager/format b/home-manager/format
new file mode 100755
index 00000000000..539ae60de22
--- /dev/null
+++ b/home-manager/format
@@ -0,0 +1,70 @@
+#! /usr/bin/env nix-shell
+#! nix-shell -i bash -p findutils nixfmt
+
+CHECK_ARG=
+
+case $1 in
+ -h)
+ echo "$0 [-c]"
+ ;;
+ -c)
+ CHECK_ARG=-c
+ ;;
+esac
+
+# The first block of excludes are files where nixfmt does a poor job,
+# IMHO. The second block of excludes are files touched by open pull
+# requests and we want to avoid merge conflicts.
+find . -name '*.nix' \
+ ! -path ./modules/programs/irssi.nix \
+ \
+ ! -path ./home-manager/default.nix \
+ ! -path ./home-manager/home-manager.nix \
+ ! -path ./modules/accounts/email.nix \
+ ! -path ./modules/default.nix \
+ ! -path ./modules/files.nix \
+ ! -path ./modules/home-environment.nix \
+ ! -path ./modules/lib/default.nix \
+ ! -path ./modules/lib/file-type.nix \
+ ! -path ./modules/lib/types.nix \
+ ! -path ./modules/manual.nix \
+ ! -path ./modules/misc/dconf.nix \
+ ! -path ./modules/misc/gtk.nix \
+ ! -path ./modules/misc/news.nix \
+ ! -path ./modules/misc/nixpkgs.nix \
+ ! -path ./modules/misc/xdg.nix \
+ ! -path ./modules/modules.nix \
+ ! -path ./modules/programs/afew.nix \
+ ! -path ./modules/programs/alot.nix \
+ ! -path ./modules/programs/bash.nix \
+ ! -path ./modules/programs/emacs.nix \
+ ! -path ./modules/programs/firefox.nix \
+ ! -path ./modules/programs/fish.nix \
+ ! -path ./modules/programs/gpg.nix \
+ ! -path ./modules/programs/lesspipe.nix \
+ ! -path ./modules/programs/neovim.nix \
+ ! -path ./modules/programs/ssh.nix \
+ ! -path ./modules/programs/tmux.nix \
+ ! -path ./modules/programs/vscode.nix \
+ ! -path ./modules/programs/zsh.nix \
+ ! -path ./modules/services/gpg-agent.nix \
+ ! -path ./modules/services/kbfs.nix \
+ ! -path ./modules/services/keybase.nix \
+ ! -path ./modules/services/mpd.nix \
+ ! -path ./modules/services/sxhkd.nix \
+ ! -path ./modules/services/window-managers/i3.nix \
+ ! -path ./modules/systemd.nix \
+ ! -path ./nix-darwin/default.nix \
+ ! -path ./tests/default.nix \
+ ! -path ./tests/modules/home-environment/default.nix \
+ ! -path ./tests/modules/home-environment/session-variables.nix \
+ ! -path ./tests/modules/programs/gpg/override-defaults.nix \
+ ! -path ./tests/modules/programs/tmux/default.nix \
+ ! -path ./tests/modules/programs/tmux/disable-confirmation-prompt.nix \
+ ! -path ./tests/modules/programs/tmux/secure-socket-enabled.nix \
+ ! -path ./tests/modules/programs/zsh/session-variables.nix \
+ ! -path ./tests/modules/services/sxhkd/service.nix \
+ ! -path ./tests/modules/systemd/default.nix \
+ ! -path ./tests/modules/systemd/services.nix \
+ ! -path ./tests/modules/systemd/session-variables.nix \
+ -exec nixfmt $CHECK_ARG {} +
diff --git a/home-manager/home-manager/completion.bash b/home-manager/home-manager/completion.bash
new file mode 100644
index 00000000000..501b87279fa
--- /dev/null
+++ b/home-manager/home-manager/completion.bash
@@ -0,0 +1,356 @@
+#!/bin/env bash
+
+##################################################
+
+# « home-manager » command-line completion
+#
+# © 2019 "Sam Boosalis" <samboosalis@gmail.com>
+#
+# MIT License
+#
+
+##################################################
+# Contributing:
+
+# Compatibility — Bash 3.
+#
+# OSX won't update Bash 3 (last updated circa 2009) to Bash 4,
+# and we'd like this completion script to work on both Linux and Mac.
+#
+# For example, OSX Yosemite (released circa 2014) ships with Bash 3:
+#
+# $ echo $BASH_VERSION
+# 3.2
+#
+# While Ubuntu LTS 14.04 (a.k.a. Trusty, also released circa 2016)
+# ships with the latest version, Bash 4 (updated circa 2016):
+#
+# $ echo $BASH_VERSION
+# 4.3
+#
+
+# Testing
+#
+# (1) Invoke « shellcheck »
+#
+# * source: « https://github.com/koalaman/shellcheck »
+# * run: « shellcheck ./share/bash-completion/completions/home-manager »
+#
+# (2) Interpret via Bash 3
+#
+# * run: « bash --noprofile --norc ./share/bash-completion/completions/home-manager »
+#
+
+##################################################
+# Examples:
+
+# $ home-manager <TAB>
+#
+# -A
+# -I
+# -f
+# --file
+# -h
+# --help
+# -n
+# --dry-run
+# -v
+# --verbose
+# build
+# edit
+# expire-generations
+# generations
+# help
+# news
+# packages
+# remove-generations
+# switch
+# uninstall
+
+# $ home-manager e<TAB>
+#
+# edit
+# expire-generations
+
+# $ home-manager remove-generations 20<TAB>
+#
+# 200
+# 201
+# 202
+# 203
+
+##################################################
+# Notes:
+
+# « home-manager » Subcommands:
+#
+# help
+# edit
+# build
+# switch
+# generations
+# remove-generations
+# expire-generations
+# packages
+# news
+# uninstall
+
+# « home-manager » Options:
+#
+# -b EXT
+# -f FILE
+# --file FILE
+# -A ATTRIBUTE
+# -I PATH
+# -v
+# --verbose
+# -n
+# --dry-run
+# -h
+# --help
+
+# $ home-manager
+#
+# Usage: /home/sboo/.nix-profile/bin/home-manager [OPTION] COMMAND
+#
+# Options
+#
+# -f FILE The home configuration file.
+# Default is '~/.config/nixpkgs/home.nix'.
+# -A ATTRIBUTE Optional attribute that selects a configuration
+# expression in the configuration file.
+# -I PATH Add a path to the Nix expression search path.
+# -b EXT Move existing files to new path rather than fail.
+# -v Verbose output
+# -n Do a dry run, only prints what actions would be taken
+# -h Print this help
+#
+# Commands
+#
+# help Print this help
+#
+# edit Open the home configuration in $EDITOR
+#
+# build Build configuration into result directory
+#
+# switch Build and activate configuration
+#
+# generations List all home environment generations
+#
+# remove-generations ID...
+# Remove indicated generations. Use 'generations' command to
+# find suitable generation numbers.
+#
+# expire-generations TIMESTAMP
+# Remove generations older than TIMESTAMP where TIMESTAMP is
+# interpreted as in the -d argument of the date tool. For
+# example "-30 days" or "2018-01-01".
+#
+# packages List all packages installed in home-manager-path
+#
+# news Show news entries in a pager
+#
+# uninstall Remove Home Manager
+#
+##################################################
+# Dependencies:
+
+command -v home-manager >/dev/null
+command -v grep >/dev/null
+command -v sed >/dev/null
+
+##################################################
+# Code:
+
+_home-manager_list-generation-identifiers ()
+
+{
+
+ home-manager generations | sed -n -e 's/^................ : id \([[:alnum:]]\+\) -> .*/\1/p'
+
+}
+
+# NOTES
+#
+# (1) the « sed -n -e 's/.../.../p' » invocation:
+#
+# * the « -e '...' » option takes a Sed Script.
+# * the « -n » option only prints when « .../p » would print.
+# * the « s/xxx/yyy/ » Sed Script substitutes « yyy » whenever « xxx » is matched.
+#
+# (2) the « '^................ : id \([[:alnum:]]\+\) -> .*' » regular expression:
+#
+# * matches « 199 », for example, in the line « 2019-03-13 15:26 : id 199 -> /nix/store/mv619y9pzgsx3kndq0q7fjfvbqqdy5k8-home-manager-generation »
+#
+#
+
+#------------------------------------------------#
+
+# shellcheck disable=SC2120
+_home-manager_list-nix-attributes ()
+
+{
+ local HomeFile
+ local HomeAttrsString
+ # local HomeAttrsArray
+ # local HomeAttr
+
+ if [ -z "$1" ]
+ then
+ HomeFile=$(readlink -f "$(_home-manager_get-default-home-file)")
+ else
+ HomeFile="$1"
+ fi
+
+ HomeAttrsString=$(nix-instantiate --eval -E "let home = import ${HomeFile}; in (builtins.trace (builtins.toString (builtins.attrNames home)) null)" |& grep '^trace: ')
+ HomeAttrsString="${HomeAttrsString#trace: }"
+
+ echo "${HomeAttrsString}"
+
+ # IFS=" " read -ar HomeAttrsArray <<< "${HomeAttrsString}"
+ #
+ # local HomeAttr
+ # for HomeAttr in "${HomeAttrsArray[@]}"
+ # do
+ # echo "${HomeAttr}"
+ # done
+
+}
+
+# e.g.:
+#
+# $ nix-instantiate --eval -E 'let home = import /home/sboo/configuration/configs/nixpkgs/home-attrs.nix; in (builtins.trace (builtins.toString (builtins.attrNames home)) null)' 1>/dev/null
+# trace: darwin linux
+#
+# $ _home-manager_list-nix-attributes
+# linux darwin
+#
+
+#------------------------------------------------#
+
+_home-manager_get-default-home-file ()
+
+{
+ local HomeFileDefault
+
+ HomeFileDefault="$(_home-manager_xdg-get-config-home)/nixpkgs/home.nix"
+
+ echo "${HomeFileDefault}"
+}
+
+# e.g.:
+#
+# $ _home-manager_get-default-home-file
+# ~/.config/nixpkgs/home.nix
+#
+
+##################################################
+# XDG-BaseDirs:
+
+_home-manager_xdg-get-config-home () {
+
+ echo "${XDG_CONFIG_HOME:-$HOME/.config}"
+
+}
+
+#------------------------------------------------#
+
+_home-manager_xdg-get-data-home () {
+
+ echo "${XDG_DATA_HOME:-$HOME/.local/share}"
+
+}
+
+
+#------------------------------------------------#
+_home-manager_xdg-get-cache-home () {
+
+ echo "${XDG_CACHE_HOME:-$HOME/.cache}"
+
+}
+
+##################################################
+
+# shellcheck disable=SC2207
+_home-manager_completions ()
+{
+
+ #--------------------------#
+
+ local Subcommands
+ Subcommands=( "help" "edit" "build" "switch" "generations" "remove-generations" "expire-generations" "packages" "news" "uninstall" )
+
+ # ^ « home-manager »'s subcommands.
+
+ #--------------------------#
+
+ local Options
+ Options=( "-f" "--file" "-b" "-A" "-I" "-h" "--help" "-n" "--dry-run" "-v" "--verbose" "--show-trace" )
+
+ # ^ « home-manager »'s options.
+
+ #--------------------------#
+
+ local CurrentWord
+ CurrentWord="${COMP_WORDS[$COMP_CWORD]}"
+
+ # ^ the word currently being completed
+
+ local PreviousWord
+ if [ "$COMP_CWORD" -ge 1 ]
+ then
+ PreviousWord="${COMP_WORDS[COMP_CWORD-1]}"
+ else
+ PreviousWord=""
+ fi
+
+ # ^ the word to the left of the current word.
+ #
+ # e.g. in « home-manager -v -f ./<TAB> »:
+ #
+ # PreviousWord="-f"
+ # CurrentWord="./"
+
+ #--------------------------#
+
+ COMPREPLY=()
+
+ case "$PreviousWord" in
+
+ "-f"|"--file")
+
+ COMPREPLY+=( $( compgen -A file -- "$CurrentWord") )
+ ;;
+
+ "-I")
+
+ COMPREPLY+=( $( compgen -A directory -- "$CurrentWord") )
+ ;;
+
+ "-A")
+
+ # shellcheck disable=SC2119
+ COMPREPLY+=( $( compgen -W "$(_home-manager_list-nix-attributes)" -- "$CurrentWord") )
+ ;;
+
+ "remove-generations")
+
+ COMPREPLY+=( $( compgen -W "$(_home-manager_list-generation-identifiers)" -- "$CurrentWord" ) )
+ ;;
+
+ *)
+
+ COMPREPLY+=( $( compgen -W "${Subcommands[*]}" -- "$CurrentWord" ) )
+ COMPREPLY+=( $( compgen -W "${Options[*]}" -- "$CurrentWord" ) )
+ ;;
+
+ esac
+
+ #--------------------------#
+}
+
+##################################################
+
+complete -F _home-manager_completions -o default home-manager
+
+#complete -W "help edit build switch generations remove-generations expire-generations packages news" home-manager
diff --git a/home-manager/home-manager/default.nix b/home-manager/home-manager/default.nix
new file mode 100644
index 00000000000..8b5ae75e0fd
--- /dev/null
+++ b/home-manager/home-manager/default.nix
@@ -0,0 +1,40 @@
+{ runCommand, lib, bash, coreutils, findutils, gnused, less
+
+ # Extra path to Home Manager. If set then this path will be tried
+ # before `$HOME/.config/nixpkgs/home-manager` and
+ # `$HOME/.nixpkgs/home-manager`.
+, path ? null
+}:
+
+let
+
+ pathStr = if path == null then "" else path;
+
+in
+
+runCommand
+ "home-manager"
+ {
+ preferLocalBuild = true;
+ allowSubstitutes = false;
+ meta = with lib; {
+ description = "A user environment configurator";
+ maintainers = [ maintainers.rycee ];
+ platforms = platforms.unix;
+ license = licenses.mit;
+ };
+ }
+ ''
+ install -v -D -m755 ${./home-manager} $out/bin/home-manager
+
+ substituteInPlace $out/bin/home-manager \
+ --subst-var-by bash "${bash}" \
+ --subst-var-by coreutils "${coreutils}" \
+ --subst-var-by findutils "${findutils}" \
+ --subst-var-by gnused "${gnused}" \
+ --subst-var-by less "${less}" \
+ --subst-var-by HOME_MANAGER_PATH '${pathStr}'
+
+ install -D -m755 ${./completion.bash} \
+ $out/share/bash-completion/completions/home-manager
+ ''
diff --git a/home-manager/home-manager/home-manager b/home-manager/home-manager/home-manager
new file mode 100644
index 00000000000..d5a4c4f41a1
--- /dev/null
+++ b/home-manager/home-manager/home-manager
@@ -0,0 +1,579 @@
+#!@bash@/bin/bash
+
+# Prepare to use tools from Nixpkgs.
+PATH=@coreutils@/bin:@findutils@/bin:@gnused@/bin:@less@/bin${PATH:+:}$PATH
+
+set -euo pipefail
+
+function errorEcho() {
+ # shellcheck disable=2048,2086
+ echo $* >&2
+}
+
+function setVerboseAndDryRun() {
+ if [[ -v VERBOSE ]]; then
+ export VERBOSE_ARG="--verbose"
+ else
+ export VERBOSE_ARG=""
+ fi
+
+ if [[ -v DRY_RUN ]] ; then
+ export DRY_RUN_CMD=echo
+ else
+ export DRY_RUN_CMD=""
+ fi
+}
+
+function setWorkDir() {
+ if [[ ! -v WORK_DIR ]]; then
+ WORK_DIR="$(mktemp --tmpdir -d home-manager-build.XXXXXXXXXX)"
+ # shellcheck disable=2064
+ trap "rm -r '$WORK_DIR'" EXIT
+ fi
+}
+
+# Attempts to set the HOME_MANAGER_CONFIG global variable.
+#
+# If no configuration file can be found then this function will print
+# an error message and exit with an error code.
+function setConfigFile() {
+ if [[ -v HOME_MANAGER_CONFIG ]] ; then
+ if [[ ! -e "$HOME_MANAGER_CONFIG" ]] ; then
+ errorEcho "No configuration file found at $HOME_MANAGER_CONFIG"
+ exit 1
+ fi
+
+ HOME_MANAGER_CONFIG="$(realpath "$HOME_MANAGER_CONFIG")"
+ return
+ fi
+
+ local defaultConfFile="${XDG_CONFIG_HOME:-$HOME/.config}/nixpkgs/home.nix"
+ local confFile
+ for confFile in "$defaultConfFile" \
+ "$HOME/.nixpkgs/home.nix" ; do
+ if [[ -e "$confFile" ]] ; then
+ HOME_MANAGER_CONFIG="$(realpath "$confFile")"
+ return
+ fi
+ done
+
+ errorEcho "No configuration file found." \
+ "Please create one at $defaultConfFile"
+ exit 1
+}
+
+function setHomeManagerNixPath() {
+ local path
+ for path in "@HOME_MANAGER_PATH@" \
+ "${XDG_CONFIG_HOME:-$HOME/.config}/nixpkgs/home-manager" \
+ "$HOME/.nixpkgs/home-manager" ; do
+ if [[ -e "$path" || "$path" =~ ^https?:// ]] ; then
+ export NIX_PATH="home-manager=$path${NIX_PATH:+:}$NIX_PATH"
+ return
+ fi
+ done
+}
+
+function doBuildAttr() {
+ setConfigFile
+ setHomeManagerNixPath
+
+ local extraArgs="$*"
+
+ for p in "${EXTRA_NIX_PATH[@]}"; do
+ extraArgs="$extraArgs -I $p"
+ done
+
+ if [[ -v VERBOSE ]]; then
+ extraArgs="$extraArgs --show-trace"
+ fi
+
+ # shellcheck disable=2086
+ if [[ -v USE_NIX2_COMMAND ]]; then
+ nix build \
+ -f "<home-manager/home-manager/home-manager.nix>" \
+ $extraArgs \
+ "${PASSTHROUGH_OPTS[@]}" \
+ --argstr confPath "$HOME_MANAGER_CONFIG" \
+ --argstr confAttr "$HOME_MANAGER_CONFIG_ATTRIBUTE"
+ else
+ nix-build \
+ "<home-manager/home-manager/home-manager.nix>" \
+ $extraArgs \
+ "${PASSTHROUGH_OPTS[@]}" \
+ --argstr confPath "$HOME_MANAGER_CONFIG" \
+ --argstr confAttr "$HOME_MANAGER_CONFIG_ATTRIBUTE"
+ fi
+}
+
+# Presents news to the user. Takes as argument the path to a "news
+# info" file as generated by `buildNews`.
+function presentNews() {
+ local infoFile="$1"
+
+ # shellcheck source=/dev/null
+ . "$infoFile"
+
+ # shellcheck disable=2154
+ if [[ $newsNumUnread -eq 0 ]]; then
+ return
+ elif [[ "$newsDisplay" == "silent" ]]; then
+ return
+ elif [[ "$newsDisplay" == "notify" ]]; then
+ local msg
+ if [[ $newsNumUnread -eq 1 ]]; then
+ msg="There is an unread and relevant news item.\n"
+ msg+="Read it by running the command '$(basename "$0") news'."
+ else
+ msg="There are $newsNumUnread unread and relevant news items.\n"
+ msg+="Read them by running the command '$(basename "$0") news'."
+ fi
+
+ # Not actually an error but here stdout is reserved for
+ # nix-build output.
+ errorEcho
+ errorEcho -e "$msg"
+ errorEcho
+
+ if [[ -v DISPLAY ]] && type -P notify-send > /dev/null; then
+ notify-send "Home Manager" "$msg"
+ fi
+ elif [[ "$newsDisplay" == "show" ]]; then
+ doShowNews --unread
+ else
+ errorEcho "Unknown 'news.display' setting '$newsDisplay'."
+ fi
+}
+
+function doEdit() {
+ if [[ ! -v EDITOR || -z $EDITOR ]]; then
+ errorEcho "Please set the \$EDITOR environment variable"
+ return 1
+ fi
+
+ setConfigFile
+
+ exec "$EDITOR" "$HOME_MANAGER_CONFIG"
+}
+
+function doBuild() {
+ if [[ ! -w . ]]; then
+ errorEcho "Cannot run build in read-only directory";
+ return 1
+ fi
+
+ setWorkDir
+
+ local newsInfo
+ newsInfo=$(buildNews)
+
+ local exitCode
+
+ if [[ -v USE_NIX2_COMMAND ]]; then
+ doBuildAttr activationPackage \
+ && exitCode=0 || exitCode=1
+ else
+ doBuildAttr --attr activationPackage \
+ && exitCode=0 || exitCode=1
+ fi
+
+ presentNews "$newsInfo"
+
+ return $exitCode
+}
+
+function doSwitch() {
+ setWorkDir
+
+ local newsInfo
+ newsInfo=$(buildNews)
+
+ local generation
+ local exitCode=0
+
+ # Build the generation and run the activate script. Note, we
+ # specify an output link so that it is treated as a GC root. This
+ # prevents an unfortunately timed GC from removing the generation
+ # before activation completes.
+ generation="$WORK_DIR/generation"
+
+ if [[ -v USE_NIX2_COMMAND ]]; then
+ doBuildAttr \
+ --out-link "$generation" \
+ activationPackage \
+ && "$generation/activate" || exitCode=1
+ else
+ doBuildAttr \
+ --out-link "$generation" \
+ --attr activationPackage \
+ && "$generation/activate" || exitCode=1
+ fi
+
+ presentNews "$newsInfo"
+
+ return $exitCode
+}
+
+function doListGens() {
+ # Whether to colorize the generations output.
+ local color="never"
+ if [[ -t 1 ]]; then
+ color="always"
+ fi
+
+ pushd "/nix/var/nix/profiles/per-user/$USER" > /dev/null
+ # shellcheck disable=2012
+ ls --color=$color -gG --time-style=long-iso --sort time home-manager-*-link \
+ | cut -d' ' -f 4- \
+ | sed -E 's/home-manager-([[:digit:]]*)-link/: id \1/'
+ popd > /dev/null
+}
+
+# Removes linked generations. Takes as arguments identifiers of
+# generations to remove.
+function doRmGenerations() {
+ setVerboseAndDryRun
+
+ pushd "/nix/var/nix/profiles/per-user/$USER" > /dev/null
+
+ for generationId in "$@"; do
+ local linkName="home-manager-$generationId-link"
+
+ if [[ ! -e $linkName ]]; then
+ errorEcho "No generation with ID $generationId"
+ elif [[ $linkName == $(readlink home-manager) ]]; then
+ errorEcho "Cannot remove the current generation $generationId"
+ else
+ echo Removing generation $generationId
+ $DRY_RUN_CMD rm $VERBOSE_ARG $linkName
+ fi
+ done
+
+ popd > /dev/null
+}
+
+function doRmAllGenerations() {
+ $DRY_RUN_CMD rm $VERBOSE_ARG \
+ "/nix/var/nix/profiles/per-user/$USER/home-manager"*
+}
+
+function doExpireGenerations() {
+ local profileDir="/nix/var/nix/profiles/per-user/$USER"
+
+ local generations
+ generations="$( \
+ find "$profileDir" -name 'home-manager-*-link' -not -newermt "$1" \
+ | sed 's/^.*-\([0-9]*\)-link$/\1/' \
+ )"
+
+ if [[ -n $generations ]]; then
+ # shellcheck disable=2086
+ doRmGenerations $generations
+ elif [[ -v VERBOSE ]]; then
+ echo "No generations to expire"
+ fi
+}
+
+function doListPackages() {
+ local outPath
+ outPath="$(nix-env -q --out-path | grep -o '/.*home-manager-path$')"
+ if [[ -n "$outPath" ]] ; then
+ nix-store -q --references "$outPath" | sed 's/[^-]*-//'
+ else
+ errorEcho "No home-manager packages seem to be installed."
+ fi
+}
+
+function newsReadIdsFile() {
+ local dataDir="${XDG_DATA_HOME:-$HOME/.local/share}/home-manager"
+ local path="$dataDir/news-read-ids"
+
+ # If the path doesn't exist then we should create it, otherwise
+ # Nix will error out when we attempt to use builtins.readFile.
+ if [[ ! -f "$path" ]]; then
+ mkdir -p "$dataDir"
+ touch "$path"
+ fi
+
+ echo "$path"
+}
+
+# Builds news meta information to be sourced into this script.
+#
+# Note, we suppress build output to remove unnecessary verbosity. We
+# put the output in the work directory to avoid the risk of an
+# unfortunately timed GC removing it.
+function buildNews() {
+ local output
+ output="$WORK_DIR/news-info.sh"
+
+ if [[ -v USE_NIX2_COMMAND ]]; then
+ doBuildAttr \
+ --out-link "$output" \
+ --quiet \
+ --arg check false \
+ --argstr newsReadIdsFile "$(newsReadIdsFile)" \
+ newsInfo
+ else
+ doBuildAttr \
+ --out-link "$output" \
+ --no-build-output \
+ --quiet \
+ --arg check false \
+ --argstr newsReadIdsFile "$(newsReadIdsFile)" \
+ --attr newsInfo \
+ > /dev/null
+ fi
+
+ echo "$output"
+}
+
+function doShowNews() {
+ setWorkDir
+
+ local infoFile
+ infoFile=$(buildNews) || return 1
+
+ # shellcheck source=/dev/null
+ . "$infoFile"
+
+ # shellcheck disable=2154
+ case $1 in
+ --all)
+ ${PAGER:-less} "$newsFileAll"
+ ;;
+ --unread)
+ ${PAGER:-less} "$newsFileUnread"
+ ;;
+ *)
+ errorEcho "Unknown argument $1"
+ return 1
+ esac
+
+ # shellcheck disable=2154
+ if [[ -s "$newsUnreadIdsFile" ]]; then
+ local newsReadIdsFile
+ newsReadIdsFile="$(newsReadIdsFile)"
+ cat "$newsUnreadIdsFile" >> "$newsReadIdsFile"
+ fi
+}
+
+function doUninstall() {
+ setVerboseAndDryRun
+
+ echo "This will remove Home Manager from your system."
+
+ if [[ -v DRY_RUN ]]; then
+ echo "This is a dry run, nothing will actually be uninstalled."
+ fi
+
+ local confirmation
+ read -r -n 1 -p "Really uninstall Home Manager? [y/n] " confirmation
+ echo
+
+ case $confirmation in
+ y|Y)
+ echo "Switching to empty Home Manager configuration..."
+ HOME_MANAGER_CONFIG="$(mktemp --tmpdir home-manager.XXXXXXXXXX)"
+ echo "{}" > "$HOME_MANAGER_CONFIG"
+ doSwitch
+ rm "$HOME_MANAGER_CONFIG"
+ $DRY_RUN_CMD rm $VERBOSE_ARG -r \
+ "${XDG_DATA_HOME:-$HOME/.local/share}/home-manager"
+ $DRY_RUN_CMD rm $VERBOSE_ARG \
+ "/nix/var/nix/gcroots/per-user/$USER/current-home"
+ ;;
+ *)
+ echo "Yay!"
+ exit 0
+ ;;
+ esac
+
+ local deleteProfiles
+ read -r -n 1 \
+ -p 'Remove all Home Manager generations? [y/n] ' \
+ deleteProfiles
+ echo
+
+ case $deleteProfiles in
+ y|Y)
+ doRmAllGenerations
+ echo "All generations are now eligible for garbage collection."
+ ;;
+ *)
+ echo "Leaving generations but they may still be garbage collected."
+ ;;
+ esac
+
+ echo "Home Manager is uninstalled but your home.nix is left untouched."
+}
+
+function doHelp() {
+ echo "Usage: $0 [OPTION] COMMAND"
+ echo
+ echo "Options"
+ echo
+ echo " -f FILE The home configuration file."
+ echo " Default is '~/.config/nixpkgs/home.nix'."
+ echo " -A ATTRIBUTE Optional attribute that selects a configuration"
+ echo " expression in the configuration file."
+ echo " -I PATH Add a path to the Nix expression search path."
+ echo " -b EXT Move existing files to new path rather than fail."
+ echo " -v Verbose output"
+ echo " -n Do a dry run, only prints what actions would be taken"
+ echo " -h Print this help"
+ echo
+ echo "Options passed on to nix-build(1)"
+ echo
+ echo " --cores NUM"
+ echo " --keep-failed"
+ echo " --keep-going"
+ echo " --max-jobs NUM"
+ echo " --option NAME VALUE"
+ echo " --show-trace"
+ echo " --(no-)substitute"
+ echo
+ echo "Commands"
+ echo
+ echo " help Print this help"
+ echo
+ echo " edit Open the home configuration in \$EDITOR"
+ echo
+ echo " build Build configuration into result directory"
+ echo
+ echo " switch Build and activate configuration"
+ echo
+ echo " generations List all home environment generations"
+ echo
+ echo " remove-generations ID..."
+ echo " Remove indicated generations. Use 'generations' command to"
+ echo " find suitable generation numbers."
+ echo
+ echo " expire-generations TIMESTAMP"
+ echo " Remove generations older than TIMESTAMP where TIMESTAMP is"
+ echo " interpreted as in the -d argument of the date tool. For"
+ echo " example \"-30 days\" or \"2018-01-01\"."
+ echo
+ echo " packages List all packages installed in home-manager-path"
+ echo
+ echo " news Show news entries in a pager"
+ echo
+ echo " uninstall Remove Home Manager"
+}
+
+EXTRA_NIX_PATH=()
+HOME_MANAGER_CONFIG_ATTRIBUTE=""
+PASSTHROUGH_OPTS=()
+COMMAND=""
+COMMAND_ARGS=()
+
+while [[ $# -gt 0 ]]; do
+ opt="$1"
+ shift
+ case $opt in
+ build|edit|expire-generations|generations|help|news|packages|remove-generations|switch|uninstall)
+ COMMAND="$opt"
+ ;;
+ -2)
+ USE_NIX2_COMMAND=1
+ ;;
+ -A)
+ HOME_MANAGER_CONFIG_ATTRIBUTE="$1"
+ shift
+ ;;
+ -I)
+ EXTRA_NIX_PATH+=("$1")
+ shift
+ ;;
+ -b)
+ export HOME_MANAGER_BACKUP_EXT="$1"
+ shift
+ ;;
+ -f|--file)
+ HOME_MANAGER_CONFIG="$1"
+ shift
+ ;;
+ -h|--help)
+ doHelp
+ exit 0
+ ;;
+ -n|--dry-run)
+ export DRY_RUN=1
+ ;;
+ --option)
+ PASSTHROUGH_OPTS+=("$opt" "$1" "$2")
+ shift 2
+ ;;
+ --max-jobs|--cores)
+ PASSTHROUGH_OPTS+=("$opt" "$1")
+ shift
+ ;;
+ --keep-failed|--keep-going|--show-trace\
+ |--substitute|--no-substitute)
+ PASSTHROUGH_OPTS+=("$opt")
+ ;;
+ -v|--verbose)
+ export VERBOSE=1
+ ;;
+ *)
+ case $COMMAND in
+ expire-generations|remove-generations)
+ COMMAND_ARGS+=("$opt")
+ ;;
+ *)
+ errorEcho "$0: unknown option '$opt'"
+ errorEcho "Run '$0 --help' for usage help"
+ exit 1
+ ;;
+ esac
+ ;;
+ esac
+done
+
+if [[ -z $COMMAND ]]; then
+ doHelp >&2
+ exit 1
+fi
+
+case $COMMAND in
+ edit)
+ doEdit
+ ;;
+ build)
+ doBuild
+ ;;
+ switch)
+ doSwitch
+ ;;
+ generations)
+ doListGens
+ ;;
+ remove-generations)
+ doRmGenerations "${COMMAND_ARGS[@]}"
+ ;;
+ expire-generations)
+ if [[ ${#COMMAND_ARGS[@]} != 1 ]]; then
+ errorEcho "expire-generations expects one argument, got ${#COMMAND_ARGS[@]}."
+ exit 1
+ else
+ doExpireGenerations "${COMMAND_ARGS[@]}"
+ fi
+ ;;
+ packages)
+ doListPackages
+ ;;
+ news)
+ doShowNews --all
+ ;;
+ uninstall)
+ doUninstall
+ ;;
+ help)
+ doHelp
+ ;;
+ *)
+ errorEcho "Unknown command: $COMMAND"
+ doHelp >&2
+ exit 1
+ ;;
+esac
diff --git a/home-manager/home-manager/home-manager.nix b/home-manager/home-manager/home-manager.nix
new file mode 100644
index 00000000000..7a6748942c8
--- /dev/null
+++ b/home-manager/home-manager/home-manager.nix
@@ -0,0 +1,88 @@
+{ pkgs ? import <nixpkgs> {}
+, confPath
+, confAttr
+, check ? true
+, newsReadIdsFile ? null
+}:
+
+with pkgs.lib;
+
+let
+
+ env = import ../modules {
+ configuration =
+ if confAttr == ""
+ then confPath
+ else (import confPath).${confAttr};
+ pkgs = pkgs;
+ check = check;
+ };
+
+ newsReadIds =
+ if newsReadIdsFile == null
+ then {}
+ else
+ let
+ ids = splitString "\n" (fileContents newsReadIdsFile);
+ in
+ builtins.listToAttrs (map (id: { name = id; value = null; }) ids);
+
+ newsIsRead = entry: builtins.hasAttr entry.id newsReadIds;
+
+ newsFiltered =
+ let
+ pred = entry: entry.condition && ! newsIsRead entry;
+ in
+ filter pred env.newsEntries;
+
+ newsNumUnread = length newsFiltered;
+
+ newsFileUnread = pkgs.writeText "news-unread.txt" (
+ concatMapStringsSep "\n\n" (entry:
+ let
+ time = replaceStrings ["T"] [" "] (removeSuffix "+00:00" entry.time);
+ in
+ ''
+ * ${time}
+
+ ${replaceStrings ["\n"] ["\n "] entry.message}
+ ''
+ ) newsFiltered
+ );
+
+ newsFileAll = pkgs.writeText "news-all.txt" (
+ concatMapStringsSep "\n\n" (entry:
+ let
+ flag = if newsIsRead entry then "read" else "unread";
+ time = replaceStrings ["T"] [" "] (removeSuffix "+00:00" entry.time);
+ in
+ ''
+ * ${time} [${flag}]
+
+ ${replaceStrings ["\n"] ["\n "] entry.message}
+ ''
+ ) env.newsEntries
+ );
+
+ # File where each line corresponds to an unread news entry
+ # identifier. If non-empty then the file ends in "\n".
+ newsUnreadIdsFile = pkgs.writeText "news-unread-ids" (
+ let
+ text = concatMapStringsSep "\n" (entry: entry.id) newsFiltered;
+ in
+ text + optionalString (text != "") "\n"
+ );
+
+ newsInfo = pkgs.writeText "news-info.sh" ''
+ local newsNumUnread=${toString newsNumUnread}
+ local newsDisplay="${env.newsDisplay}"
+ local newsFileAll="${newsFileAll}"
+ local newsFileUnread="${newsFileUnread}"
+ local newsUnreadIdsFile="${newsUnreadIdsFile}"
+ '';
+
+in
+ {
+ inherit (env) activationPackage;
+ inherit newsInfo;
+ }
diff --git a/home-manager/home-manager/install.nix b/home-manager/home-manager/install.nix
new file mode 100644
index 00000000000..87aae50470e
--- /dev/null
+++ b/home-manager/home-manager/install.nix
@@ -0,0 +1,67 @@
+{ home-manager, runCommand }:
+
+runCommand "home-manager-install" {
+ propagatedBuildInputs = [ home-manager ];
+ preferLocalBuild = true;
+ allowSubstitutes = false;
+ shellHookOnly = true;
+ shellHook = ''
+ confFile="''${XDG_CONFIG_HOME:-$HOME/.config}/nixpkgs/home.nix"
+
+ if [[ ! -e $confFile ]]; then
+ echo
+ echo "Creating initial Home Manager configuration..."
+
+ mkdir -p "$(dirname "$confFile")"
+ cat > $confFile <<EOF
+ { config, pkgs, ... }:
+
+ {
+ # Let Home Manager install and manage itself.
+ programs.home-manager.enable = true;
+
+ # This value determines the Home Manager release that your
+ # configuration is compatible with. This helps avoid breakage
+ # when a new Home Manager release introduces backwards
+ # incompatible changes.
+ #
+ # You can update Home Manager without changing this value. See
+ # the Home Manager release notes for a list of state version
+ # changes in each release.
+ home.stateVersion = "19.09";
+ }
+ EOF
+ fi
+
+ echo
+ echo "Creating initial Home Manager generation..."
+ echo
+
+ if home-manager switch; then
+ cat <<EOF
+
+ All done! The home-manager tool should now be installed and you
+ can edit
+
+ $confFile
+
+ to configure Home Manager. Run 'man home-configuration.nix' to
+ see all available options.
+ EOF
+ exit 0
+ else
+ cat <<EOF
+
+ Uh oh, the installation failed! Please create an issue at
+
+ https://github.com/rycee/home-manager/issues
+
+ if the error seems to be the fault of Home Manager.
+ EOF
+ exit 1
+ fi
+ '';
+} ''
+ echo This derivation is not buildable, instead run it using nix-shell.
+ exit 1
+''
diff --git a/home-manager/modules/accounts/email.nix b/home-manager/modules/accounts/email.nix
new file mode 100644
index 00000000000..b3a9db947a9
--- /dev/null
+++ b/home-manager/modules/accounts/email.nix
@@ -0,0 +1,423 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.accounts.email;
+
+ gpgModule = types.submodule {
+ options = {
+ key = mkOption {
+ type = types.str;
+ description = ''
+ The key to use as listed in <command>gpg --list-keys</command>.
+ '';
+ };
+
+ signByDefault = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Sign messages by default.";
+ };
+
+ encryptByDefault = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Encrypt outgoing messages by default.";
+ };
+ };
+ };
+
+ signatureModule = types.submodule {
+ options = {
+ text = mkOption {
+ type = types.str;
+ default = "";
+ example = ''
+ --
+ Luke Skywalker
+ May the force be with you.
+ '';
+ description = ''
+ Signature content.
+ '';
+ };
+
+ showSignature = mkOption {
+ type = types.enum [ "append" "attach" "none" ];
+ default = "none";
+ description = "Method to communicate the signature.";
+ };
+ };
+ };
+
+ tlsModule = types.submodule {
+ options = {
+ enable = mkOption {
+ type = types.bool;
+ default = true;
+ description = ''
+ Whether to enable TLS/SSL.
+ '';
+ };
+
+ useStartTls = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Whether to use STARTTLS.
+ '';
+ };
+
+ certificatesFile = mkOption {
+ type = types.path;
+ default = config.accounts.email.certificatesFile;
+ defaultText = "config.accounts.email.certificatesFile";
+ description = ''
+ Path to file containing certificate authorities that should
+ be used to validate the connection authenticity. If
+ <literal>null</literal> then the system default is used.
+ Note, if set then the system default may still be accepted.
+ '';
+ };
+ };
+ };
+
+ imapModule = types.submodule {
+ options = {
+ host = mkOption {
+ type = types.str;
+ example = "imap.example.org";
+ description = ''
+ Hostname of IMAP server.
+ '';
+ };
+
+ port = mkOption {
+ type = types.nullOr types.port;
+ default = null;
+ example = 993;
+ description = ''
+ The port on which the IMAP server listens. If
+ <literal>null</literal> then the default port is used.
+ '';
+ };
+
+ tls = mkOption {
+ type = tlsModule;
+ default = {};
+ description = ''
+ Configuration for secure connections.
+ '';
+ };
+ };
+ };
+
+ smtpModule = types.submodule {
+ options = {
+ host = mkOption {
+ type = types.str;
+ example = "smtp.example.org";
+ description = ''
+ Hostname of SMTP server.
+ '';
+ };
+
+ port = mkOption {
+ type = types.nullOr types.port;
+ default = null;
+ example = 465;
+ description = ''
+ The port on which the SMTP server listens. If
+ <literal>null</literal> then the default port is used.
+ '';
+ };
+
+ tls = mkOption {
+ type = tlsModule;
+ default = {};
+ description = ''
+ Configuration for secure connections.
+ '';
+ };
+ };
+ };
+
+ maildirModule = types.submodule ({ config, ... }: {
+ options = {
+ path = mkOption {
+ type = types.str;
+ description = ''
+ Path to maildir directory where mail for this account is
+ stored. This is relative to the base maildir path.
+ '';
+ };
+
+ absPath = mkOption {
+ type = types.path;
+ readOnly = true;
+ internal = true;
+ default = "${cfg.maildirBasePath}/${config.path}";
+ description = ''
+ A convenience option whose value is the absolute path of
+ this maildir.
+ '';
+ };
+ };
+ });
+
+ mailAccountOpts = { name, config, ... }: {
+ options = {
+ name = mkOption {
+ type = types.str;
+ readOnly = true;
+ description = ''
+ Unique identifier of the account. This is set to the
+ attribute name of the account configuration.
+ '';
+ };
+
+ primary = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Whether this is the primary account. Only one account may be
+ set as primary.
+ '';
+ };
+
+ flavor = mkOption {
+ type = types.enum [ "plain" "gmail.com" "runbox.com" ];
+ default = "plain";
+ description = ''
+ Some email providers have peculiar behavior that require
+ special treatment. This option is therefore intended to
+ indicate the nature of the provider.
+ </para><para>
+ When this indicates a specific provider then, for example,
+ the IMAP and SMTP server configuration may be set
+ automatically.
+ '';
+ };
+
+ address = mkOption {
+ type = types.strMatching ".*@.*";
+ example = "jane.doe@example.org";
+ description = "The email address of this account.";
+ };
+
+ aliases = mkOption {
+ type = types.listOf (types.strMatching ".*@.*");
+ default = [];
+ example = [ "webmaster@example.org" "admin@example.org" ];
+ description = "Alternative email addresses of this account.";
+ };
+
+ realName = mkOption {
+ type = types.str;
+ example = "Jane Doe";
+ description = "Name displayed when sending mails.";
+ };
+
+ userName = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = ''
+ The server username of this account. This will be used as
+ the SMTP and IMAP user name.
+ '';
+ };
+
+ passwordCommand = mkOption {
+ type = types.nullOr (types.either types.str (types.listOf types.str));
+ default = null;
+ apply = p: if isString p then splitString " " p else p;
+ example = "secret-tool lookup email me@example.org";
+ description = ''
+ A command, which when run writes the account password on
+ standard output.
+ '';
+ };
+
+ folders = mkOption {
+ type = types.submodule {
+ options = {
+ inbox = mkOption {
+ type = types.str;
+ default = "Inbox";
+ description = ''
+ Relative path of the inbox mail.
+ '';
+ };
+
+ sent = mkOption {
+ type = types.nullOr types.str;
+ default = "Sent";
+ description = ''
+ Relative path of the sent mail folder.
+ '';
+ };
+
+ drafts = mkOption {
+ type = types.str;
+ default = "Drafts";
+ description = ''
+ Relative path of the drafts mail folder.
+ '';
+ };
+
+ trash = mkOption {
+ type = types.str;
+ default = "Trash";
+ description = ''
+ Relative path of the deleted mail folder.
+ '';
+ };
+ };
+ };
+ default = {};
+ description = ''
+ Standard email folders.
+ '';
+ };
+
+ imap = mkOption {
+ type = types.nullOr imapModule;
+ default = null;
+ description = ''
+ The IMAP configuration to use for this account.
+ '';
+ };
+
+ signature = mkOption {
+ type = signatureModule;
+ default = {};
+ description = ''
+ Signature configuration.
+ '';
+ };
+
+ gpg = mkOption {
+ type = types.nullOr gpgModule;
+ default = null;
+ description = ''
+ GPG configuration.
+ '';
+ };
+
+ smtp = mkOption {
+ type = types.nullOr smtpModule;
+ default = null;
+ description = ''
+ The SMTP configuration to use for this account.
+ '';
+ };
+
+ maildir = mkOption {
+ type = types.nullOr maildirModule;
+ defaultText = { path = "\${name}"; };
+ description = ''
+ Maildir configuration for this account.
+ '';
+ };
+ };
+
+ config = mkMerge [
+ {
+ name = name;
+ maildir = mkOptionDefault { path = "${name}"; };
+ }
+
+ (mkIf (config.flavor == "gmail.com") {
+ userName = mkDefault config.address;
+
+ imap = {
+ host = "imap.gmail.com";
+ };
+
+ smtp = {
+ host = "smtp.gmail.com";
+ port = if config.smtp.tls.useStartTls then 587 else 465;
+ };
+ })
+
+ (mkIf (config.flavor == "runbox.com") {
+ imap = {
+ host = "mail.runbox.com";
+ };
+
+ smtp = {
+ host = "mail.runbox.com";
+ };
+ })
+ ];
+ };
+
+in
+
+{
+ options.accounts.email = {
+ certificatesFile = mkOption {
+ type = types.path;
+ default = "/etc/ssl/certs/ca-certificates.crt";
+ description = ''
+ Path to default file containing certificate authorities that
+ should be used to validate the connection authenticity. This
+ path may be overridden on a per-account basis.
+ '';
+ };
+
+ maildirBasePath = mkOption {
+ type = types.str;
+ default = "${config.home.homeDirectory}/Maildir";
+ defaultText = "$HOME/Maildir";
+ apply = p:
+ if hasPrefix "/" p
+ then p
+ else "${config.home.homeDirectory}/${p}";
+ description = ''
+ The base directory for account maildir directories. May be a
+ relative path, in which case it is relative the home
+ directory.
+ '';
+ };
+
+ accounts = mkOption {
+ type = types.attrsOf (types.submodule [
+ mailAccountOpts
+ (import ../programs/alot-accounts.nix pkgs)
+ (import ../programs/astroid-accounts.nix)
+ (import ../programs/getmail-accounts.nix)
+ (import ../programs/mbsync-accounts.nix)
+ (import ../programs/msmtp-accounts.nix)
+ (import ../programs/neomutt-accounts.nix)
+ (import ../programs/notmuch-accounts.nix)
+ (import ../programs/offlineimap-accounts.nix)
+ ]);
+ default = {};
+ description = "List of email accounts.";
+ };
+ };
+
+ config = mkIf (cfg.accounts != {}) {
+ assertions = [
+ (
+ let
+ primaries =
+ catAttrs "name"
+ (filter (a: a.primary)
+ (attrValues cfg.accounts));
+ in
+ {
+ assertion = length primaries == 1;
+ message =
+ "Must have exactly one primary mail account but found "
+ + toString (length primaries)
+ + optionalString (length primaries > 1)
+ (", namely " + concatStringsSep ", " primaries);
+ }
+ )
+ ];
+ };
+}
diff --git a/home-manager/modules/default.nix b/home-manager/modules/default.nix
new file mode 100644
index 00000000000..7f3494e4dea
--- /dev/null
+++ b/home-manager/modules/default.nix
@@ -0,0 +1,62 @@
+{ configuration
+, pkgs
+, lib ? pkgs.stdenv.lib
+
+ # Whether to check that each option has a matching declaration.
+, check ? true
+}:
+
+with lib;
+
+let
+
+ collectFailed = cfg:
+ map (x: x.message) (filter (x: !x.assertion) cfg.assertions);
+
+ showWarnings = res:
+ let
+ f = w: x: builtins.trace "warning: ${w}" x;
+ in
+ fold f res res.config.warnings;
+
+ extendedLib = import ./lib/stdlib-extended.nix pkgs.lib;
+
+ hmModules =
+ import ./modules.nix {
+ inherit check pkgs;
+ lib = extendedLib;
+ };
+
+ rawModule = extendedLib.evalModules {
+ modules = [ configuration ] ++ hmModules;
+ specialArgs = {
+ modulesPath = builtins.toString ./.;
+ };
+ };
+
+ module = showWarnings (
+ let
+ failed = collectFailed rawModule.config;
+ failedStr = concatStringsSep "\n" (map (x: "- ${x}") failed);
+ in
+ if failed == []
+ then rawModule
+ else throw "\nFailed assertions:\n${failedStr}"
+ );
+
+in
+
+{
+ inherit (module) options config;
+
+ activationPackage = module.config.home.activationPackage;
+
+ # For backwards compatibility. Please use activationPackage instead.
+ activation-script = module.config.home.activationPackage;
+
+ newsDisplay = rawModule.config.news.display;
+ newsEntries =
+ sort (a: b: a.time > b.time) (
+ filter (a: a.condition) rawModule.config.news.entries
+ );
+}
diff --git a/home-manager/modules/files.nix b/home-manager/modules/files.nix
new file mode 100644
index 00000000000..94b3aa565a8
--- /dev/null
+++ b/home-manager/modules/files.nix
@@ -0,0 +1,302 @@
+{ pkgs, config, lib, ... }:
+
+with lib;
+
+let
+
+ cfg = config.home.file;
+
+ homeDirectory = config.home.homeDirectory;
+
+ fileType = (import lib/file-type.nix {
+ inherit homeDirectory lib pkgs;
+ }).fileType;
+
+ sourceStorePath = file:
+ let
+ sourcePath = toString file.source;
+ sourceName = config.lib.strings.storeFileName (baseNameOf sourcePath);
+ in
+ if builtins.hasContext sourcePath
+ then file.source
+ else builtins.path { path = file.source; name = sourceName; };
+
+in
+
+{
+ options = {
+ home.file = mkOption {
+ description = "Attribute set of files to link into the user home.";
+ default = {};
+ type = fileType "<envar>HOME</envar>" homeDirectory;
+ };
+
+ home-files = mkOption {
+ type = types.package;
+ internal = true;
+ description = "Package to contain all home files";
+ };
+ };
+
+ config = {
+ # This verifies that the links we are about to create will not
+ # overwrite an existing file.
+ home.activation.checkLinkTargets = hm.dag.entryBefore ["writeBoundary"] (
+ let
+ check = pkgs.writeText "check" ''
+ . ${./lib-bash/color-echo.sh}
+
+ # A symbolic link whose target path matches this pattern will be
+ # considered part of a Home Manager generation.
+ homeFilePattern="$(readlink -e "${builtins.storeDir}")/*-home-manager-files/*"
+
+ newGenFiles="$1"
+ shift
+ for sourcePath in "$@" ; do
+ relativePath="''${sourcePath#$newGenFiles/}"
+ targetPath="$HOME/$relativePath"
+ if [[ -e "$targetPath" \
+ && ! "$(readlink "$targetPath")" == $homeFilePattern ]] ; then
+ if [[ ! -L "$targetPath" && -n "$HOME_MANAGER_BACKUP_EXT" ]] ; then
+ backup="$targetPath.$HOME_MANAGER_BACKUP_EXT"
+ if [[ -e "$backup" ]]; then
+ errorEcho "Existing file '$backup' would be clobbered by backing up '$targetPath'"
+ collision=1
+ else
+ warnEcho "Existing file '$targetPath' is in the way, will be moved to '$backup'"
+ fi
+ else
+ errorEcho "Existing file '$targetPath' is in the way"
+ collision=1
+ fi
+ fi
+ done
+
+ if [[ -v collision ]] ; then
+ errorEcho "Please move the above files and try again or use -b <ext> to move automatically."
+ exit 1
+ fi
+ '';
+ in
+ ''
+ function checkNewGenCollision() {
+ local newGenFiles
+ newGenFiles="$(readlink -e "$newGenPath/home-files")"
+ find "$newGenFiles" \( -type f -or -type l \) \
+ -exec bash ${check} "$newGenFiles" {} +
+ }
+
+ checkNewGenCollision || exit 1
+ ''
+ );
+
+ # This activation script will
+ #
+ # 1. Remove files from the old generation that are not in the new
+ # generation.
+ #
+ # 2. Switch over the Home Manager gcroot and current profile
+ # links.
+ #
+ # 3. Symlink files from the new generation into $HOME.
+ #
+ # This order is needed to ensure that we always know which links
+ # belong to which generation. Specifically, if we're moving from
+ # generation A to generation B having sets of home file links FA
+ # and FB, respectively then cleaning before linking produces state
+ # transitions similar to
+ #
+ # FA → FA ∩ FB → (FA ∩ FB) ∪ FB = FB
+ #
+ # and a failure during the intermediate state FA ∩ FB will not
+ # result in lost links because this set of links are in both the
+ # source and target generation.
+ home.activation.linkGeneration = hm.dag.entryAfter ["writeBoundary"] (
+ let
+ link = pkgs.writeText "link" ''
+ newGenFiles="$1"
+ shift
+ for sourcePath in "$@" ; do
+ relativePath="''${sourcePath#$newGenFiles/}"
+ targetPath="$HOME/$relativePath"
+ if [[ -e "$targetPath" && ! -L "$targetPath" && -n "$HOME_MANAGER_BACKUP_EXT" ]] ; then
+ backup="$targetPath.$HOME_MANAGER_BACKUP_EXT"
+ $DRY_RUN_CMD mv $VERBOSE_ARG "$targetPath" "$backup" || errorEcho "Moving '$targetPath' failed!"
+ fi
+ $DRY_RUN_CMD mkdir -p $VERBOSE_ARG "$(dirname "$targetPath")"
+ $DRY_RUN_CMD ln -nsf $VERBOSE_ARG "$sourcePath" "$targetPath"
+ done
+ '';
+
+ cleanup = pkgs.writeText "cleanup" ''
+ . ${./lib-bash/color-echo.sh}
+
+ # A symbolic link whose target path matches this pattern will be
+ # considered part of a Home Manager generation.
+ homeFilePattern="$(readlink -e "${builtins.storeDir}")/*-home-manager-files/*"
+
+ newGenFiles="$1"
+ shift 1
+ for relativePath in "$@" ; do
+ targetPath="$HOME/$relativePath"
+ if [[ -e "$newGenFiles/$relativePath" ]] ; then
+ $VERBOSE_ECHO "Checking $targetPath: exists"
+ elif [[ ! "$(readlink "$targetPath")" == $homeFilePattern ]] ; then
+ warnEcho "Path '$targetPath' not link into Home Manager generation. Skipping delete."
+ else
+ $VERBOSE_ECHO "Checking $targetPath: gone (deleting)"
+ $DRY_RUN_CMD rm $VERBOSE_ARG "$targetPath"
+
+ # Recursively delete empty parent directories.
+ targetDir="$(dirname "$relativePath")"
+ if [[ "$targetDir" != "." ]] ; then
+ pushd "$HOME" > /dev/null
+
+ # Call rmdir with a relative path excluding $HOME.
+ # Otherwise, it might try to delete $HOME and exit
+ # with a permission error.
+ $DRY_RUN_CMD rmdir $VERBOSE_ARG \
+ -p --ignore-fail-on-non-empty \
+ "$targetDir"
+
+ popd > /dev/null
+ fi
+ fi
+ done
+ '';
+ in
+ ''
+ function linkNewGen() {
+ echo "Creating home file links in $HOME"
+
+ local newGenFiles
+ newGenFiles="$(readlink -e "$newGenPath/home-files")"
+ find "$newGenFiles" \( -type f -or -type l \) \
+ -exec bash ${link} "$newGenFiles" {} +
+ }
+
+ function cleanOldGen() {
+ if [[ ! -v oldGenPath ]] ; then
+ return
+ fi
+
+ echo "Cleaning up orphan links from $HOME"
+
+ local newGenFiles oldGenFiles
+ newGenFiles="$(readlink -e "$newGenPath/home-files")"
+ oldGenFiles="$(readlink -e "$oldGenPath/home-files")"
+
+ # Apply the cleanup script on each leaf in the old
+ # generation. The find command below will print the
+ # relative path of the entry.
+ find "$oldGenFiles" '(' -type f -or -type l ')' -printf '%P\0' \
+ | xargs -0 bash ${cleanup} "$newGenFiles"
+ }
+
+ cleanOldGen
+
+ if [[ ! -v oldGenPath || "$oldGenPath" != "$newGenPath" ]] ; then
+ echo "Creating profile generation $newGenNum"
+ $DRY_RUN_CMD ln -Tsf $VERBOSE_ARG "$newGenPath" "$newGenProfilePath"
+ $DRY_RUN_CMD ln -Tsf $VERBOSE_ARG $(basename "$newGenProfilePath") "$genProfilePath"
+ $DRY_RUN_CMD ln -Tsf $VERBOSE_ARG "$newGenPath" "$newGenGcPath"
+ else
+ echo "No change so reusing latest profile generation $oldGenNum"
+ fi
+
+ linkNewGen
+ ''
+ );
+
+ home.activation.checkFilesChanged = hm.dag.entryBefore ["linkGeneration"] (
+ ''
+ declare -A changedFiles
+ '' + concatMapStrings (v: ''
+ cmp --quiet "${sourceStorePath v}" "${homeDirectory}/${v.target}" \
+ && changedFiles["${v.target}"]=0 \
+ || changedFiles["${v.target}"]=1
+ '') (filter (v: v.onChange != "") (attrValues cfg))
+ );
+
+ home.activation.onFilesChange = hm.dag.entryAfter ["linkGeneration"] (
+ concatMapStrings (v: ''
+ if [[ ${"$\{changedFiles"}["${v.target}"]} -eq 1 ]]; then
+ ${v.onChange}
+ fi
+ '') (filter (v: v.onChange != "") (attrValues cfg))
+ );
+
+ # Symlink directories and files that have the right execute bit.
+ # Copy files that need their execute bit changed.
+ home-files = pkgs.runCommand
+ "home-manager-files"
+ {
+ nativeBuildInputs = [ pkgs.xlibs.lndir ];
+ preferLocalBuild = true;
+ allowSubstitutes = false;
+ }
+ (''
+ mkdir -p $out
+
+ # Needed in case /nix is a symbolic link.
+ realOut="$(realpath -m "$out")"
+
+ function insertFile() {
+ local source="$1"
+ local relTarget="$2"
+ local executable="$3"
+ local recursive="$4"
+
+ # Figure out the real absolute path to the target.
+ local target
+ target="$(realpath -m "$realOut/$relTarget")"
+
+ # Target path must be within $HOME.
+ if [[ ! $target == $realOut* ]] ; then
+ echo "Error installing file '$relTarget' outside \$HOME" >&2
+ exit 1
+ fi
+
+ mkdir -p "$(dirname "$target")"
+ if [[ -d $source ]]; then
+ if [[ $recursive ]]; then
+ mkdir -p "$target"
+ lndir -silent "$source" "$target"
+ else
+ ln -s "$source" "$target"
+ fi
+ else
+ [[ -x $source ]] && isExecutable=1 || isExecutable=""
+
+ # Link the file into the home file directory if possible,
+ # i.e., if the executable bit of the source is the same we
+ # expect for the target. Otherwise, we copy the file and
+ # set the executable bit to the expected value.
+ if [[ $executable == inherit || $isExecutable == $executable ]]; then
+ ln -s "$source" "$target"
+ else
+ cp "$source" "$target"
+
+ if [[ $executable == inherit ]]; then
+ # Don't change file mode if it should match the source.
+ :
+ elif [[ $executable ]]; then
+ chmod +x "$target"
+ else
+ chmod -x "$target"
+ fi
+ fi
+ fi
+ }
+ '' + concatStrings (
+ mapAttrsToList (n: v: ''
+ insertFile "${sourceStorePath v}" \
+ "${v.target}" \
+ "${if v.executable == null
+ then "inherit"
+ else builtins.toString v.executable}" \
+ "${builtins.toString v.recursive}"
+ '') cfg
+ ));
+ };
+}
diff --git a/home-manager/modules/home-environment.nix b/home-manager/modules/home-environment.nix
new file mode 100644
index 00000000000..0b2fb8f4ef9
--- /dev/null
+++ b/home-manager/modules/home-environment.nix
@@ -0,0 +1,470 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.home;
+
+ languageSubModule = types.submodule {
+ options = {
+ base = mkOption {
+ default = null;
+ type = types.nullOr types.str;
+ description = ''
+ The language to use unless overridden by a more specific option.
+ '';
+ };
+
+ address = mkOption {
+ default = null;
+ type = types.nullOr types.str;
+ description = ''
+ The language to use for addresses.
+ '';
+ };
+
+ monetary = mkOption {
+ default = null;
+ type = types.nullOr types.str;
+ description = ''
+ The language to use for formatting currencies and money amounts.
+ '';
+ };
+
+ paper = mkOption {
+ default = null;
+ type = types.nullOr types.str;
+ description = ''
+ The language to use for paper sizes.
+ '';
+ };
+
+ time = mkOption {
+ default = null;
+ type = types.nullOr types.str;
+ description = ''
+ The language to use for formatting times.
+ '';
+ };
+ };
+ };
+
+ keyboardSubModule = types.submodule {
+ options = {
+ layout = mkOption {
+ type = with types; nullOr str;
+ default =
+ if versionAtLeast config.home.stateVersion "19.09"
+ then null
+ else "us";
+ defaultText = literalExample "null";
+ description = ''
+ Keyboard layout. If <literal>null</literal>, then the system
+ configuration will be used.
+ </para><para>
+ This defaults to <literal>null</literal> for state
+ version ≥ 19.09 and <literal>"us"</literal> otherwise.
+ '';
+ };
+
+ model = mkOption {
+ type = with types; nullOr str;
+ default = null;
+ example = "presario";
+ description = ''
+ Keyboard model.
+ '';
+ };
+
+ options = mkOption {
+ type = types.listOf types.str;
+ default = [];
+ example = ["grp:caps_toggle" "grp_led:scroll"];
+ description = ''
+ X keyboard options; layout switching goes here.
+ '';
+ };
+
+ variant = mkOption {
+ type = with types; nullOr str;
+ default =
+ if versionAtLeast config.home.stateVersion "19.09"
+ then null
+ else "";
+ defaultText = literalExample "null";
+ example = "colemak";
+ description = ''
+ X keyboard variant. If <literal>null</literal>, then the
+ system configuration will be used.
+ </para><para>
+ This defaults to <literal>null</literal> for state
+ version ≥ 19.09 and <literal>""</literal> otherwise.
+ '';
+ };
+ };
+ };
+
+in
+
+{
+ meta.maintainers = [ maintainers.rycee ];
+
+ imports = [
+ (mkRemovedOptionModule [ "home" "sessionVariableSetter" ] ''
+ Session variables are now always set through the shell. This is
+ done automatically if the shell configuration is managed by Home
+ Manager. If not, then you must source the
+
+ ~/.nix-profile/etc/profile.d/hm-session-vars.sh
+
+ file yourself.
+ '')
+ ];
+
+ options = {
+ home.username = mkOption {
+ type = types.str;
+ defaultText = "$USER";
+ description = "The user's username.";
+ };
+
+ home.homeDirectory = mkOption {
+ type = types.path;
+ defaultText = "$HOME";
+ description = "The user's home directory.";
+ };
+
+ home.profileDirectory = mkOption {
+ type = types.path;
+ defaultText = "~/.nix-profile";
+ internal = true;
+ readOnly = true;
+ description = ''
+ The profile directory where Home Manager generations are
+ installed.
+ '';
+ };
+
+ home.language = mkOption {
+ type = languageSubModule;
+ default = {};
+ description = "Language configuration.";
+ };
+
+ home.keyboard = mkOption {
+ type = types.nullOr keyboardSubModule;
+ default = {};
+ description = ''
+ Keyboard configuration. Set to <literal>null</literal> to
+ disable Home Manager keyboard management.
+ '';
+ };
+
+ home.sessionVariables = mkOption {
+ default = {};
+ type = types.attrs;
+ example = { EDITOR = "emacs"; GS_OPTIONS = "-sPAPERSIZE=a4"; };
+ description = ''
+ Environment variables to always set at login.
+ </para><para>
+ The values may refer to other environment variables using
+ POSIX.2 style variable references. For example, a variable
+ <varname>parameter</varname> may be referenced as
+ <code>$parameter</code> or <code>''${parameter}</code>. A
+ default value <literal>foo</literal> may be given as per
+ <code>''${parameter:-foo}</code> and, similarly, an alternate
+ value <literal>bar</literal> can be given as per
+ <code>''${parameter:+bar}</code>.
+ </para><para>
+ Note, these variables may be set in any order so no session
+ variable may have a runtime dependency on another session
+ variable. In particular code like
+ <programlisting language="nix">
+ home.sessionVariables = {
+ FOO = "Hello";
+ BAR = "$FOO World!";
+ };
+ </programlisting>
+ may not work as expected. If you need to reference another
+ session variable, then do so inside Nix instead. The above
+ example then becomes
+ <programlisting language="nix">
+ home.sessionVariables = {
+ FOO = "Hello";
+ BAR = "''${config.home.sessionVariables.FOO} World!";
+ };
+ </programlisting>
+ '';
+ };
+
+ home.packages = mkOption {
+ type = types.listOf types.package;
+ default = [];
+ description = "The set of packages to appear in the user environment.";
+ };
+
+ home.extraOutputsToInstall = mkOption {
+ type = types.listOf types.str;
+ default = [];
+ example = [ "doc" "info" "devdoc" ];
+ description = ''
+ List of additional package outputs of the packages
+ <varname>home.packages</varname> that should be installed into
+ the user environment.
+ '';
+ };
+
+ home.path = mkOption {
+ internal = true;
+ description = "The derivation installing the user packages.";
+ };
+
+ home.emptyActivationPath = mkOption {
+ internal = true;
+ default = false;
+ type = types.bool;
+ description = ''
+ Whether the activation script should start with an empty
+ <envvar>PATH</envvar> variable. When <literal>false</literal>
+ then the user's <envvar>PATH</envvar> will be used.
+ '';
+ };
+
+ home.activation = mkOption {
+ type = hm.types.dagOf types.str;
+ default = {};
+ example = literalExample ''
+ {
+ myActivationAction = lib.hm.dag.entryAfter ["writeBoundary"] '''
+ $DRY_RUN_CMD ln -s $VERBOSE_ARG \
+ ''${builtins.toPath ./link-me-directly} $HOME
+ ''';
+ }
+ '';
+ description = ''
+ The activation scripts blocks to run when activating a Home
+ Manager generation. Any entry here should be idempotent,
+ meaning running twice or more times produces the same result
+ as running it once.
+
+ </para><para>
+
+ If the script block produces any observable side effect, such
+ as writing or deleting files, then it
+ <emphasis>must</emphasis> be placed after the special
+ <literal>writeBoundary</literal> script block. Prior to the
+ write boundary one can place script blocks that verifies, but
+ does not modify, the state of the system and exits if an
+ unexpected state is found. For example, the
+ <literal>checkLinkTargets</literal> script block checks for
+ collisions between non-managed files and files defined in
+ <varname><link linkend="opt-home.file">home.file</link></varname>.
+
+ </para><para>
+
+ A script block should respect the <varname>DRY_RUN</varname>
+ variable, if it is set then the actions taken by the script
+ should be logged to standard out and not actually performed.
+ The variable <varname>DRY_RUN_CMD</varname> is set to
+ <command>echo</command> if dry run is enabled.
+
+ </para><para>
+
+ A script block should also respect the
+ <varname>VERBOSE</varname> variable, and if set print
+ information on standard out that may be useful for debugging
+ any issue that may arise. The variable
+ <varname>VERBOSE_ARG</varname> is set to
+ <option>--verbose</option> if verbose output is enabled.
+ '';
+ };
+
+ home.activationPackage = mkOption {
+ internal = true;
+ type = types.package;
+ description = "The package containing the complete activation script.";
+ };
+
+ home.extraBuilderCommands = mkOption {
+ type = types.lines;
+ default = "";
+ internal = true;
+ description = ''
+ Extra commands to run in the Home Manager generation builder.
+ '';
+ };
+
+ home.extraProfileCommands = mkOption {
+ type = types.lines;
+ default = "";
+ internal = true;
+ description = ''
+ Extra commands to run in the Home Manager profile builder.
+ '';
+ };
+ };
+
+ config = {
+ assertions = [
+ {
+ assertion = config.home.username != "";
+ message = "Username could not be determined";
+ }
+ {
+ assertion = config.home.homeDirectory != "";
+ message = "Home directory could not be determined";
+ }
+ ];
+
+ home.username = mkDefault (builtins.getEnv "USER");
+ home.homeDirectory = mkDefault (builtins.getEnv "HOME");
+
+ home.profileDirectory =
+ if config.submoduleSupport.enable
+ && config.submoduleSupport.externalPackageInstall
+ then config.home.path
+ else cfg.homeDirectory + "/.nix-profile";
+
+ home.sessionVariables =
+ let
+ maybeSet = n: v: optionalAttrs (v != null) { ${n} = v; };
+ in
+ (maybeSet "LANG" cfg.language.base)
+ //
+ (maybeSet "LC_ADDRESS" cfg.language.address)
+ //
+ (maybeSet "LC_MONETARY" cfg.language.monetary)
+ //
+ (maybeSet "LC_PAPER" cfg.language.paper)
+ //
+ (maybeSet "LC_TIME" cfg.language.time);
+
+ home.packages = [
+ # Provide a file holding all session variables.
+ (
+ pkgs.writeTextFile {
+ name = "hm-session-vars.sh";
+ destination = "/etc/profile.d/hm-session-vars.sh";
+ text = ''
+ # Only source this once.
+ if [ -n "$__HM_SESS_VARS_SOURCED" ]; then return; fi
+ export __HM_SESS_VARS_SOURCED=1
+
+ ${config.lib.shell.exportAll cfg.sessionVariables}
+ '';
+ }
+ )
+ ];
+
+ # A dummy entry acting as a boundary between the activation
+ # script's "check" and the "write" phases.
+ home.activation.writeBoundary = hm.dag.entryAnywhere "";
+
+ # Install packages to the user environment.
+ #
+ # Note, sometimes our target may not allow modification of the Nix
+ # store and then we cannot rely on `nix-env -i`. This is the case,
+ # for example, if we are running as a NixOS module and building a
+ # virtual machine. Then we must instead rely on an external
+ # mechanism for installing packages, which in NixOS is provided by
+ # the `users.users.<name?>.packages` option. The activation
+ # command is still needed since some modules need to run their
+ # activation commands after the packages are guaranteed to be
+ # installed.
+ #
+ # In case the user has moved from a user-install of Home Manager
+ # to a submodule managed one we attempt to uninstall the
+ # `home-manager-path` package if it is installed.
+ home.activation.installPackages = hm.dag.entryAfter ["writeBoundary"] (
+ if config.submoduleSupport.externalPackageInstall
+ then
+ ''
+ if nix-env -q | grep '^home-manager-path$'; then
+ $DRY_RUN_CMD nix-env -e home-manager-path
+ fi
+ ''
+ else
+ ''
+ $DRY_RUN_CMD nix-env -i ${cfg.path}
+ ''
+ );
+
+ home.activationPackage =
+ let
+ mkCmd = res: ''
+ noteEcho Activating ${res.name}
+ ${res.data}
+ '';
+ sortedCommands = hm.dag.topoSort cfg.activation;
+ activationCmds =
+ if sortedCommands ? result then
+ concatStringsSep "\n" (map mkCmd sortedCommands.result)
+ else
+ abort ("Dependency cycle in activation script: "
+ + builtins.toJSON sortedCommands);
+
+ # Programs that always should be available on the activation
+ # script's PATH.
+ activationBinPaths = lib.makeBinPath [
+ pkgs.bash
+ pkgs.coreutils
+ pkgs.diffutils # For `cmp` and `diff`.
+ pkgs.findutils
+ pkgs.gnugrep
+ pkgs.gnused
+ pkgs.ncurses # For `tput`.
+ ]
+ + optionalString (!cfg.emptyActivationPath) "\${PATH:+:}$PATH";
+
+ activationScript = pkgs.writeScript "activation-script" ''
+ #!${pkgs.runtimeShell}
+
+ set -eu
+ set -o pipefail
+
+ cd $HOME
+
+ export PATH="${activationBinPaths}"
+
+ . ${./lib-bash/color-echo.sh}
+
+ ${builtins.readFile ./lib-bash/activation-init.sh}
+
+ ${activationCmds}
+ '';
+ in
+ pkgs.runCommand
+ "home-manager-generation"
+ {
+ preferLocalBuild = true;
+ allowSubstitutes = false;
+ }
+ ''
+ mkdir -p $out
+
+ cp ${activationScript} $out/activate
+
+ substituteInPlace $out/activate \
+ --subst-var-by GENERATION_DIR $out
+
+ ln -s ${config.home-files} $out/home-files
+ ln -s ${cfg.path} $out/home-path
+
+ ${cfg.extraBuilderCommands}
+ '';
+
+ home.path = pkgs.buildEnv {
+ name = "home-manager-path";
+
+ paths = cfg.packages;
+ inherit (cfg) extraOutputsToInstall;
+
+ postBuild = cfg.extraProfileCommands;
+
+ meta = {
+ description = "Environment of packages installed through home-manager";
+ };
+ };
+ };
+}
diff --git a/home-manager/modules/lib-bash/activation-init.sh b/home-manager/modules/lib-bash/activation-init.sh
new file mode 100755
index 00000000000..5cdb66e5920
--- /dev/null
+++ b/home-manager/modules/lib-bash/activation-init.sh
@@ -0,0 +1,83 @@
+#!/usr/bin/env bash
+
+function setupVars() {
+ local profilesPath="/nix/var/nix/profiles/per-user/$USER"
+ local gcPath="/nix/var/nix/gcroots/per-user/$USER"
+ local greatestGenNum
+
+ greatestGenNum=$( \
+ find "$profilesPath" -name 'home-manager-*-link' \
+ | sed 's/^.*-\([0-9]*\)-link$/\1/' \
+ | sort -rn \
+ | head -1)
+
+ if [[ -n $greatestGenNum ]] ; then
+ oldGenNum=$greatestGenNum
+ newGenNum=$((oldGenNum + 1))
+ else
+ newGenNum=1
+ fi
+
+ if [[ -e $gcPath/current-home ]] ; then
+ oldGenPath="$(readlink -e "$gcPath/current-home")"
+ fi
+
+ $VERBOSE_ECHO "Sanity checking oldGenNum and oldGenPath"
+ if [[ -v oldGenNum && ! -v oldGenPath
+ || ! -v oldGenNum && -v oldGenPath ]]; then
+ errorEcho "Invalid profile number and GC root values! These must be"
+ errorEcho "either both empty or both set but are now set to"
+ errorEcho " '${oldGenNum:-}' and '${oldGenPath:-}'"
+ errorEcho "If you don't mind losing previous profile generations then"
+ errorEcho "the easiest solution is probably to run"
+ errorEcho " rm $profilesPath/home-manager*"
+ errorEcho " rm $gcPath/current-home"
+ errorEcho "and trying home-manager switch again. Good luck!"
+ exit 1
+ fi
+
+
+ genProfilePath="$profilesPath/home-manager"
+ newGenPath="@GENERATION_DIR@";
+ newGenProfilePath="$profilesPath/home-manager-$newGenNum-link"
+ newGenGcPath="$gcPath/current-home"
+}
+
+if [[ -v VERBOSE ]]; then
+ export VERBOSE_ECHO=echo
+ export VERBOSE_ARG="--verbose"
+else
+ export VERBOSE_ECHO=true
+ export VERBOSE_ARG=""
+fi
+
+echo "Starting home manager activation"
+
+setupVars
+
+if [[ -v DRY_RUN ]] ; then
+ echo "This is a dry run"
+ export DRY_RUN_CMD=echo
+else
+ $VERBOSE_ECHO "This is a live run"
+ export DRY_RUN_CMD=""
+fi
+
+if [[ -v VERBOSE ]]; then
+ echo -n "Using Nix version: "
+ nix-env --version
+fi
+
+$VERBOSE_ECHO "Activation variables:"
+if [[ -v oldGenNum ]] ; then
+ $VERBOSE_ECHO " oldGenNum=$oldGenNum"
+ $VERBOSE_ECHO " oldGenPath=$oldGenPath"
+else
+ $VERBOSE_ECHO " oldGenNum undefined (first run?)"
+ $VERBOSE_ECHO " oldGenPath undefined (first run?)"
+fi
+$VERBOSE_ECHO " newGenPath=$newGenPath"
+$VERBOSE_ECHO " newGenNum=$newGenNum"
+$VERBOSE_ECHO " newGenProfilePath=$newGenProfilePath"
+$VERBOSE_ECHO " newGenGcPath=$newGenGcPath"
+$VERBOSE_ECHO " genProfilePath=$genProfilePath"
diff --git a/home-manager/modules/lib-bash/color-echo.sh b/home-manager/modules/lib-bash/color-echo.sh
new file mode 100644
index 00000000000..ef708b29c4d
--- /dev/null
+++ b/home-manager/modules/lib-bash/color-echo.sh
@@ -0,0 +1,37 @@
+# The check for terminal output and color support is heavily inspired
+# by https://unix.stackexchange.com/a/10065.
+
+function setupColors() {
+ normalColor=""
+ errorColor=""
+ warnColor=""
+ noteColor=""
+
+ # Check if stdout is a terminal.
+ if [[ -t 1 ]]; then
+ # See if it supports colors.
+ local ncolors
+ ncolors=$(tput colors)
+
+ if [[ -n "$ncolors" && "$ncolors" -ge 8 ]]; then
+ normalColor="$(tput sgr0)"
+ errorColor="$(tput bold)$(tput setaf 1)"
+ warnColor="$(tput setaf 3)"
+ noteColor="$(tput bold)$(tput setaf 6)"
+ fi
+ fi
+}
+
+setupColors
+
+function errorEcho() {
+ echo "${errorColor}$*${normalColor}"
+}
+
+function warnEcho() {
+ echo "${warnColor}$*${normalColor}"
+}
+
+function noteEcho() {
+ echo "${noteColor}$*${normalColor}"
+}
diff --git a/home-manager/modules/lib/dag.nix b/home-manager/modules/lib/dag.nix
new file mode 100644
index 00000000000..cbe34129652
--- /dev/null
+++ b/home-manager/modules/lib/dag.nix
@@ -0,0 +1,117 @@
+# A generalization of Nixpkgs's `strings-with-deps.nix`.
+#
+# The main differences from the Nixpkgs version are
+#
+# - not specific to strings, i.e., any payload is OK,
+#
+# - the addition of the function `dagEntryBefore` indicating a
+# "wanted by" relationship.
+
+{ lib }:
+
+with lib;
+
+rec {
+
+ emptyDag = { };
+
+ isDag = dag:
+ let isEntry = e: (e ? data) && (e ? after) && (e ? before);
+ in builtins.isAttrs dag && all (x: x) (mapAttrsToList (n: isEntry) dag);
+
+ # Takes an attribute set containing entries built by
+ # dagEntryAnywhere, dagEntryAfter, and dagEntryBefore to a
+ # topologically sorted list of entries.
+ #
+ # Internally this function uses the `toposort` function in
+ # `<nixpkgs/lib/lists.nix>` and its value is accordingly.
+ #
+ # Specifically, the result on success is
+ #
+ # { result = [{name = ?; data = ?;} …] }
+ #
+ # For example
+ #
+ # nix-repl> dagTopoSort {
+ # a = dagEntryAnywhere "1";
+ # b = dagEntryAfter ["a" "c"] "2";
+ # c = dagEntryBefore ["d"] "3";
+ # d = dagEntryBefore ["e"] "4";
+ # e = dagEntryAnywhere "5";
+ # } == {
+ # result = [
+ # { data = "1"; name = "a"; }
+ # { data = "3"; name = "c"; }
+ # { data = "2"; name = "b"; }
+ # { data = "4"; name = "d"; }
+ # { data = "5"; name = "e"; }
+ # ];
+ # }
+ # true
+ #
+ # And the result on error is
+ #
+ # {
+ # cycle = [ {after = ?; name = ?; data = ?} … ];
+ # loops = [ {after = ?; name = ?; data = ?} … ];
+ # }
+ #
+ # For example
+ #
+ # nix-repl> dagTopoSort {
+ # a = dagEntryAnywhere "1";
+ # b = dagEntryAfter ["a" "c"] "2";
+ # c = dagEntryAfter ["d"] "3";
+ # d = dagEntryAfter ["b"] "4";
+ # e = dagEntryAnywhere "5";
+ # } == {
+ # cycle = [
+ # { after = ["a" "c"]; data = "2"; name = "b"; }
+ # { after = ["d"]; data = "3"; name = "c"; }
+ # { after = ["b"]; data = "4"; name = "d"; }
+ # ];
+ # loops = [
+ # { after = ["a" "c"]; data = "2"; name = "b"; }
+ # ];
+ # } == {}
+ # true
+ dagTopoSort = dag:
+ let
+ dagBefore = dag: name:
+ mapAttrsToList (n: v: n)
+ (filterAttrs (n: v: any (a: a == name) v.before) dag);
+ normalizedDag = mapAttrs (n: v: {
+ name = n;
+ data = v.data;
+ after = v.after ++ dagBefore dag n;
+ }) dag;
+ before = a: b: any (c: a.name == c) b.after;
+ sorted = toposort before (mapAttrsToList (n: v: v) normalizedDag);
+ in if sorted ? result then {
+ result = map (v: { inherit (v) name data; }) sorted.result;
+ } else
+ sorted;
+
+ # Applies a function to each element of the given DAG.
+ dagMap = f: dag: mapAttrs (n: v: v // { data = f n v.data; }) dag;
+
+ # Create a DAG entry with no particular dependency information.
+ dagEntryAnywhere = data: {
+ inherit data;
+ before = [ ];
+ after = [ ];
+ };
+
+ dagEntryBetween = before: after: data: { inherit data before after; };
+
+ dagEntryAfter = after: data: {
+ inherit data after;
+ before = [ ];
+ };
+
+ dagEntryBefore = before: data: {
+ inherit data before;
+ after = [ ];
+ };
+
+}
diff --git a/home-manager/modules/lib/default.nix b/home-manager/modules/lib/default.nix
new file mode 100644
index 00000000000..6b2bbacc249
--- /dev/null
+++ b/home-manager/modules/lib/default.nix
@@ -0,0 +1,24 @@
+{ lib }:
+
+rec {
+ dag =
+ let
+ d = import ./dag.nix { inherit lib; };
+ in
+ {
+ empty = d.emptyDag;
+ isDag = d.isDag;
+ topoSort = d.dagTopoSort;
+ map = d.dagMap;
+ entryAnywhere = d.dagEntryAnywhere;
+ entryBetween = d.dagEntryBetween;
+ entryAfter = d.dagEntryAfter;
+ entryBefore = d.dagEntryBefore;
+ };
+
+ strings = import ./strings.nix { inherit lib; };
+ types = import ./types.nix { inherit dag lib; };
+
+ shell = import ./shell.nix { inherit lib; };
+ zsh = import ./zsh.nix { inherit lib; };
+}
diff --git a/home-manager/modules/lib/file-type.nix b/home-manager/modules/lib/file-type.nix
new file mode 100644
index 00000000000..3096a6d37bb
--- /dev/null
+++ b/home-manager/modules/lib/file-type.nix
@@ -0,0 +1,96 @@
+{ homeDirectory, lib, pkgs }:
+
+with lib;
+
+{
+ # Constructs a type suitable for a `home.file` like option. The
+ # target path may be either absolute or relative, in which case it
+ # is relative the `basePath` argument (which itself must be an
+ # absolute path).
+ #
+ # Arguments:
+ # - basePathDesc docbook compatible description of the base path
+ # - basePath the file base path
+ fileType = basePathDesc: basePath: types.loaOf (types.submodule (
+ { name, config, ... }: {
+ options = {
+ target = mkOption {
+ type = types.str;
+ apply = p:
+ let
+ absPath = if hasPrefix "/" p then p else "${basePath}/${p}";
+ in
+ removePrefix (homeDirectory + "/") absPath;
+ description = ''
+ Path to target file relative to ${basePathDesc}.
+ '';
+ };
+
+ text = mkOption {
+ default = null;
+ type = types.nullOr types.lines;
+ description = "Text of the file.";
+ };
+
+ source = mkOption {
+ type = types.path;
+ description = ''
+ Path of the source file. The file name must not start
+ with a period since Nix will not allow such names in
+ the Nix store.
+ </para><para>
+ This may refer to a directory.
+ '';
+ };
+
+ executable = mkOption {
+ type = types.nullOr types.bool;
+ default = null;
+ description = ''
+ Set the execute bit. If <literal>null</literal>, defaults to the mode
+ of the <varname>source</varname> file or to <literal>false</literal>
+ for files created through the <varname>text</varname> option.
+ '';
+ };
+
+ recursive = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ If the file source is a directory, then this option
+ determines whether the directory should be recursively
+ linked to the target location. This option has no effect
+ if the source is a file.
+ </para><para>
+ If <literal>false</literal> (the default) then the target
+ will be a symbolic link to the source directory. If
+ <literal>true</literal> then the target will be a
+ directory structure matching the source's but whose leafs
+ are symbolic links to the files of the source directory.
+ '';
+ };
+
+ onChange = mkOption {
+ type = types.lines;
+ default = "";
+ description = ''
+ Shell commands to run when file has changed between
+ generations. The script will be run
+ <emphasis>after</emphasis> the new files have been linked
+ into place.
+ '';
+ };
+ };
+
+ config = {
+ target = mkDefault name;
+ source = mkIf (config.text != null) (
+ mkDefault (pkgs.writeTextFile {
+ inherit (config) executable text;
+ name = hm.strings.storeFileName name;
+ })
+ );
+ };
+ }
+ ));
+}
diff --git a/home-manager/modules/lib/shell.nix b/home-manager/modules/lib/shell.nix
new file mode 100644
index 00000000000..5e5743f51ea
--- /dev/null
+++ b/home-manager/modules/lib/shell.nix
@@ -0,0 +1,11 @@
+{ lib }:
+
+rec {
+ # Produces a Bourne shell like variable export statement.
+ export = n: v: ''export ${n}="${toString v}"'';
+
+ # Given an attribute set containing shell variable names and their
+ # assignment, this function produces a string containing an export
+ # statement for each set entry.
+ exportAll = vars: lib.concatStringsSep "\n" (lib.mapAttrsToList export vars);
+}
diff --git a/home-manager/modules/lib/stdlib-extended.nix b/home-manager/modules/lib/stdlib-extended.nix
new file mode 100644
index 00000000000..93f2397cee8
--- /dev/null
+++ b/home-manager/modules/lib/stdlib-extended.nix
@@ -0,0 +1,7 @@
+# Just a convenience function that returns the given Nixpkgs standard
+# library extended with the HM library.
+
+nixpkgsLib:
+
+let mkHmLib = import ./.;
+in nixpkgsLib.extend (self: super: { hm = mkHmLib { lib = super; }; })
diff --git a/home-manager/modules/lib/strings.nix b/home-manager/modules/lib/strings.nix
new file mode 100644
index 00000000000..fe7b2fa3061
--- /dev/null
+++ b/home-manager/modules/lib/strings.nix
@@ -0,0 +1,22 @@
+{ lib }:
+
+with lib;
+
+{
+ # Figures out a valid Nix store name for the given path.
+ storeFileName = path:
+ let
+ # All characters that are considered safe. Note "-" is not
+ # included to avoid "-" followed by digit being interpreted as a
+ # version.
+ safeChars = [ "+" "." "_" "?" "=" ] ++ lowerChars ++ upperChars
+ ++ stringToCharacters "0123456789";
+
+ empties = l: genList (x: "") (length l);
+
+ unsafeInName =
+ stringToCharacters (replaceStrings safeChars (empties safeChars) path);
+
+ safeName = replaceStrings unsafeInName (empties unsafeInName) path;
+ in "hm_" + safeName;
+}
diff --git a/home-manager/modules/lib/types-dag.nix b/home-manager/modules/lib/types-dag.nix
new file mode 100644
index 00000000000..4dbdb907b0e
--- /dev/null
+++ b/home-manager/modules/lib/types-dag.nix
@@ -0,0 +1,84 @@
+{ dag, lib }:
+
+with lib;
+
+let
+
+ isDagEntry = e: isAttrs e && (e ? data) && (e ? after) && (e ? before);
+
+ dagContentType = elemType:
+ types.submodule {
+ options = {
+ data = mkOption { type = elemType; };
+ after = mkOption { type = with types; uniq (listOf str); };
+ before = mkOption { type = with types; uniq (listOf str); };
+ };
+ };
+
+in {
+ # A directed acyclic graph of some inner type.
+ dagOf = elemType:
+ let
+ convertAllToDags = let
+ maybeConvert = n: v: if isDagEntry v then v else dag.entryAnywhere v;
+ in map (def: def // { value = mapAttrs maybeConvert def.value; });
+
+ attrEquivalent = types.attrsOf (dagContentType elemType);
+ in mkOptionType rec {
+ name = "dagOf";
+ description = "DAG of ${elemType.description}s";
+ check = isAttrs;
+ merge = loc: defs: attrEquivalent.merge loc (convertAllToDags defs);
+ getSubOptions = prefix: elemType.getSubOptions (prefix ++ [ "<name>" ]);
+ getSubModules = elemType.getSubModules;
+ substSubModules = m: dagOf (elemType.substSubModules m);
+ functor = (defaultFunctor name) // { wrapped = elemType; };
+ };
+
+ # A directed acyclic graph of some inner type OR a list of that
+ # inner type. This is a temporary hack for use by the
+ # `programs.ssh.matchBlocks` and is only guaranteed to be vaguely
+ # correct!
+ #
+ # In particular, adding a dependency on one of the "unnamed-N-M"
+ # entries generated by a list value is almost guaranteed to destroy
+ # the list's order.
+ #
+ # This function will be removed in version 20.09.
+ listOrDagOf = elemType:
+ let
+ paddedIndexStr = list: i:
+ let padWidth = stringLength (toString (length list));
+ in fixedWidthNumber padWidth i;
+
+ convertAllToDags = defs:
+ let
+ convertAttrValue = n: v:
+ if isDagEntry v then v else dag.entryAnywhere v;
+
+ convertListValue = namePrefix: vs:
+ let
+ pad = paddedIndexStr vs;
+ makeEntry = i: v:
+ nameValuePair "${namePrefix}.${pad i}" (dag.entryAnywhere v);
+ in listToAttrs (imap1 makeEntry vs);
+
+ convertValue = i: value:
+ if isList value then
+ convertListValue "unnamed-${paddedIndexStr defs i}" value
+ else
+ mapAttrs convertAttrValue value;
+ in imap1 (i: def: def // { value = convertValue i def.value; }) defs;
+
+ attrEquivalent = types.attrsOf (dagContentType elemType);
+ in mkOptionType rec {
+ name = "dagOf";
+ description = "DAG of ${elemType.description}s";
+ check = x: isAttrs x || isList x;
+ merge = loc: defs: attrEquivalent.merge loc (convertAllToDags defs);
+ getSubOptions = prefix: elemType.getSubOptions (prefix ++ [ "<name>" ]);
+ getSubModules = elemType.getSubModules;
+ substSubModules = m: dagOf (elemType.substSubModules m);
+ functor = (defaultFunctor name) // { wrapped = elemType; };
+ };
+}
diff --git a/home-manager/modules/lib/types.nix b/home-manager/modules/lib/types.nix
new file mode 100644
index 00000000000..78a875f519e
--- /dev/null
+++ b/home-manager/modules/lib/types.nix
@@ -0,0 +1,36 @@
+{ lib, dag ? import ./dag.nix { inherit lib; } }:
+
+with lib;
+
+let
+
+ typesDag = import ./types-dag.nix { inherit dag lib; };
+
+in
+
+{
+
+ inherit (typesDag) dagOf listOrDagOf;
+
+ selectorFunction = mkOptionType {
+ name = "selectorFunction";
+ description =
+ "Function that takes an attribute set and returns a list"
+ + " containing a selection of the values of the input set";
+ check = isFunction;
+ merge = _loc: defs:
+ as: concatMap (select: select as) (getValues defs);
+ };
+
+ overlayFunction = mkOptionType {
+ name = "overlayFunction";
+ description =
+ "An overlay function, takes self and super and returns"
+ + " an attribute set overriding the desired attributes.";
+ check = isFunction;
+ merge = _loc: defs:
+ self: super:
+ foldl' (res: def: mergeAttrs res (def.value self super)) {} defs;
+ };
+
+}
diff --git a/home-manager/modules/lib/zsh.nix b/home-manager/modules/lib/zsh.nix
new file mode 100644
index 00000000000..c6901350f50
--- /dev/null
+++ b/home-manager/modules/lib/zsh.nix
@@ -0,0 +1,30 @@
+{ lib }:
+
+rec {
+ # Produces a Zsh shell like value
+ toZshValue = v:
+ if builtins.isBool v then
+ if v then "true" else "false"
+ else if builtins.isString v then
+ ''"${v}"''
+ else if builtins.isList v then
+ "(${lib.concatStringsSep " " (map toZshValue v)})"
+ else
+ ''"${toString v}"'';
+
+ # Produces a Zsh shell like definition statement
+ define = n: v: "${n}=${toZshValue v}";
+
+ # Given an attribute set containing shell variable names and their
+ # assignments, this function produces a string containing a definition
+ # statement for each set entry.
+ defineAll = vars: lib.concatStringsSep "\n" (lib.mapAttrsToList define vars);
+
+ # Produces a Zsh shell like export statement
+ export = n: v: "export ${define n v}";
+
+ # Given an attribute set containing shell variable names and their
+ # assignments, this function produces a string containing an export
+ # statement for each set entry.
+ exportAll = vars: lib.concatStringsSep "\n" (lib.mapAttrsToList export vars);
+}
diff --git a/home-manager/modules/manual.nix b/home-manager/modules/manual.nix
new file mode 100644
index 00000000000..ab01c45003e
--- /dev/null
+++ b/home-manager/modules/manual.nix
@@ -0,0 +1,68 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.manual;
+
+ docs = import ../doc { inherit lib pkgs; };
+
+in
+
+{
+ options = {
+ manual.html.enable = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Whether to install the HTML manual. This also installs the
+ <command>home-manager-help</command> tool, which opens a local
+ copy of the Home Manager manual in the system web browser.
+ '';
+ };
+
+ manual.manpages.enable = mkOption {
+ type = types.bool;
+ default = true;
+ example = false;
+ description = ''
+ Whether to install the configuration manual page. The manual can
+ be reached by <command>man home-configuration.nix</command>.
+ </para><para>
+ When looking at the manual page pretend that all references to
+ NixOS stuff are actually references to Home Manager stuff.
+ Thanks!
+ '';
+ };
+
+ manual.json.enable = mkOption {
+ type = types.bool;
+ default = false;
+ example = true;
+ description = ''
+ Whether to install a JSON formatted list of all Home Manager
+ options. This can be located at
+ <filename>&lt;profile directory&gt;/share/doc/home-manager/options.json</filename>,
+ and may be used for navigating definitions, auto-completing,
+ and other miscellaneous tasks.
+ '';
+ };
+ };
+
+ config = {
+ home.packages = mkMerge [
+ (mkIf cfg.html.enable [ docs.manual.html docs.manual.htmlOpenTool ])
+ (mkIf cfg.manpages.enable [ docs.manPages ])
+ (mkIf cfg.json.enable [ docs.options.json ])
+ ];
+
+ # Whether a dependency on nmd should be introduced.
+ home.extraBuilderCommands =
+ mkIf (cfg.html.enable || cfg.manpages.enable || cfg.json.enable) ''
+ mkdir $out/lib
+ ln -s ${docs.nmdSrc} $out/lib/nmd
+ '';
+ };
+
+}
diff --git a/home-manager/modules/misc/dconf.nix b/home-manager/modules/misc/dconf.nix
new file mode 100644
index 00000000000..f5c9bf71456
--- /dev/null
+++ b/home-manager/modules/misc/dconf.nix
@@ -0,0 +1,88 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.dconf;
+
+ toDconfIni = generators.toINI { mkKeyValue = mkIniKeyValue; };
+
+ mkIniKeyValue = key: value:
+ let
+ tweakVal = v:
+ if isString v then "'${v}'"
+ else if isList v then tweakList v
+ else if isBool v then (if v then "true" else "false")
+ else toString v;
+
+ # Assume empty list is a list of strings, see #769
+ tweakList = v:
+ if v == [] then "@as []"
+ else "[" + concatMapStringsSep "," tweakVal v + "]";
+
+ in
+ "${key}=${tweakVal value}";
+
+ primitive = with types; either bool (either int (either float str));
+
+in
+
+{
+ meta.maintainers = [ maintainers.gnidorah maintainers.rycee ];
+
+ options = {
+ dconf = {
+ enable = mkOption {
+ type = types.bool;
+ default = true;
+ visible = false;
+ description = ''
+ Whether to enable dconf settings.
+ '';
+ };
+
+ settings = mkOption {
+ type = with types;
+ attrsOf (attrsOf (either primitive (listOf primitive)));
+ default = {};
+ example = literalExample ''
+ {
+ "org/gnome/calculator" = {
+ button-mode = "programming";
+ show-thousands = true;
+ base = 10;
+ word-size = 64;
+ };
+ }
+ '';
+ description = ''
+ Settings to write to the dconf configuration system.
+ '';
+ };
+ };
+ };
+
+ config = mkIf (cfg.enable && cfg.settings != {}) {
+ home.activation.dconfSettings = hm.dag.entryAfter ["installPackages"] (
+ let
+ iniFile = pkgs.writeText "hm-dconf.ini" (toDconfIni cfg.settings);
+ in
+ ''
+ if [[ -v DBUS_SESSION_BUS_ADDRESS ]]; then
+ DCONF_DBUS_RUN_SESSION=""
+ else
+ DCONF_DBUS_RUN_SESSION="${pkgs.dbus}/bin/dbus-run-session"
+ fi
+
+ if [[ -v DRY_RUN ]]; then
+ echo $DCONF_DBUS_RUN_SESSION ${pkgs.gnome3.dconf}/bin/dconf load / "<" ${iniFile}
+ else
+ $DCONF_DBUS_RUN_SESSION ${pkgs.gnome3.dconf}/bin/dconf load / < ${iniFile}
+ fi
+
+ unset DCONF_DBUS_RUN_SESSION
+ ''
+ );
+ };
+}
diff --git a/home-manager/modules/misc/fontconfig.nix b/home-manager/modules/misc/fontconfig.nix
new file mode 100644
index 00000000000..795ab3a74f6
--- /dev/null
+++ b/home-manager/modules/misc/fontconfig.nix
@@ -0,0 +1,105 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.fonts.fontconfig;
+
+ profileDirectory = config.home.profileDirectory;
+
+in {
+ meta.maintainers = [ maintainers.rycee ];
+
+ imports = [
+ (mkRenamedOptionModule [ "fonts" "fontconfig" "enableProfileFonts" ] [
+ "fonts"
+ "fontconfig"
+ "enable"
+ ])
+ ];
+
+ options = {
+ fonts.fontconfig = {
+ enable = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Whether to enable fontconfig configuration. This will, for
+ example, allow fontconfig to discover fonts and
+ configurations installed through
+ <varname>home.packages</varname> and
+ <command>nix-env</command>.
+ '';
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ # Create two dummy files in /lib/fontconfig to make sure that
+ # buildEnv creates a real directory path. These files are removed
+ # in home.extraProfileCommands below so the packages will not
+ # become "runtime" dependencies.
+ home.packages = [
+ (pkgs.writeTextFile {
+ name = "hm-dummy1";
+ destination = "/lib/fontconfig/hm-dummy1";
+ text = "dummy";
+ })
+
+ (pkgs.writeTextFile {
+ name = "hm-dummy2";
+ destination = "/lib/fontconfig/hm-dummy2";
+ text = "dummy";
+ })
+ ];
+
+ home.extraProfileCommands = ''
+ if [[ -d $out/lib/X11/fonts || -d $out/share/fonts ]]; then
+ export FONTCONFIG_FILE="$(pwd)/fonts.conf"
+
+ cat > $FONTCONFIG_FILE << EOF
+ <?xml version='1.0'?>
+ <!DOCTYPE fontconfig SYSTEM 'fonts.dtd'>
+ <fontconfig>
+ <dir>$out/lib/X11/fonts</dir>
+ <dir>$out/share/fonts</dir>
+ <cachedir>$out/lib/fontconfig/cache</cachedir>
+ </fontconfig>
+ EOF
+
+ ${getBin pkgs.fontconfig}/bin/fc-cache -f
+ rm -f $out/lib/fontconfig/cache/CACHEDIR.TAG
+ rmdir --ignore-fail-on-non-empty -p $out/lib/fontconfig/cache
+
+ rm "$FONTCONFIG_FILE"
+ unset FONTCONFIG_FILE
+ fi
+
+ # Remove hacky dummy files.
+ rm $out/lib/fontconfig/hm-dummy?
+ rmdir --ignore-fail-on-non-empty -p $out/lib/fontconfig
+ '';
+
+ xdg.configFile = {
+ "fontconfig/conf.d/10-hm-fonts.conf".text = ''
+ <?xml version='1.0'?>
+
+ <!-- Generated by Home Manager. -->
+
+ <!DOCTYPE fontconfig SYSTEM 'fonts.dtd'>
+ <fontconfig>
+ <include ignore_missing="yes">${config.home.path}/etc/fonts/conf.d</include>
+ <include ignore_missing="yes">${config.home.path}/etc/fonts/fonts.conf</include>
+
+ <dir>${config.home.path}/lib/X11/fonts</dir>
+ <dir>${config.home.path}/share/fonts</dir>
+ <dir>${profileDirectory}/lib/X11/fonts</dir>
+ <dir>${profileDirectory}/share/fonts</dir>
+
+ <cachedir>${config.home.path}/lib/fontconfig/cache</cachedir>
+ </fontconfig>
+ '';
+ };
+ };
+}
diff --git a/home-manager/modules/misc/gtk.nix b/home-manager/modules/misc/gtk.nix
new file mode 100644
index 00000000000..1222db4ecab
--- /dev/null
+++ b/home-manager/modules/misc/gtk.nix
@@ -0,0 +1,188 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.gtk;
+ cfg2 = config.gtk.gtk2;
+ cfg3 = config.gtk.gtk3;
+
+ toGtk3Ini = generators.toINI {
+ mkKeyValue = key: value:
+ let
+ value' =
+ if isBool value then (if value then "true" else "false")
+ else toString value;
+ in
+ "${key}=${value'}";
+ };
+
+ formatGtk2Option = n: v:
+ let
+ v' =
+ if isBool v then (if v then "true" else "false")
+ else if isString v then "\"${v}\""
+ else toString v;
+ in
+ "${n} = ${v'}";
+
+ fontType = types.submodule {
+ options = {
+ package = mkOption {
+ type = types.nullOr types.package;
+ default = null;
+ example = literalExample "pkgs.dejavu_fonts";
+ description = ''
+ Package providing the font. This package will be installed
+ to your profile. If <literal>null</literal> then the font
+ is assumed to already be available in your profile.
+ '';
+ };
+
+ name = mkOption {
+ type = types.str;
+ example = "DejaVu Sans 8";
+ description = ''
+ The family name and size of the font within the package.
+ '';
+ };
+ };
+ };
+
+ themeType = types.submodule {
+ options = {
+ package = mkOption {
+ type = types.nullOr types.package;
+ default = null;
+ example = literalExample "pkgs.gnome3.gnome_themes_standard";
+ description = ''
+ Package providing the theme. This package will be installed
+ to your profile. If <literal>null</literal> then the theme
+ is assumed to already be available in your profile.
+ '';
+ };
+
+ name = mkOption {
+ type = types.str;
+ example = "Adwaita";
+ description = "The name of the theme within the package.";
+ };
+ };
+ };
+
+in
+
+{
+ meta.maintainers = [ maintainers.rycee ];
+
+ imports = [
+ (mkRemovedOptionModule ["gtk" "gtk3" "waylandSupport"] ''
+ This options is not longer needed and can be removed.
+ '')
+ ];
+
+ options = {
+ gtk = {
+ enable = mkEnableOption "GTK 2/3 configuration";
+
+ font = mkOption {
+ type = types.nullOr fontType;
+ default = null;
+ description = ''
+ The font to use in GTK+ 2/3 applications.
+ '';
+ };
+
+ iconTheme = mkOption {
+ type = types.nullOr themeType;
+ default = null;
+ description = "The icon theme to use.";
+ };
+
+ theme = mkOption {
+ type = types.nullOr themeType;
+ default = null;
+ description = "The GTK+2/3 theme to use.";
+ };
+
+ gtk2 = {
+ extraConfig = mkOption {
+ type = types.lines;
+ default = "";
+ example = "gtk-can-change-accels = 1";
+ description = ''
+ Extra configuration lines to add verbatim to
+ <filename>~/.gtkrc-2.0</filename>.
+ '';
+ };
+ };
+
+ gtk3 = {
+ extraConfig = mkOption {
+ type = with types; attrsOf (either bool (either int str));
+ default = {};
+ example = { gtk-cursor-blink = false; gtk-recent-files-limit = 20; };
+ description = ''
+ Extra configuration options to add to
+ <filename>~/.config/gtk-3.0/settings.ini</filename>.
+ '';
+ };
+
+ extraCss = mkOption {
+ type = types.lines;
+ default = "";
+ description = ''
+ Extra configuration lines to add verbatim to
+ <filename>~/.config/gtk-3.0/gtk.css</filename>.
+ '';
+ };
+ };
+ };
+ };
+
+ config = mkIf cfg.enable (
+ let
+ ini =
+ optionalAttrs (cfg.font != null)
+ { gtk-font-name = cfg.font.name; }
+ //
+ optionalAttrs (cfg.theme != null)
+ { gtk-theme-name = cfg.theme.name; }
+ //
+ optionalAttrs (cfg.iconTheme != null)
+ { gtk-icon-theme-name = cfg.iconTheme.name; };
+
+ dconfIni =
+ optionalAttrs (cfg.font != null)
+ { font-name = cfg.font.name; }
+ //
+ optionalAttrs (cfg.theme != null)
+ { gtk-theme = cfg.theme.name; }
+ //
+ optionalAttrs (cfg.iconTheme != null)
+ { icon-theme = cfg.iconTheme.name; };
+
+ optionalPackage = opt:
+ optional (opt != null && opt.package != null) opt.package;
+ in
+ {
+ home.packages =
+ optionalPackage cfg.font
+ ++ optionalPackage cfg.theme
+ ++ optionalPackage cfg.iconTheme;
+
+ home.file.".gtkrc-2.0".text =
+ concatStringsSep "\n" (
+ mapAttrsToList formatGtk2Option ini
+ ) + "\n" + cfg2.extraConfig;
+
+ xdg.configFile."gtk-3.0/settings.ini".text =
+ toGtk3Ini { Settings = ini // cfg3.extraConfig; };
+
+ xdg.configFile."gtk-3.0/gtk.css".text = cfg3.extraCss;
+
+ dconf.settings."org/gnome/desktop/interface" = dconfIni;
+ }
+ );
+}
diff --git a/home-manager/modules/misc/lib.nix b/home-manager/modules/misc/lib.nix
new file mode 100644
index 00000000000..13c00dc59a6
--- /dev/null
+++ b/home-manager/modules/misc/lib.nix
@@ -0,0 +1,14 @@
+{ lib, ... }:
+
+{
+ options = {
+ lib = lib.mkOption {
+ type = lib.types.attrsOf lib.types.attrs;
+ default = { };
+ description = ''
+ This option allows modules to define helper functions,
+ constants, etc.
+ '';
+ };
+ };
+}
diff --git a/home-manager/modules/misc/news.nix b/home-manager/modules/misc/news.nix
new file mode 100644
index 00000000000..6b01617fc55
--- /dev/null
+++ b/home-manager/modules/misc/news.nix
@@ -0,0 +1,1323 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.news;
+
+ hostPlatform = pkgs.stdenv.hostPlatform;
+
+ entryModule = types.submodule ({ config, ... }: {
+ options = {
+ id = mkOption {
+ internal = true;
+ type = types.str;
+ description = ''
+ A unique entry identifier. By default it is a base16
+ formatted hash of the entry message.
+ '';
+ };
+
+ time = mkOption {
+ internal = true;
+ type = types.str;
+ example = "2017-07-10T21:55:04+00:00";
+ description = ''
+ News entry time stamp in ISO-8601 format. Must be in UTC
+ (ending in '+00:00').
+ '';
+ };
+
+ condition = mkOption {
+ internal = true;
+ default = true;
+ description = "Whether the news entry should be active.";
+ };
+
+ message = mkOption {
+ internal = true;
+ type = types.str;
+ description = "The news entry content.";
+ };
+ };
+
+ config = {
+ id = mkDefault (builtins.hashString "sha256" config.message);
+ };
+ });
+
+in
+
+{
+ meta.maintainers = [ maintainers.rycee ];
+
+ options = {
+ news = {
+ display = mkOption {
+ type = types.enum [ "silent" "notify" "show" ];
+ default = "notify";
+ description = ''
+ How unread and relevant news should be presented when
+ running <command>home-manager build</command> and
+ <command>home-manager switch</command>.
+
+ </para><para>
+
+ The options are
+
+ <variablelist>
+ <varlistentry>
+ <term><literal>silent</literal></term>
+ <listitem>
+ <para>
+ Do not print anything during build or switch. The
+ <command>home-manager news</command> command still
+ works for viewing the entries.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><literal>notify</literal></term>
+ <listitem>
+ <para>
+ The number of unread and relevant news entries will be
+ printed to standard output. The <command>home-manager
+ news</command> command can later be used to view the
+ entries.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><literal>show</literal></term>
+ <listitem>
+ <para>
+ A pager showing unread news entries is opened.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ '';
+ };
+
+ entries = mkOption {
+ internal = true;
+ type = types.listOf entryModule;
+ default = [];
+ description = "News entries.";
+ };
+ };
+ };
+
+ config = {
+ # Add news entries in chronological order (i.e., latest time
+ # should be at the bottom of the list). The time should be
+ # formatted as given in the output of
+ #
+ # date --iso-8601=second --universal
+ #
+ news.entries = [
+ {
+ time = "2017-09-01T10:56:28+00:00";
+ message = ''
+ Hello! This is a news entry and it represents an
+ experimental new feature of Home Manager. The idea is to
+ inform you when something of importance happens in Home
+ Manager or its modules.
+
+ We will try to not disturb you about the same news more than
+ once so the next time you run
+
+ home-manager switch
+
+ or
+
+ home-manager build
+
+ it should not notify you about this text again.
+
+ News items may be conditional and will then only show if the
+ condition holds, for example if they are relevant to your
+ configuration.
+
+ If you want to see all relevant news then please use the
+
+ home-manager news
+
+ command.
+
+ Since this is an experimental feature any positive or
+ negative feedback would be greatly appreciated. For example,
+ by commenting in https://git.io/v5BJL.
+ '';
+ }
+
+ {
+ time = "2017-09-10T22:15:19+00:00";
+ condition = config.programs.zsh.enable;
+ message = ''
+ Home Manager now offers its own minimal zsh plugin manager
+ under the 'programs.zsh.plugins' option path. By statically
+ sourcing your plugins it achieves no startup overhead.
+ '';
+ }
+
+ {
+ time = "2017-09-12T13:11:48+00:00";
+ condition = (
+ config.programs.zsh.enable &&
+ config.programs.zsh.shellAliases != {}
+ );
+ message = ''
+ Aliases defined in 'programs.zsh.shellAliases'
+ are now have the highest priority. Such aliases will
+ not be redefined by the code in 'programs.zsh.initExtra'
+ or any external plugins.
+ '';
+ }
+
+ {
+ time = "2017-09-12T14:22:18+00:00";
+ message = ''
+ A new service is available: 'services.blueman-applet'.
+ '';
+ }
+
+ {
+ time = "2017-09-13T11:30:22+00:00";
+ message = ''
+ A new service is available: 'services.compton'.
+ '';
+ }
+
+ {
+ time = "2017-09-20T14:47:14+00:00";
+ message = ''
+ A new service is available: 'services.screen-locker'.
+ '';
+ }
+
+ {
+ time = "2017-09-22T12:09:01+00:00";
+ condition = isString config.programs.git.extraConfig;
+ message = ''
+ The 'programs.git.extraConfig' parameter now accepts
+ attributes instead of strings which allows more flexible
+ configuration.
+
+ The string parameter type will be deprecated in the future,
+ please change your configuration file accordingly.
+
+ For example, if your configuration includes
+
+ programs.git.extraConfig = '''
+ [core]
+ editor = vim
+ ''';
+
+ then you can now change it to
+
+ programs.git.extraConfig = {
+ core = {
+ editor = "vim";
+ };
+ };
+ '';
+ }
+
+ {
+ time = "2017-09-27T07:28:54+00:00";
+ message = ''
+ A new program module is available: 'programs.command-not-found'.
+
+ Note, this differs from the NixOS system command-not-found
+ tool in that NIX_AUTO_INSTALL is not supported.
+ '';
+ }
+
+ {
+ time = "2017-09-28T12:39:36+00:00";
+ message = ''
+ A new program module is available: 'programs.rofi';
+ '';
+ }
+
+ {
+ time = "2017-10-02T11:15:03+00:00";
+ condition = config.services.udiskie.enable;
+ message = ''
+ The udiskie service now defaults to automatically mounting
+ new devices. Previous behavior was to not automatically
+ mount. To restore this previous behavior add
+
+ services.udiskie.automount = false;
+
+ to your Home Manager configuration.
+ '';
+ }
+
+ {
+ time = "2017-10-04T18:36:07+00:00";
+ message = ''
+ A new module is available: 'xsession.windowManager.xmonad'.
+ '';
+ }
+
+ {
+ time = "2017-10-06T08:21:43+00:00";
+ message = ''
+ A new service is available: 'services.polybar'.
+ '';
+ }
+
+ {
+ time = "2017-10-09T16:38:34+00:00";
+ message = ''
+ A new module is available: 'fonts.fontconfig'.
+
+ In particular, the Boolean option
+
+ fonts.fontconfig.enableProfileFonts
+
+ was added for those who do not use NixOS and want to install
+ font packages using 'nix-env' or 'home.packages'. If you are
+ using NixOS then you do not need to enable this option.
+ '';
+ }
+
+ {
+ time = "2017-10-12T11:21:45+00:00";
+ condition = config.programs.zsh.enable;
+ message = ''
+ A new option in zsh module is available: 'programs.zsh.sessionVariables'.
+
+ This option can be used to set zsh specific session variables which
+ will be set only on zsh launch.
+ '';
+ }
+
+ {
+ time = "2017-10-15T13:59:47+00:00";
+ message = ''
+ A new module is available: 'programs.man'.
+
+ This module is enabled by default and makes sure that manual
+ pages are installed for packages in 'home.packages'.
+ '';
+ }
+
+ {
+ time = "2017-10-20T12:15:27+00:00";
+ condition = with config.systemd.user;
+ services != {} || sockets != {} || targets != {} || timers != {};
+ message = ''
+ Home Manager's interaction with systemd is now done using
+ 'systemctl' from Nixpkgs, not the 'systemctl' in '$PATH'.
+
+ If you are using a distribution whose systemd is
+ incompatible with the version in Nixpkgs then you can
+ override this behavior by adding
+
+ systemd.user.systemctlPath = "/usr/bin/systemctl"
+
+ to your configuration. Home Manager will then use your
+ chosen version.
+ '';
+ }
+
+ {
+ time = "2017-10-23T23:10:29+00:00";
+ condition = !config.programs.home-manager.enable;
+ message = ''
+ Unfortunately, due to some internal restructuring it is no
+ longer possible to install the home-manager command when
+ having
+
+ home-manager = import ./home-manager { inherit pkgs; };
+
+ in the '~/.config/nixpkgs/config.nix' package override
+ section. Attempting to use the above override will now
+ result in the error "cannot coerce a set to a string".
+
+ To resolve this please delete the override from the
+ 'config.nix' file and either link the Home Manager overlay
+
+ $ ln -s ~/.config/nixpkgs/home-manager/overlay.nix \
+ ~/.config/nixpkgs/overlays/home-manager.nix
+
+ or add
+
+ programs.home-manager.enable = true;
+
+ to your Home Manager configuration. The latter is
+ recommended as the home-manager tool then is updated
+ automatically whenever you do a switch.
+ '';
+ }
+
+ {
+ time = "2017-10-23T23:26:17+00:00";
+ message = ''
+ A new module is available: 'nixpkgs'.
+
+ Like the identically named NixOS module, this allows you to
+ set Nixpkgs options and define Nixpkgs overlays. Note, the
+ changes you make here will not automatically apply to Nix
+ commands run outside Home Manager.
+ '';
+ }
+
+ {
+ time = "2017-10-28T23:39:55+00:00";
+ message = ''
+ A new module is available: 'xdg'.
+
+ If enabled, this module allows configuration of the XDG base
+ directory paths.
+
+ Whether the module is enabled or not, it also offers the
+ option 'xdg.configFile', which acts much like 'home.file'
+ except the target path is relative to the XDG configuration
+ directory. That is, unless `XDG_CONFIG_HOME` is configured
+ otherwise, the assignment
+
+ xdg.configFile.hello.text = "hello world";
+
+ will result in a file '$HOME/.config/hello'.
+
+ Most modules in Home Manager that previously were hard coded
+ to write configuration to '$HOME/.config' now use this
+ option and will therefore honor the XDG configuration
+ directory.
+ '';
+ }
+
+ {
+ time = "2017-10-31T11:46:07+00:00";
+ message = ''
+ A new window manager module is available: 'xsession.windowManager.i3'.
+ '';
+ }
+
+ {
+ time = "2017-11-12T00:18:59+00:00";
+ message = ''
+ A new program module is available: 'programs.neovim'.
+ '';
+ }
+
+ {
+ time = "2017-11-14T19:56:49+00:00";
+ condition = with config.xsession.windowManager; (
+ i3.enable && i3.config != null && i3.config.startup != []
+ );
+ message = ''
+ A new 'notification' option was added to
+ xsession.windowManager.i3.startup submodule.
+
+ Startup commands are now executed with the startup-notification
+ support enabled by default. Please, set 'notification' to false
+ where --no-startup-id option is necessary.
+ '';
+ }
+
+ {
+ time = "2017-11-17T10:36:10+00:00";
+ condition = config.xsession.windowManager.i3.enable;
+ message = ''
+ The i3 window manager module has been extended with the following options:
+
+ i3.config.keycodebindings
+ i3.config.window.commands
+ i3.config.window.hideEdgeBorders
+ i3.config.focus.mouseWarping
+ '';
+ }
+
+ {
+ time = "2017-11-26T21:57:23+00:00";
+ message = ''
+ Two new modules are available:
+
+ 'services.kbfs' and 'services.keybase'
+ '';
+ }
+
+ {
+ time = "2017-12-07T22:23:11+00:00";
+ message = ''
+ A new module is available: 'services.parcellite'
+ '';
+ }
+
+ {
+ time = "2017-12-11T17:23:12+00:00";
+ condition = config.home.activation ? reloadSystemD;
+ message = ''
+ The Boolean option 'systemd.user.startServices' is now
+ available. When enabled the current naive systemd unit
+ reload logic is replaced by a more sophisticated one that
+ attempts to automatically start, stop, and restart units as
+ necessary.
+ '';
+ }
+
+ {
+ time = "2018-02-02T11:15:00+00:00";
+ message = ''
+ A new program configuration is available: 'programs.mercurial'
+ '';
+ }
+
+ {
+ time = "2018-02-03T10:00:00+00:00";
+ message = ''
+ A new module is available: 'services.stalonetray'
+ '';
+ }
+
+ {
+ time = "2018-02-04T22:58:49+00:00";
+ condition = config.xsession.enable;
+ message = ''
+ A new option 'xsession.pointerCursor' is now available. It
+ allows specifying the pointer cursor theme and size. The
+ settings will be applied in the xsession, Xresources, and
+ GTK configurations.
+ '';
+ }
+
+ {
+ time = "2018-02-06T20:23:34+00:00";
+ message = ''
+ It is now possible to use Home Manager as a NixOS module.
+ This allows you to prepare user environments from the system
+ configuration file, which often is more convenient than
+ using the 'home-manager' tool. It also opens up additional
+ possibilities, for example, to automatically configure user
+ environments in NixOS declarative containers or on systems
+ deployed through NixOps.
+
+ This feature should be considered experimental for now and
+ some critial limitations apply. For example, it is currently
+ not possible to use 'nixos-rebuild build-vm' when using the
+ Home Manager NixOS module. That said, it should be
+ reasonably robust and stable for simpler use cases.
+
+ To make Home Manager available in your NixOS system
+ configuration you can add
+
+ imports = [
+ "''${builtins.fetchTarball https://github.com/rycee/home-manager/archive/master.tar.gz}/nixos"
+ ];
+
+ to your 'configuration.nix' file. This will introduce a new
+ NixOS option called 'home-manager.users' whose type is an
+ attribute set mapping user names to Home Manager
+ configurations.
+
+ For example, a NixOS configuration may include the lines
+
+ users.users.eve.isNormalUser = true;
+ home-manager.users.eve = {
+ home.packages = [ pkgs.atool pkgs.httpie ];
+ programs.bash.enable = true;
+ };
+
+ and after a 'nixos-rebuild switch' the user eve's
+ environment should include a basic Bash configuration and
+ the packages atool and httpie.
+
+ More detailed documentation on the intricacies of this new
+ feature is slowly forthcoming.
+ '';
+ }
+
+ {
+ time = "2018-02-09T21:14:42+00:00";
+ condition = with config.programs.rofi; enable && colors != null;
+ message = ''
+ The new and preferred way to configure the rofi theme is
+ using rasi themes through the 'programs.rofi.theme' option.
+ This option can take as value either the name of a
+ pre-installed theme or the path to a theme file.
+
+ A rasi theme can be generated from an Xresources config
+ using 'rofi -dump-theme'.
+
+ The option 'programs.rofi.colors' is still supported but may
+ become deprecated and removed in the future.
+ '';
+ }
+
+ {
+ time = "2018-02-19T21:45:26+00:00";
+ message = ''
+ A new module is available: 'programs.pidgin'
+ '';
+ }
+
+ {
+ time = "2018-03-04T06:54:26+00:00";
+ message = ''
+ A new module is available: 'services.unclutter'
+ '';
+ }
+
+ {
+ time = "2018-03-07T21:38:27+00:00";
+ message = ''
+ A new module is available: 'programs.fzf'.
+ '';
+ }
+
+ {
+ time = "2018-03-25T06:49:57+00:00";
+ condition = with config.programs.ssh; enable && matchBlocks != {};
+ message = ''
+ Options set through the 'programs.ssh' module are now placed
+ at the end of the SSH configuration file. This was done to
+ make it possible to override global options such as
+ 'ForwardAgent' or 'Compression' inside a host match block.
+
+ If you truly need to override an SSH option across all match
+ blocks then the new option
+
+ programs.ssh.extraOptionOverrides
+
+ can be used.
+ '';
+ }
+
+ {
+ time = "2018-04-19T07:42:01+00:00";
+ message = ''
+ A new module is available: 'programs.autorandr'.
+ '';
+ }
+
+ {
+ time = "2018-04-19T15:44:55+00:00";
+ condition = config.programs.git.enable;
+ message = ''
+ A new option 'programs.git.includes' is available. Additional
+ Git configuration files may be included via
+
+ programs.git.includes = [
+ { path = "~/path/to/config.inc"; }
+ ];
+
+ or conditionally via
+
+ programs.git.includes = [
+ { path = "~/path/to/config.inc"; condition = "gitdir:~/src/"; }
+ ];
+
+ and the corresponding '[include]' or '[includeIf]' sections will be
+ appended to the main Git configuration file.
+ '';
+ }
+
+ {
+ time = "2018-05-01T20:49:31+00:00";
+ message = ''
+ A new module is available: 'services.mbsync'.
+ '';
+ }
+ {
+ time = "2018-05-03T12:34:47+00:00";
+ message = ''
+ A new module is available: 'services.flameshot'.
+ '';
+ }
+
+ {
+ time = "2018-05-18T18:34:15+00:00";
+ message = ''
+ A new module is available: 'qt'
+
+ At the moment this module allows you to set up Qt to use the
+ GTK+ theme, and not much else.
+ '';
+ }
+
+ {
+ time = "2018-06-05T01:36:45+00:00";
+ message = ''
+ A new module is available: 'services.kdeconnect'.
+ '';
+ }
+
+ {
+ time = "2018-06-09T09:11:59+00:00";
+ message = ''
+ A new module is available: `programs.newsboat`.
+ '';
+ }
+
+ {
+ time = "2018-07-01T14:33:15+00:00";
+ message = ''
+ A new module is available: 'accounts.email'.
+
+ As the name suggests, this new module offers a number of
+ options for configuring email accounts. This, for example,
+ includes the email address and owner's real name but also
+ server settings for IMAP and SMTP.
+
+ The intent is to have a central location for account
+ specific configuration that other modules can use.
+
+ Note, this module is still somewhat experimental and its
+ structure should not be seen as final. Feedback is greatly
+ appreciated, both positive and negative.
+ '';
+ }
+
+ {
+ time = "2018-07-01T16:07:04+00:00";
+ message = ''
+ A new module is available: 'programs.mbsync'.
+ '';
+ }
+
+ {
+ time = "2018-07-01T16:12:20+00:00";
+ message = ''
+ A new module is available: 'programs.notmuch'.
+ '';
+ }
+
+ {
+ time = "2018-07-07T15:48:56+00:00";
+ message = ''
+ A new module is available: 'xsession.windowManager.awesome'.
+ '';
+ }
+
+ {
+ time = "2018-07-18T20:14:11+00:00";
+ message = ''
+ A new module is available: 'services.mpd'.
+ '';
+ }
+
+ {
+ time = "2018-07-31T13:33:39+00:00";
+ message = ''
+ A new module is available: 'services.status-notifier-watcher'.
+ '';
+ }
+
+ {
+ time = "2018-07-31T13:47:06+00:00";
+ message = ''
+ A new module is available: 'programs.direnv'.
+ '';
+ }
+
+ {
+ time = "2018-08-17T20:30:14+00:00";
+ message = ''
+ A new module is available: 'programs.fish'.
+ '';
+ }
+
+ {
+ time = "2018-08-18T19:03:42+00:00";
+ condition = config.services.gpg-agent.enable;
+ message = ''
+ A new option is available: 'services.gpg-agent.extraConfig'.
+
+ Extra lines may be appended to $HOME/.gnupg/gpg-agent.conf
+ using this option.
+ '';
+ }
+
+ {
+ time = "2018-08-19T20:46:09+00:00";
+ condition = hostPlatform.isLinux;
+ message = ''
+ A new modules is available: 'programs.chromium'.
+ '';
+ }
+
+ {
+ time = "2018-08-20T20:27:26+00:00";
+ message = ''
+ A new module is available: 'programs.msmtp'.
+ '';
+ }
+
+ {
+ time = "2018-08-21T20:13:50+00:00";
+ message = ''
+ A new module is available: 'services.pasystray'.
+ '';
+ }
+
+ {
+ time = "2018-08-29T20:27:04+00:00";
+ message = ''
+ A new module is available: 'programs.offlineimap'.
+ '';
+ }
+
+ {
+ time = "2018-09-18T21:25:14+00:00";
+ message = ''
+ A new module is available: 'programs.taskwarrior'.
+ '';
+ }
+
+ {
+ time = "2018-09-18T21:43:54+00:00";
+ message = ''
+ A new module is available: 'programs.zathura'.
+ '';
+ }
+
+ {
+ time = "2018-09-20T19:26:40+00:00";
+ message = ''
+ A new module is available: 'programs.noti'.
+ '';
+ }
+
+ {
+ time = "2018-09-20T22:10:45+00:00";
+ message = ''
+ A new module is available: 'programs.go'.
+ '';
+ }
+
+ {
+ time = "2018-09-27T17:48:08+00:00";
+ message = ''
+ A new module is available: 'programs.obs-studio'.
+ '';
+ }
+
+ {
+ time = "2018-09-28T21:38:48+00:00";
+ message = ''
+ A new module is available: 'programs.alot'.
+ '';
+ }
+
+ {
+ time = "2018-10-20T09:30:57+00:00";
+ message = ''
+ A new module is available: 'programs.urxvt'.
+ '';
+ }
+
+ {
+ time = "2018-11-13T23:08:03+00:00";
+ message = ''
+ A new module is available: 'programs.tmux'.
+ '';
+ }
+
+ {
+ time = "2018-11-18T18:55:15+00:00";
+ message = ''
+ A new module is available: 'programs.astroid'.
+ '';
+ }
+
+ {
+ time = "2018-11-18T21:41:51+00:00";
+ message = ''
+ A new module is available: 'programs.afew'.
+ '';
+ }
+
+ {
+ time = "2018-11-19T00:40:34+00:00";
+ message = ''
+ A new nix-darwin module is available. Use it the same way the NixOS
+ module is used. A major limitation is that Home Manager services don't
+ work, as they depend explicitly on Linux and systemd user services.
+ However, 'home.file' and 'home.packages' do work. Everything else is
+ untested at this time.
+ '';
+ }
+
+ {
+ time = "2018-11-24T16:22:19+00:00";
+ message = ''
+ A new option 'home.stateVersion' is available. Its function
+ is much like the 'system.stateVersion' option in NixOS.
+
+ Briefly, the state version indicates a stable set of option
+ defaults. In the future, whenever Home Manager changes an
+ option default in a way that may cause program breakage it
+ will do so only for the unstable state version, currently
+ 19.03. Once 19.03 becomes the stable version only backwards
+ compatible changes will be made and 19.09 becomes the
+ unstable state version.
+
+ The default value for this option is 18.09 but it may still
+ be a good idea to explicitly add
+
+ home.stateVersion = "18.09";
+
+ to your Home Manager configuration.
+ '';
+ }
+
+ {
+ time = "2018-11-25T22:10:15+00:00";
+ message = ''
+ A new module is available: 'services.nextcloud-client'.
+ '';
+ }
+
+ {
+ time = "2018-11-25T22:55:12+00:00";
+ message = ''
+ A new module is available: 'programs.vscode'.
+ '';
+ }
+
+ {
+ time = "2018-12-04T21:54:38+00:00";
+ condition = config.programs.beets.settings != {};
+ message = ''
+ A new option 'programs.beets.enable' has been added.
+ Starting with state version 19.03 this option defaults to
+ false. For earlier versions it defaults to true if
+ 'programs.beets.settings' is non-empty.
+
+ It is recommended to explicitly add
+
+ programs.beets.enable = true;
+
+ to your configuration.
+ '';
+ }
+
+ {
+ time = "2018-12-12T21:02:05+00:00";
+ message = ''
+ A new module is available: 'programs.jq'.
+ '';
+ }
+
+ {
+ time = "2018-12-24T16:26:16+00:00";
+ message = ''
+ A new module is available: 'dconf'.
+
+ Note, on NixOS you may need to add
+
+ services.dbus.packages = with pkgs; [ gnome3.dconf ];
+
+ to the system configuration for this module to work as
+ expected. In particular if you get the error message
+
+ The name ca.desrt.dconf was not provided by any .service files
+
+ when activating your Home Manager configuration.
+ '';
+ }
+
+ {
+ time = "2018-12-28T12:32:30+00:00";
+ message = ''
+ A new module is available: 'programs.opam'.
+ '';
+ }
+
+ {
+ time = "2019-01-18T00:21:56+00:00";
+ message = ''
+ A new module is available: 'programs.matplotlib'.
+ '';
+ }
+
+ {
+ time = "2019-01-26T13:20:37+00:00";
+ condition = hostPlatform.isLinux;
+ message = ''
+ A new module is available: 'services.xembed-sni-proxy'.
+ '';
+ }
+
+ {
+ time = "2019-01-28T23:36:10+00:00";
+ message = ''
+ A new module is available: 'programs.irssi'.
+ '';
+ }
+
+ {
+ time = "2019-02-09T14:09:58+00:00";
+ condition = hostPlatform.isLinux;
+ message = ''
+ A new module is available: 'services.emacs'.
+
+ This module provides a user service that runs the Emacs
+ configured in
+
+ programs.emacs
+
+ as an Emacs daemon.
+ '';
+ }
+
+ {
+ time = "2019-02-16T20:33:56+00:00";
+ condition = hostPlatform.isLinux;
+ message = ''
+ When using Home Manager as a NixOS submodule it is now
+ possible to install packages using the NixOS
+
+ users.users.<name?>.packages
+
+ option. This is enabled by adding
+
+ home-manager.useUserPackages = true;
+
+ to your NixOS system configuration. This mode of operation
+ is necessary if you want to use 'nixos-rebuild build-vm'.
+ '';
+ }
+
+ {
+ time = "2019-02-17T21:11:24+00:00";
+ message = ''
+ A new module is available: 'programs.keychain'.
+ '';
+ }
+
+ {
+ time = "2019-02-24T00:32:23+00:00";
+ condition = hostPlatform.isLinux;
+ message = ''
+ A new service is available: 'services.mpdris2'.
+ '';
+ }
+
+ {
+ time = "2019-03-19T22:56:20+00:00";
+ message = ''
+ A new module is available: 'programs.bat'.
+ '';
+ }
+
+ {
+ time = "2019-03-19T23:07:34+00:00";
+ message = ''
+ A new module is available: 'programs.lsd'.
+ '';
+ }
+
+ {
+ time = "2019-04-09T20:10:22+00:00";
+ condition = hostPlatform.isLinux;
+ message = ''
+ A new module is available: 'services.xcape'.
+ '';
+ }
+
+ {
+ time = "2019-04-11T22:50:10+00:00";
+ condition = hostPlatform.isLinux;
+ message = ''
+ The type used for the systemd unit options under
+
+ systemd.user.services, systemd.user.sockets, etc.
+
+ has been changed to offer more robust merging of configurations.
+
+ If you don't override values within systemd units then you are not
+ affected by this change. Unfortunately, if you do override unit values
+ you may encounter errors due to this change.
+
+ In particular, if you get an error saying that a "unique option" is
+ "defined multiple times" then you need to use 'lib.mkForce'. For
+ example,
+
+ systemd.user.services.foo.Service.ExecStart = "/foo/bar";
+
+ becomes
+
+ systemd.user.services.foo.Service.ExecStart = lib.mkForce "/foo/bar";
+
+ We had to make this change because the old merging was causing too
+ many confusing situations for people. Apologies for potentially
+ breaking your configuration!
+ '';
+ }
+
+ {
+ time = "2019-04-14T15:35:16+00:00";
+ message = ''
+ A new module is available: 'programs.skim'.
+ '';
+ }
+
+ {
+ time = "2019-04-22T12:43:20+00:00";
+ message = ''
+ A new module is available: 'programs.alacritty'.
+ '';
+ }
+
+ {
+ time = "2019-04-26T22:53:48+00:00";
+ condition = config.programs.vscode.enable;
+ message = ''
+ A new module is available: 'programs.vscode.haskell'.
+
+ Enable to add Haskell IDE Engine and syntax highlighting
+ support to your VSCode.
+ '';
+ }
+
+ {
+ time = "2019-05-04T23:56:39+00:00";
+ condition = hostPlatform.isLinux;
+ message = ''
+ A new module is available: 'services.rsibreak'.
+ '';
+ }
+
+ {
+ time = "2019-05-07T20:49:29+00:00";
+ message = ''
+ A new module is available: 'programs.mpv'.
+ '';
+ }
+
+ {
+ time = "2019-05-30T17:49:29+00:00";
+ condition = hostPlatform.isLinux;
+ message = ''
+ A new module is available: 'services.xsuspender'.
+ '';
+ }
+
+ {
+ time = "2019-06-03T21:47:10+00:00";
+ message = ''
+ A new module is available: 'programs.gpg'.
+ '';
+ }
+
+ {
+ time = "2019-06-09T12:19:18+00:00";
+ message = ''
+ Collisions between unmanaged and managed files can now be
+ automatically resolved by moving the target file to a new
+ path instead of failing the switch operation. To enable
+ this, use the new '-b' command line argument. For example,
+
+ home-manager -b bck switch
+
+ where 'bck' is the suffix to give the moved file. In this
+ case a colliding file 'foo.conf' will be moved to
+ 'foo.conf.bck'.
+ '';
+ }
+
+ {
+ time = "2019-06-19T17:49:29+00:00";
+ condition = hostPlatform.isLinux;
+ message = ''
+ A new module is available: `services.getmail`.
+ '';
+ }
+
+ {
+ time = "2019-07-02T09:27:56+00:00";
+ message = ''
+ A new module is available: 'programs.broot'.
+ '';
+ }
+
+ {
+ time = "2019-07-17T19:30:29+00:00";
+ condition = hostPlatform.isLinux;
+ message = ''
+ A new module is available: 'services.taskwarrior-sync'.
+ '';
+ }
+
+ {
+ time = "2019-07-17T20:05:29+00:00";
+ message = ''
+ A new module is available: 'programs.kakoune'.
+ '';
+ }
+
+ {
+ time = "2019-08-08T11:49:35+00:00";
+ condition = hostPlatform.isLinux;
+ message = ''
+ A new module is available: 'services.hound'.
+ '';
+ }
+
+ {
+ time = "2019-08-17T12:24:58+00:00";
+ condition = hostPlatform.isLinux;
+ message = ''
+ A new module is available: 'services.muchsync'.
+ '';
+ }
+
+ {
+ time = "2019-08-18T14:22:41+00:00";
+ condition = hostPlatform.isLinux;
+ message = ''
+ A new module is available: 'services.dwm-status'.
+ '';
+ }
+
+ {
+ time = "2019-08-28T10:18:07+00:00";
+ condition = config.programs.vim.enable;
+ message = ''
+ The 'programs.vim.plugins' option now accepts packages.
+ Specifying them as strings is deprecated.
+ '';
+ }
+
+ {
+ time = "2019-09-17T19:33:49+00:00";
+ condition = hostPlatform.isLinux;
+ message = ''
+ A new module is available: 'services.sxhkd'.
+ '';
+ }
+
+ {
+ time = "2019-09-26T21:05:24+00:00";
+ message = ''
+ A new module is available: 'programs.starship'.
+ '';
+ }
+
+ {
+ time = "2019-09-26T21:47:13+00:00";
+ message = ''
+ A new module is available: 'programs.rtorrent'.
+ '';
+ }
+
+ {
+ time = "2019-11-04T20:56:29+00:00";
+ message = ''
+ A new module is available: 'programs.pazi'.
+ '';
+ }
+
+ {
+ time = "2019-11-05T21:54:04+00:00";
+ condition = config.programs.zsh.enable;
+ message = ''
+ The 'programs.zsh.history.path' option behavior and the
+ default value has changed for state version 20.03 and above.
+
+ Specifically, '$HOME' will no longer be prepended to the
+ option value, which allows specifying absolute paths (e.g.
+ using the xdg module). Also, the default value is fixed to
+ '$HOME/.zsh_history' and 'dotDir' path is not prepended to
+ it anymore.
+ '';
+ }
+
+ {
+ time = "2019-11-17T18:47:40+00:00";
+ condition = hostPlatform.isLinux;
+ message = ''
+ A new module is available: 'services.lorri'.
+ '';
+ }
+
+ {
+ time = "2019-11-24T17:46:57+00:00";
+ condition = hostPlatform.isLinux;
+ message = ''
+ A new module is available: 'services.spotifyd'.
+ '';
+ }
+
+ {
+ time = "2019-11-29T21:18:48+00:00";
+ message = ''
+ A new module is available: 'programs.password-store'.
+ '';
+ }
+
+ {
+ time = "2019-11-29T21:18:48+00:00";
+ condition = hostPlatform.isLinux;
+ message = ''
+ A new module is available: 'services.password-store-sync'.
+ '';
+ }
+
+ {
+ time = "2019-11-29T22:46:49+00:00";
+ condition = hostPlatform.isLinux;
+ message = ''
+ A new module is available: 'services.unison'.
+ '';
+ }
+
+ {
+ time = "2019-12-01T22:10:23+00:00";
+ condition = hostPlatform.isLinux;
+ message = ''
+ A new module is available: 'xdg.mime'.
+
+ If enabled, which it is by default, this module will create
+ the XDG mime database and desktop file database caches from
+ programs installed via Home Manager.
+ '';
+ }
+
+ {
+ time = "2019-12-08T19:48:26+00:00";
+ message = ''
+ A new module is available: 'programs.readline'.
+ '';
+ }
+
+ {
+ time = "2020-01-11T11:49:51+00:00";
+ condition = hostPlatform.isLinux;
+ message = ''
+ A new module is available: 'services.cbatticon'.
+ '';
+ }
+
+ {
+ time = "2020-01-26T12:42:33+00:00";
+ condition = hostPlatform.isLinux;
+ message = ''
+ A new module is available: 'xsession.windowManager.bspwm'.
+ '';
+ }
+
+ {
+ time = "2020-01-26T12:49:40+00:00";
+ condition = hostPlatform.isLinux;
+ message = ''
+ A new module is available: 'services.grobi'.
+ '';
+ }
+
+ {
+ time = "2020-01-26T19:37:57+00:00";
+ message = ''
+ A new module is available: 'programs.neomutt'.
+ '';
+ }
+ ];
+ };
+}
diff --git a/home-manager/modules/misc/nixpkgs.nix b/home-manager/modules/misc/nixpkgs.nix
new file mode 100644
index 00000000000..7b0904a5f20
--- /dev/null
+++ b/home-manager/modules/misc/nixpkgs.nix
@@ -0,0 +1,152 @@
+# Adapted from Nixpkgs.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ isConfig = x:
+ builtins.isAttrs x || builtins.isFunction x;
+
+ optCall = f: x:
+ if builtins.isFunction f
+ then f x
+ else f;
+
+ mergeConfig = lhs_: rhs_:
+ let
+ lhs = optCall lhs_ { inherit pkgs; };
+ rhs = optCall rhs_ { inherit pkgs; };
+ in
+ lhs // rhs //
+ optionalAttrs (lhs ? packageOverrides) {
+ packageOverrides = pkgs:
+ optCall lhs.packageOverrides pkgs //
+ optCall (attrByPath ["packageOverrides"] ({}) rhs) pkgs;
+ } //
+ optionalAttrs (lhs ? perlPackageOverrides) {
+ perlPackageOverrides = pkgs:
+ optCall lhs.perlPackageOverrides pkgs //
+ optCall (attrByPath ["perlPackageOverrides"] ({}) rhs) pkgs;
+ };
+
+ configType = mkOptionType {
+ name = "nixpkgs-config";
+ description = "nixpkgs config";
+ check = x:
+ let traceXIfNot = c:
+ if c x then true
+ else lib.traceSeqN 1 x false;
+ in traceXIfNot isConfig;
+ merge = args: fold (def: mergeConfig def.value) {};
+ };
+
+ overlayType = mkOptionType {
+ name = "nixpkgs-overlay";
+ description = "nixpkgs overlay";
+ check = builtins.isFunction;
+ merge = lib.mergeOneOption;
+ };
+
+ _pkgs = import <nixpkgs> (
+ filterAttrs (n: v: v != null) config.nixpkgs
+ );
+
+in
+
+{
+ options.nixpkgs = {
+ config = mkOption {
+ default = null;
+ example = { allowBroken = true; };
+ type = types.nullOr configType;
+ description = ''
+ The configuration of the Nix Packages collection. (For
+ details, see the Nixpkgs documentation.) It allows you to set
+ package configuration options.
+
+ </para><para>
+
+ If <literal>null</literal>, then configuration is taken from
+ the fallback location, for example,
+ <filename>~/.config/nixpkgs/config.nix</filename>.
+
+ </para><para>
+
+ Note, this option will not apply outside your Home Manager
+ configuration like when installing manually through
+ <command>nix-env</command>. If you want to apply it both
+ inside and outside Home Manager you can put it in a separate
+ file and include something like
+
+ <programlisting language="nix">
+ nixpkgs.config = import ./nixpkgs-config.nix;
+ xdg.configFile."nixpkgs/config.nix".source = ./nixpkgs-config.nix;
+ </programlisting>
+
+ in your Home Manager configuration.
+ '';
+ };
+
+ overlays = mkOption {
+ default = null;
+ example = literalExample
+ ''
+ [ (self: super: {
+ openssh = super.openssh.override {
+ hpnSupport = true;
+ withKerberos = true;
+ kerberos = self.libkrb5;
+ };
+ };
+ ) ]
+ '';
+ type = types.nullOr (types.listOf overlayType);
+ description = ''
+ List of overlays to use with the Nix Packages collection. (For
+ details, see the Nixpkgs documentation.) It allows you to
+ override packages globally. This is a function that takes as
+ an argument the <emphasis>original</emphasis> Nixpkgs. The
+ first argument should be used for finding dependencies, and
+ the second should be used for overriding recipes.
+
+ </para><para>
+
+ If <literal>null</literal>, then the overlays are taken from
+ the fallback location, for example,
+ <filename>~/.config/nixpkgs/overlays</filename>.
+
+ </para><para>
+
+ Like <varname>nixpkgs.config</varname> this option only
+ applies within the Home Manager configuration. See
+ <varname>nixpkgs.config</varname> for a suggested setup that
+ works both internally and externally.
+ '';
+ };
+
+ system = mkOption {
+ type = types.str;
+ example = "i686-linux";
+ internal = true;
+ description = ''
+ Specifies the Nix platform type for which the user environment
+ should be built. If unset, it defaults to the platform type of
+ your host system. Specifying this option is useful when doing
+ distributed multi-platform deployment, or when building
+ virtual machines.
+ '';
+ };
+ };
+
+ config = {
+ _module.args = {
+ pkgs = mkOverride modules.defaultPriority _pkgs;
+ pkgs_i686 =
+ if _pkgs.stdenv.isLinux && _pkgs.stdenv.hostPlatform.isx86
+ then _pkgs.pkgsi686Linux
+ else { };
+ };
+ };
+}
diff --git a/home-manager/modules/misc/numlock.nix b/home-manager/modules/misc/numlock.nix
new file mode 100644
index 00000000000..199dd317daa
--- /dev/null
+++ b/home-manager/modules/misc/numlock.nix
@@ -0,0 +1,29 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.xsession.numlock;
+
+in {
+ options = { xsession.numlock.enable = mkEnableOption "Num Lock"; };
+
+ config = mkIf cfg.enable {
+ systemd.user.services.numlockx = {
+ Unit = {
+ Description = "NumLockX";
+ After = [ "graphical-session-pre.target" ];
+ PartOf = [ "graphical-session.target" ];
+ };
+
+ Service = {
+ Type = "oneshot";
+ RemainAfterExit = true;
+ ExecStart = "${pkgs.numlockx}/bin/numlockx";
+ };
+
+ Install = { WantedBy = [ "graphical-session.target" ]; };
+ };
+ };
+}
diff --git a/home-manager/modules/misc/pam.nix b/home-manager/modules/misc/pam.nix
new file mode 100644
index 00000000000..f54f4b95089
--- /dev/null
+++ b/home-manager/modules/misc/pam.nix
@@ -0,0 +1,32 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ vars = config.pam.sessionVariables;
+
+in {
+ meta.maintainers = [ maintainers.rycee ];
+
+ options = {
+ pam.sessionVariables = mkOption {
+ default = { };
+ type = types.attrs;
+ example = { EDITOR = "vim"; };
+ description = ''
+ Environment variables that will be set for the PAM session.
+ The variable values must be as described in
+ <citerefentry>
+ <refentrytitle>pam_env.conf</refentrytitle>
+ <manvolnum>5</manvolnum>
+ </citerefentry>.
+ '';
+ };
+ };
+
+ config = mkIf (vars != { }) {
+ home.file.".pam_environment".text = concatStringsSep "\n"
+ (mapAttrsToList (n: v: ''${n} OVERRIDE="${toString v}"'') vars) + "\n";
+ };
+}
diff --git a/home-manager/modules/misc/qt.nix b/home-manager/modules/misc/qt.nix
new file mode 100644
index 00000000000..ff38f842c81
--- /dev/null
+++ b/home-manager/modules/misc/qt.nix
@@ -0,0 +1,68 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.qt;
+
+in {
+ meta.maintainers = [ maintainers.rycee ];
+
+ imports = [
+ (mkChangedOptionModule [ "qt" "useGtkTheme" ] [ "qt" "platformTheme" ]
+ (config:
+ if getAttrFromPath [ "qt" "useGtkTheme" ] config then "gtk" else null))
+ ];
+
+ options = {
+ qt = {
+ enable = mkEnableOption "Qt 4 and 5 configuration";
+
+ platformTheme = mkOption {
+ type = types.nullOr (types.enum [ "gtk" "gnome" ]);
+ default = null;
+ example = "gnome";
+ relatedPackages =
+ [ "qgnomeplatform" [ "libsForQt5" "qtstyleplugins" ] ];
+ description = ''
+ Selects the platform theme to use for Qt applications.</para>
+ <para>The options are
+ <variablelist>
+ <varlistentry>
+ <term><literal>gtk</literal></term>
+ <listitem><para>Use GTK theme with
+ <link xlink:href="https://github.com/qt/qtstyleplugins">qtstyleplugins</link>
+ </para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><literal>gnome</literal></term>
+ <listitem><para>Use GNOME theme with
+ <link xlink:href="https://github.com/FedoraQt/QGnomePlatform">qgnomeplatform</link>
+ </para></listitem>
+ </varlistentry>
+ </variablelist>
+ '';
+ };
+ };
+ };
+
+ config = mkIf (cfg.enable && cfg.platformTheme != null) {
+ home.sessionVariables.QT_QPA_PLATFORMTHEME =
+ if cfg.platformTheme == "gnome" then "gnome" else "gtk2";
+
+ home.packages = if cfg.platformTheme == "gnome" then
+ [ pkgs.qgnomeplatform ]
+ else
+ [ pkgs.libsForQt5.qtstyleplugins ];
+
+ xsession.importedVariables = [ "QT_QPA_PLATFORMTHEME" ];
+
+ # Enable GTK+ style for Qt4 in either case.
+ # It doesn’t support the platform theme packages.
+ home.activation.useGtkThemeInQt4 = hm.dag.entryAfter [ "writeBoundary" ] ''
+ $DRY_RUN_CMD ${pkgs.crudini}/bin/crudini $VERBOSE_ARG \
+ --set "${config.xdg.configHome}/Trolltech.conf" Qt style GTK+
+ '';
+ };
+}
diff --git a/home-manager/modules/misc/submodule-support.nix b/home-manager/modules/misc/submodule-support.nix
new file mode 100644
index 00000000000..ff80291cadf
--- /dev/null
+++ b/home-manager/modules/misc/submodule-support.nix
@@ -0,0 +1,32 @@
+{ lib, ... }:
+
+with lib;
+
+{
+ meta.maintainers = [ maintainers.rycee ];
+
+ options.submoduleSupport = {
+ enable = mkOption {
+ type = types.bool;
+ default = false;
+ internal = true;
+ description = ''
+ Whether the Home Manager module system is used as a submodule
+ in, for example, NixOS or nix-darwin.
+ '';
+ };
+
+ externalPackageInstall = mkOption {
+ type = types.bool;
+ default = false;
+ internal = true;
+ description = ''
+ Whether the packages of <option>home.packages</option> are
+ installed separately from the Home Manager activation script.
+ In NixOS, for example, this may be accomplished by installing
+ the packages through
+ <option>users.users.‹name?›.packages</option>.
+ '';
+ };
+ };
+}
diff --git a/home-manager/modules/misc/version.nix b/home-manager/modules/misc/version.nix
new file mode 100644
index 00000000000..1352aadc614
--- /dev/null
+++ b/home-manager/modules/misc/version.nix
@@ -0,0 +1,24 @@
+{ config, lib, ... }:
+
+with lib;
+
+{
+ options = {
+ home.stateVersion = mkOption {
+ type = types.enum [ "18.09" "19.03" "19.09" "20.03" ];
+ default = "18.09";
+ description = ''
+ It is occasionally necessary for Home Manager to change
+ configuration defaults in a way that is incompatible with
+ stateful data. This could, for example, include switching the
+ default data format or location of a file.
+ </para><para>
+ The <emphasis>state version</emphasis> indicates which default
+ settings are in effect and will therefore help avoid breaking
+ program configurations. Switching to a higher state version
+ typically requires performing some manual steps, such as data
+ conversion or moving files.
+ '';
+ };
+ };
+}
diff --git a/home-manager/modules/misc/xdg-mime-apps.nix b/home-manager/modules/misc/xdg-mime-apps.nix
new file mode 100644
index 00000000000..7ba4083b3c0
--- /dev/null
+++ b/home-manager/modules/misc/xdg-mime-apps.nix
@@ -0,0 +1,88 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.xdg.mimeApps;
+
+ strListOrSingleton = with types;
+ coercedTo (either (listOf str) str) toList (listOf str);
+
+in {
+ meta.maintainers = with maintainers; [ pacien ];
+
+ options.xdg.mimeApps = {
+ enable = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Whether to manage <filename>$XDG_CONFIG_HOME/mimeapps.list</filename>.
+ </para>
+ <para>
+ The generated file is read-only.
+ '';
+ };
+
+ # descriptions from
+ # https://specifications.freedesktop.org/mime-apps-spec/mime-apps-spec-1.0.1.html
+
+ associations.added = mkOption {
+ type = types.attrsOf strListOrSingleton;
+ default = { };
+ example = literalExample ''
+ {
+ "mimetype1" = [ "foo1.desktop" "foo2.desktop" "foo3.desktop" ];
+ "mimetype2" = "foo4.desktop";
+ }
+ '';
+ description = ''
+ Defines additional associations of applications with
+ mimetypes, as if the .desktop file was listing this mimetype
+ in the first place.
+ '';
+ };
+
+ associations.removed = mkOption {
+ type = types.attrsOf strListOrSingleton;
+ default = { };
+ example = { "mimetype1" = "foo5.desktop"; };
+ description = ''
+ Removes associations of applications with mimetypes, as if the
+ .desktop file was <emphasis>not</emphasis> listing this
+ mimetype in the first place.
+ '';
+ };
+
+ defaultApplications = mkOption {
+ type = types.attrsOf strListOrSingleton;
+ default = { };
+ example = literalExample ''
+ {
+ "mimetype1" = [ "default1.desktop" "default2.desktop" ];
+ }
+ '';
+ description = ''
+ The default application to be used for a given mimetype. This
+ is, for instance, the one that will be started when
+ double-clicking on a file in a file manager. If the
+ application is no longer installed, the next application in
+ the list is attempted, and so on.
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+ # Deprecated but still used by some applications.
+ home.file.".local/share/applications/mimeapps.list".source =
+ config.xdg.configFile."mimeapps.list".source;
+
+ xdg.configFile."mimeapps.list".text =
+ let joinValues = mapAttrs (n: concatStringsSep ";");
+ in generators.toINI { } {
+ "Added Associations" = joinValues cfg.associations.added;
+ "Removed Associations" = joinValues cfg.associations.removed;
+ "Default Applications" = joinValues cfg.defaultApplications;
+ };
+ };
+}
diff --git a/home-manager/modules/misc/xdg-mime.nix b/home-manager/modules/misc/xdg-mime.nix
new file mode 100644
index 00000000000..32006e025ff
--- /dev/null
+++ b/home-manager/modules/misc/xdg-mime.nix
@@ -0,0 +1,47 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.xdg.mime;
+
+in {
+ options = {
+ xdg.mime.enable = mkOption {
+ type = types.bool;
+ default = true;
+ description = ''
+ Whether to install programs and files to support the
+ XDG Shared MIME-info specification and XDG MIME Applications
+ specification at
+ <link xlink:href="https://specifications.freedesktop.org/shared-mime-info-spec/shared-mime-info-spec-latest.html"/>
+ and
+ <link xlink:href="https://specifications.freedesktop.org/mime-apps-spec/mime-apps-spec-latest.html"/>,
+ respectively.
+ '';
+ };
+ };
+
+ config = mkIf config.xdg.mime.enable {
+ home.packages = [
+ # Explicitly install package to provide basic mime types.
+ pkgs.shared-mime-info
+ ];
+
+ home.extraProfileCommands = ''
+ if [[ -w $out/share/mime && -d $out/share/mime/packages ]]; then
+ XDG_DATA_DIRS=$out/share \
+ PKGSYSTEM_ENABLE_FSYNC=0 \
+ ${pkgs.buildPackages.shared-mime-info}/bin/update-mime-database \
+ -V $out/share/mime > /dev/null
+ fi
+
+ if [[ -w $out/share/applications ]]; then
+ ${pkgs.buildPackages.desktop-file-utils}/bin/update-desktop-database \
+ $out/share/applications
+ fi
+ '';
+ };
+
+}
diff --git a/home-manager/modules/misc/xdg-user-dirs.nix b/home-manager/modules/misc/xdg-user-dirs.nix
new file mode 100644
index 00000000000..da9d3c43ad9
--- /dev/null
+++ b/home-manager/modules/misc/xdg-user-dirs.nix
@@ -0,0 +1,103 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.xdg.userDirs;
+
+in {
+ meta.maintainers = with maintainers; [ pacien ];
+
+ imports = [
+ (mkRenamedOptionModule [ "xdg" "userDirs" "publishShare" ] [
+ "xdg"
+ "userDirs"
+ "publicShare"
+ ])
+ ];
+
+ options.xdg.userDirs = {
+ enable = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Whether to manage <filename>$XDG_CONFIG_HOME/user-dirs.dirs</filename>.
+ </para>
+ <para>
+ The generated file is read-only.
+ '';
+ };
+
+ # Well-known directory list from
+ # https://gitlab.freedesktop.org/xdg/xdg-user-dirs/blob/master/man/user-dirs.dirs.xml
+
+ desktop = mkOption {
+ type = types.str;
+ default = "$HOME/Desktop";
+ description = "The Desktop directory.";
+ };
+
+ documents = mkOption {
+ type = types.str;
+ default = "$HOME/Documents";
+ description = "The Documents directory.";
+ };
+
+ download = mkOption {
+ type = types.str;
+ default = "$HOME/Downloads";
+ description = "The Downloads directory.";
+ };
+
+ music = mkOption {
+ type = types.str;
+ default = "$HOME/Music";
+ description = "The Music directory.";
+ };
+
+ pictures = mkOption {
+ type = types.str;
+ default = "$HOME/Pictures";
+ description = "The Pictures directory.";
+ };
+
+ publicShare = mkOption {
+ type = types.str;
+ default = "$HOME/Public";
+ description = "The Public share directory.";
+ };
+
+ templates = mkOption {
+ type = types.str;
+ default = "$HOME/Templates";
+ description = "The Templates directory.";
+ };
+
+ videos = mkOption {
+ type = types.str;
+ default = "$HOME/Videos";
+ description = "The Videos directory.";
+ };
+
+ extraConfig = mkOption {
+ type = with types; attrsOf str;
+ default = { };
+ example = { XDG_MISC_DIR = "$HOME/Misc"; };
+ description = "Other user directories.";
+ };
+ };
+
+ config = mkIf cfg.enable {
+ xdg.configFile."user-dirs.dirs".text = generators.toKeyValue { } ({
+ XDG_DESKTOP_DIR = cfg.desktop;
+ XDG_DOCUMENTS_DIR = cfg.documents;
+ XDG_DOWNLOAD_DIR = cfg.download;
+ XDG_MUSIC_DIR = cfg.music;
+ XDG_PICTURES_DIR = cfg.pictures;
+ XDG_PUBLICSHARE_DIR = cfg.publicShare;
+ XDG_TEMPLATES_DIR = cfg.templates;
+ XDG_VIDEOS_DIR = cfg.videos;
+ } // cfg.extraConfig);
+ };
+}
diff --git a/home-manager/modules/misc/xdg.nix b/home-manager/modules/misc/xdg.nix
new file mode 100644
index 00000000000..84ab4ada59a
--- /dev/null
+++ b/home-manager/modules/misc/xdg.nix
@@ -0,0 +1,104 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.xdg;
+
+ dag = config.lib.dag;
+
+ fileType = (import ../lib/file-type.nix {
+ inherit (config.home) homeDirectory;
+ inherit lib pkgs;
+ }).fileType;
+
+ defaultCacheHome = "${config.home.homeDirectory}/.cache";
+ defaultConfigHome = "${config.home.homeDirectory}/.config";
+ defaultDataHome = "${config.home.homeDirectory}/.local/share";
+
+ getXdgDir = name: fallback:
+ let
+ value = builtins.getEnv name;
+ in
+ if value != "" then value else fallback;
+
+in
+
+{
+ options.xdg = {
+ enable = mkEnableOption "management of XDG base directories";
+
+ cacheHome = mkOption {
+ type = types.path;
+ defaultText = "~/.cache";
+ description = ''
+ Absolute path to directory holding application caches.
+ '';
+ };
+
+ configFile = mkOption {
+ type = fileType "<varname>xdg.configHome</varname>" cfg.configHome;
+ default = {};
+ description = ''
+ Attribute set of files to link into the user's XDG
+ configuration home.
+ '';
+ };
+
+ configHome = mkOption {
+ type = types.path;
+ defaultText = "~/.config";
+ description = ''
+ Absolute path to directory holding application configurations.
+ '';
+ };
+
+ dataFile = mkOption {
+ type = fileType "<varname>xdg.dataHome</varname>" cfg.dataHome;
+ default = {};
+ description = ''
+ Attribute set of files to link into the user's XDG
+ data home.
+ '';
+ };
+
+ dataHome = mkOption {
+ type = types.path;
+ defaultText = "~/.local/share";
+ description = ''
+ Absolute path to directory holding application data.
+ '';
+ };
+ };
+
+ config = mkMerge [
+ (mkIf cfg.enable {
+ xdg.cacheHome = mkDefault defaultCacheHome;
+ xdg.configHome = mkDefault defaultConfigHome;
+ xdg.dataHome = mkDefault defaultDataHome;
+
+ home.sessionVariables = {
+ XDG_CACHE_HOME = cfg.cacheHome;
+ XDG_CONFIG_HOME = cfg.configHome;
+ XDG_DATA_HOME = cfg.dataHome;
+ };
+ })
+
+ (mkIf (!cfg.enable) {
+ xdg.cacheHome = getXdgDir "XDG_CACHE_HOME" defaultCacheHome;
+ xdg.configHome = getXdgDir "XDG_CONFIG_HOME" defaultConfigHome;
+ xdg.dataHome = getXdgDir "XDG_DATA_HOME" defaultDataHome;
+ })
+
+ {
+ home.file = mkMerge [
+ cfg.configFile
+ cfg.dataFile
+ {
+ "${config.xdg.cacheHome}/.keep".text = "";
+ }
+ ];
+ }
+ ];
+}
diff --git a/home-manager/modules/modules.nix b/home-manager/modules/modules.nix
new file mode 100644
index 00000000000..64418dbae11
--- /dev/null
+++ b/home-manager/modules/modules.nix
@@ -0,0 +1,181 @@
+{ pkgs
+
+ # Note, this should be "the standard library" + HM extensions.
+, lib
+
+ # Whether to enable module type checking.
+, check ? true
+}:
+
+with lib;
+
+let
+
+ hostPlatform = pkgs.stdenv.hostPlatform;
+
+ checkPlatform = any (meta.platformMatch pkgs.stdenv.hostPlatform);
+
+ loadModule = file: { condition ? true }: {
+ inherit file condition;
+ };
+
+ allModules = [
+ (loadModule ./accounts/email.nix { })
+ (loadModule ./files.nix { })
+ (loadModule ./home-environment.nix { })
+ (loadModule ./manual.nix { })
+ (loadModule ./misc/dconf.nix { })
+ (loadModule ./misc/fontconfig.nix { })
+ (loadModule ./misc/gtk.nix { })
+ (loadModule ./misc/lib.nix { })
+ (loadModule ./misc/news.nix { })
+ (loadModule ./misc/nixpkgs.nix { })
+ (loadModule ./misc/numlock.nix { condition = hostPlatform.isLinux; })
+ (loadModule ./misc/pam.nix { })
+ (loadModule ./misc/qt.nix { })
+ (loadModule ./misc/submodule-support.nix { })
+ (loadModule ./misc/version.nix { })
+ (loadModule ./misc/xdg-mime.nix { condition = hostPlatform.isLinux; })
+ (loadModule ./misc/xdg-mime-apps.nix { condition = hostPlatform.isLinux; })
+ (loadModule ./misc/xdg-user-dirs.nix { condition = hostPlatform.isLinux; })
+ (loadModule ./misc/xdg.nix { })
+ (loadModule ./programs/afew.nix { })
+ (loadModule ./programs/alacritty.nix { })
+ (loadModule ./programs/alot.nix { })
+ (loadModule ./programs/astroid.nix { })
+ (loadModule ./programs/autorandr.nix { })
+ (loadModule ./programs/bash.nix { })
+ (loadModule ./programs/bat.nix { })
+ (loadModule ./programs/beets.nix { })
+ (loadModule ./programs/broot.nix { })
+ (loadModule ./programs/browserpass.nix { })
+ (loadModule ./programs/chromium.nix { condition = hostPlatform.isLinux; })
+ (loadModule ./programs/command-not-found/command-not-found.nix { })
+ (loadModule ./programs/direnv.nix { })
+ (loadModule ./programs/eclipse.nix { })
+ (loadModule ./programs/emacs.nix { })
+ (loadModule ./programs/feh.nix { })
+ (loadModule ./programs/firefox.nix { })
+ (loadModule ./programs/fish.nix { })
+ (loadModule ./programs/fzf.nix { })
+ (loadModule ./programs/getmail.nix { condition = hostPlatform.isLinux; })
+ (loadModule ./programs/git.nix { })
+ (loadModule ./programs/gnome-terminal.nix { })
+ (loadModule ./programs/go.nix { })
+ (loadModule ./programs/gpg.nix { })
+ (loadModule ./programs/home-manager.nix { })
+ (loadModule ./programs/htop.nix { })
+ (loadModule ./programs/info.nix { })
+ (loadModule ./programs/irssi.nix { })
+ (loadModule ./programs/jq.nix { })
+ (loadModule ./programs/kakoune.nix { })
+ (loadModule ./programs/keychain.nix { })
+ (loadModule ./programs/lesspipe.nix { })
+ (loadModule ./programs/lsd.nix { })
+ (loadModule ./programs/man.nix { })
+ (loadModule ./programs/matplotlib.nix { })
+ (loadModule ./programs/mbsync.nix { })
+ (loadModule ./programs/mercurial.nix { })
+ (loadModule ./programs/mpv.nix { })
+ (loadModule ./programs/msmtp.nix { })
+ (loadModule ./programs/neomutt.nix { })
+ (loadModule ./programs/neovim.nix { })
+ (loadModule ./programs/newsboat.nix { })
+ (loadModule ./programs/noti.nix { })
+ (loadModule ./programs/notmuch.nix { })
+ (loadModule ./programs/obs-studio.nix { })
+ (loadModule ./programs/offlineimap.nix { })
+ (loadModule ./programs/opam.nix { })
+ (loadModule ./programs/password-store.nix { })
+ (loadModule ./programs/pazi.nix { })
+ (loadModule ./programs/pidgin.nix { })
+ (loadModule ./programs/readline.nix { })
+ (loadModule ./programs/rofi.nix { })
+ (loadModule ./programs/rtorrent.nix { })
+ (loadModule ./programs/skim.nix { })
+ (loadModule ./programs/starship.nix { })
+ (loadModule ./programs/ssh.nix { })
+ (loadModule ./programs/taskwarrior.nix { })
+ (loadModule ./programs/termite.nix { })
+ (loadModule ./programs/texlive.nix { })
+ (loadModule ./programs/tmux.nix { })
+ (loadModule ./programs/urxvt.nix { })
+ (loadModule ./programs/vim.nix { })
+ (loadModule ./programs/vscode.nix { })
+ (loadModule ./programs/vscode/haskell.nix { })
+ (loadModule ./programs/z-lua.nix { })
+ (loadModule ./programs/zathura.nix { })
+ (loadModule ./programs/zsh.nix { })
+ (loadModule ./services/blueman-applet.nix { })
+ (loadModule ./services/cbatticon.nix { condition = hostPlatform.isLinux; })
+ (loadModule ./services/compton.nix { })
+ (loadModule ./services/dunst.nix { })
+ (loadModule ./services/dwm-status.nix { condition = hostPlatform.isLinux; })
+ (loadModule ./services/emacs.nix { condition = hostPlatform.isLinux; })
+ (loadModule ./services/flameshot.nix { })
+ (loadModule ./services/getmail.nix { condition = hostPlatform.isLinux; })
+ (loadModule ./services/gnome-keyring.nix { })
+ (loadModule ./services/gpg-agent.nix { })
+ (loadModule ./services/grobi.nix { condition = hostPlatform.isLinux; })
+ (loadModule ./services/hound.nix { condition = hostPlatform.isLinux; })
+ (loadModule ./services/imapnotify.nix { condition = hostPlatform.isLinux; })
+ (loadModule ./services/kbfs.nix { })
+ (loadModule ./services/kdeconnect.nix { })
+ (loadModule ./services/keepassx.nix { })
+ (loadModule ./services/keybase.nix { })
+ (loadModule ./services/lorri.nix { condition = hostPlatform.isLinux; })
+ (loadModule ./services/mbsync.nix { })
+ (loadModule ./services/mpd.nix { })
+ (loadModule ./services/mpdris2.nix { condition = hostPlatform.isLinux; })
+ (loadModule ./services/muchsync.nix { condition = hostPlatform.isLinux; })
+ (loadModule ./services/network-manager-applet.nix { })
+ (loadModule ./services/nextcloud-client.nix { })
+ (loadModule ./services/owncloud-client.nix { })
+ (loadModule ./services/parcellite.nix { })
+ (loadModule ./services/password-store-sync.nix { condition = hostPlatform.isLinux; })
+ (loadModule ./services/pasystray.nix { })
+ (loadModule ./services/polybar.nix { })
+ (loadModule ./services/random-background.nix { })
+ (loadModule ./services/redshift.nix { })
+ (loadModule ./services/rsibreak.nix { condition = hostPlatform.isLinux; })
+ (loadModule ./services/screen-locker.nix { })
+ (loadModule ./services/stalonetray.nix { })
+ (loadModule ./services/status-notifier-watcher.nix { })
+ (loadModule ./services/spotifyd.nix { condition = hostPlatform.isLinux; })
+ (loadModule ./services/sxhkd.nix { condition = hostPlatform.isLinux; })
+ (loadModule ./services/syncthing.nix { })
+ (loadModule ./services/taffybar.nix { })
+ (loadModule ./services/tahoe-lafs.nix { })
+ (loadModule ./services/taskwarrior-sync.nix { condition = hostPlatform.isLinux; })
+ (loadModule ./services/udiskie.nix { })
+ (loadModule ./services/unclutter.nix { })
+ (loadModule ./services/unison.nix { condition = hostPlatform.isLinux; })
+ (loadModule ./services/window-managers/awesome.nix { })
+ (loadModule ./services/window-managers/bspwm/default.nix { condition = hostPlatform.isLinux; })
+ (loadModule ./services/window-managers/i3.nix { })
+ (loadModule ./services/window-managers/xmonad.nix { })
+ (loadModule ./services/xcape.nix { condition = hostPlatform.isLinux; })
+ (loadModule ./services/xembed-sni-proxy.nix { condition = hostPlatform.isLinux; })
+ (loadModule ./services/xscreensaver.nix { })
+ (loadModule ./services/xsuspender.nix { condition = hostPlatform.isLinux; })
+ (loadModule ./systemd.nix { })
+ (loadModule ./xcursor.nix { })
+ (loadModule ./xresources.nix { })
+ (loadModule ./xsession.nix { })
+ (loadModule (pkgs.path + "/nixos/modules/misc/assertions.nix") { })
+ (loadModule (pkgs.path + "/nixos/modules/misc/meta.nix") { })
+ ];
+
+ modules = map (getAttr "file") (filter (getAttr "condition") allModules);
+
+ pkgsModule = {
+ config._module.args.baseModules = modules;
+ config._module.args.pkgs = lib.mkDefault pkgs;
+ config._module.check = check;
+ config.lib = lib.hm;
+ config.nixpkgs.system = mkDefault pkgs.system;
+ };
+
+in
+
+ modules ++ [ pkgsModule ]
diff --git a/home-manager/modules/programs/afew.nix b/home-manager/modules/programs/afew.nix
new file mode 100644
index 00000000000..99bae88c0ee
--- /dev/null
+++ b/home-manager/modules/programs/afew.nix
@@ -0,0 +1,52 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.afew;
+
+in
+
+{
+ options.programs.afew = {
+ enable = mkEnableOption "the afew initial tagging script for Notmuch";
+
+ extraConfig = mkOption {
+ type = types.lines;
+ default = ''
+ [SpamFilter]
+ [KillThreadsFilter]
+ [ListMailsFilter]
+ [ArchiveSentMailsFilter]
+ [InboxFilter]
+ '';
+ example = ''
+ [SpamFilter]
+
+ [Filter.0]
+ query = from:pointyheaded@boss.com
+ tags = -new;+boss
+ message = Message from above
+
+ [InboxFilter]
+ '';
+ description = ''
+ Extra lines added to afew configuration file. Available
+ configuration options are described in the afew manual:
+ <link xlink:href="https://afew.readthedocs.io/en/latest/configuration.html" />.
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ pkgs.afew ];
+
+ xdg.configFile."afew/config".text = ''
+ # Generated by Home Manager.
+ # See https://afew.readthedocs.io/
+
+ ${cfg.extraConfig}
+ '';
+ };
+}
diff --git a/home-manager/modules/programs/alacritty.nix b/home-manager/modules/programs/alacritty.nix
new file mode 100644
index 00000000000..69b9ea9673d
--- /dev/null
+++ b/home-manager/modules/programs/alacritty.nix
@@ -0,0 +1,52 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.alacritty;
+
+in {
+ options = {
+ programs.alacritty = {
+ enable = mkEnableOption "Alacritty";
+
+ settings = mkOption {
+ type = types.attrs;
+ default = { };
+ example = literalExample ''
+ {
+ window.dimensions = {
+ lines = 3;
+ columns = 200;
+ };
+ key_bindings = [
+ {
+ key = "K";
+ mods = "Control";
+ chars = "\\x0c";
+ }
+ ];
+ }
+ '';
+ description = ''
+ Configuration written to
+ <filename>~/.config/alacritty/alacritty.yml</filename>. See
+ <link xlink:href="https://github.com/jwilm/alacritty/blob/master/alacritty.yml"/>
+ for the default configuration.
+ '';
+ };
+ };
+ };
+
+ config = mkMerge [
+ (mkIf cfg.enable {
+ home.packages = [ pkgs.alacritty ];
+
+ xdg.configFile."alacritty/alacritty.yml" = mkIf (cfg.settings != { }) {
+ text =
+ replaceStrings [ "\\\\" ] [ "\\" ] (builtins.toJSON cfg.settings);
+ };
+ })
+ ];
+}
diff --git a/home-manager/modules/programs/alot-accounts.nix b/home-manager/modules/programs/alot-accounts.nix
new file mode 100644
index 00000000000..89ae28f9c8e
--- /dev/null
+++ b/home-manager/modules/programs/alot-accounts.nix
@@ -0,0 +1,58 @@
+pkgs:
+{ config, lib, ... }:
+
+with lib;
+
+{
+ options.alot = {
+ sendMailCommand = mkOption {
+ type = types.nullOr types.str;
+ description = ''
+ Command to send a mail. If msmtp is enabled for the account,
+ then this is set to
+ <command>msmtpq --read-envelope-from --read-recipients</command>.
+ '';
+ };
+
+ contactCompletion = mkOption {
+ type = types.attrsOf types.str;
+ default = {
+ type = "shellcommand";
+ command =
+ "'${pkgs.notmuch}/bin/notmuch address --format=json --output=recipients date:6M..'";
+ regexp = "'\\[?{" + ''
+ "name": "(?P<name>.*)", "address": "(?P<email>.+)", "name-addr": ".*"''
+ + "}[,\\]]?'";
+ shellcommand_external_filtering = "False";
+ };
+ example = literalExample ''
+ {
+ type = "shellcommand";
+ command = "abook --mutt-query";
+ regexp = "'^(?P<email>[^@]+@[^\t]+)\t+(?P<name>[^\t]+)'";
+ ignorecase = "True";
+ }
+ '';
+ description = ''
+ Contact completion configuration as expected per alot.
+ See <link xlink:href="http://alot.readthedocs.io/en/latest/configuration/contacts_completion.html">alot's wiki</link> for
+ explanation about possible values.
+ '';
+ };
+
+ extraConfig = mkOption {
+ type = types.lines;
+ default = "";
+ description = ''
+ Extra settings to add to this Alot account configuration.
+ '';
+ };
+ };
+
+ config = mkIf config.notmuch.enable {
+ alot.sendMailCommand = mkOptionDefault (if config.msmtp.enable then
+ "msmtpq --read-envelope-from --read-recipients"
+ else
+ null);
+ };
+}
diff --git a/home-manager/modules/programs/alot.nix b/home-manager/modules/programs/alot.nix
new file mode 100644
index 00000000000..2b28f34caa3
--- /dev/null
+++ b/home-manager/modules/programs/alot.nix
@@ -0,0 +1,173 @@
+# alot config loader is sensitive to leading space !
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.alot;
+
+ alotAccounts = filter (a: a.notmuch.enable)
+ (attrValues config.accounts.email.accounts);
+
+ boolStr = v: if v then "True" else "False";
+
+ accountStr = account: with account;
+ concatStringsSep "\n" (
+ [ "[[${name}]]" ]
+ ++ mapAttrsToList (n: v: n + "=" + v) (
+ {
+ address = address;
+ realname = realName;
+ sendmail_command =
+ optionalString (alot.sendMailCommand != null) alot.sendMailCommand;
+ sent_box = "maildir" + "://" + maildir.absPath + "/" + folders.sent;
+ draft_box = "maildir" + "://"+ maildir.absPath + "/" + folders.drafts;
+ }
+ // optionalAttrs (aliases != []) {
+ aliases = concatStringsSep "," aliases;
+ }
+ // optionalAttrs (gpg != null) {
+ gpg_key = gpg.key;
+ encrypt_by_default = if gpg.encryptByDefault then "all" else "none";
+ sign_by_default = boolStr gpg.signByDefault;
+ }
+ // optionalAttrs (signature.showSignature != "none") {
+ signature = pkgs.writeText "signature.txt" signature.text;
+ signature_as_attachment =
+ boolStr (signature.showSignature == "attach");
+ }
+ )
+ ++ [ alot.extraConfig ]
+ ++ [ "[[[abook]]]" ]
+ ++ mapAttrsToList (n: v: n + "=" + v) alot.contactCompletion
+ );
+
+ configFile =
+ let
+ bindingsToStr = attrSet:
+ concatStringsSep "\n" (mapAttrsToList (n: v: "${n} = ${v}") attrSet);
+ in
+ ''
+ # Generated by Home Manager.
+ # See http://alot.readthedocs.io/en/latest/configuration/config_options.html
+
+ ${cfg.extraConfig}
+
+ [bindings]
+ ${bindingsToStr cfg.bindings.global}
+
+ [[bufferlist]]
+ ${bindingsToStr cfg.bindings.bufferlist}
+ [[search]]
+ ${bindingsToStr cfg.bindings.search}
+ [[envelope]]
+ ${bindingsToStr cfg.bindings.envelope}
+ [[taglist]]
+ ${bindingsToStr cfg.bindings.taglist}
+ [[thread]]
+ ${bindingsToStr cfg.bindings.thread}
+
+ [accounts]
+
+ ${concatStringsSep "\n\n" (map accountStr alotAccounts)}
+ '';
+
+in
+
+{
+ options.programs.alot = {
+ enable = mkOption {
+ type = types.bool;
+ default = false;
+ example = true;
+ description = ''
+ Whether to enable the Alot mail user agent. Alot uses the
+ Notmuch email system and will therefore be automatically
+ enabled for each email account that is managed by Notmuch.
+ '';
+ };
+
+ hooks = mkOption {
+ type = types.lines;
+ default = "";
+ description = ''
+ Content of the hooks file.
+ '';
+ };
+
+ bindings = mkOption {
+ type = types.submodule {
+ options = {
+ global = mkOption {
+ type = types.attrsOf types.str;
+ default = {};
+ description = "Global keybindings.";
+ };
+
+ bufferlist = mkOption {
+ type = types.attrsOf types.str;
+ default = {};
+ description = "Bufferlist mode keybindings.";
+ };
+
+ search = mkOption {
+ type = types.attrsOf types.str;
+ default = {};
+ description = "Search mode keybindings.";
+ };
+
+ envelope = mkOption {
+ type = types.attrsOf types.str;
+ default = {};
+ description = "Envelope mode keybindings.";
+ };
+
+ taglist = mkOption {
+ type = types.attrsOf types.str;
+ default = {};
+ description = "Taglist mode keybindings.";
+ };
+
+ thread = mkOption {
+ type = types.attrsOf types.str;
+ default = {};
+ description = "Thread mode keybindings.";
+ };
+ };
+ };
+ default = {};
+ description = ''
+ Keybindings.
+ '';
+ };
+
+ extraConfig = mkOption {
+ type = types.lines;
+ default = ''
+ auto_remove_unread = True
+ ask_subject = False
+ handle_mouse = True
+ initial_command = "search tag:inbox AND NOT tag:killed"
+ input_timeout = 0.3
+ prefer_plaintext = True
+ thread_indent_replies = 4
+ '';
+ description = ''
+ Extra lines added to alot configuration file.
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ pkgs.alot ];
+
+ xdg.configFile."alot/config".text = configFile;
+
+ xdg.configFile."alot/hooks.py".text =
+ ''
+ # Generated by Home Manager.
+ ''
+ + cfg.hooks;
+ };
+}
diff --git a/home-manager/modules/programs/astroid-accounts.nix b/home-manager/modules/programs/astroid-accounts.nix
new file mode 100644
index 00000000000..17544ff7899
--- /dev/null
+++ b/home-manager/modules/programs/astroid-accounts.nix
@@ -0,0 +1,32 @@
+{ config, lib, ... }:
+
+with lib;
+
+{
+ options.astroid = {
+ enable = mkEnableOption "Astroid";
+
+ sendMailCommand = mkOption {
+ type = types.str;
+ description = ''
+ Command to send a mail. If msmtp is enabled for the account,
+ then this is set to
+ <command>msmtpq --read-envelope-from --read-recipients</command>.
+ '';
+ };
+
+ extraConfig = mkOption {
+ type = types.attrs;
+ default = { };
+ example = { select_query = ""; };
+ description = ''
+ Extra settings to add to this astroid account configuration.
+ '';
+ };
+ };
+
+ config = mkIf config.notmuch.enable {
+ astroid.sendMailCommand = mkIf config.msmtp.enable
+ (mkOptionDefault "msmtpq --read-envelope-from --read-recipients");
+ };
+}
diff --git a/home-manager/modules/programs/astroid-config-template.json b/home-manager/modules/programs/astroid-config-template.json
new file mode 100644
index 00000000000..87e3f764f9c
--- /dev/null
+++ b/home-manager/modules/programs/astroid-config-template.json
@@ -0,0 +1,113 @@
+{
+ "astroid": {
+ "config": {
+ "version": "11"
+ },
+ "debug": {
+ "dryrun_sending": "false"
+ },
+ "hints": {
+ "level": "0"
+ },
+ "log": {
+ "syslog": "false",
+ "stdout": "true",
+ "level": "info"
+ }
+ },
+ "startup": {
+ "queries": {
+ "inbox": "tag:inbox"
+ }
+ },
+ "terminal": {
+ "height": "10",
+ "font_description": "default"
+ },
+ "thread_index": {
+ "page_jump_rows": "6",
+ "sort_order": "newest",
+ "cell": {
+ "font_description": "default",
+ "line_spacing": "2",
+ "date_length": "10",
+ "message_count_length": "4",
+ "authors_length": "20",
+ "subject_color": "#807d74",
+ "subject_color_selected": "#000000",
+ "background_color_selected": "",
+ "background_color_marked": "#fff584",
+ "background_color_marked_selected": "#bcb559",
+ "tags_length": "80",
+ "tags_upper_color": "#e5e5e5",
+ "tags_lower_color": "#333333",
+ "tags_alpha": "0.5",
+ "hidden_tags": "attachment,flagged,unread"
+ }
+ },
+ "general": {
+ "time": {
+ "clock_format": "local",
+ "same_year": "%b %-e",
+ "diff_year": "%x"
+ }
+ },
+ "editor": {
+ "charset": "utf-8",
+ "save_draft_on_force_quit": "true",
+ "attachment_words": "attach",
+ "attachment_directory": "~",
+ "markdown_processor": "marked"
+ },
+ "mail": {
+ "reply": {
+ "quote_line": "Excerpts from %1's message of %2:",
+ "mailinglist_reply_to_sender": "true"
+ },
+ "forward": {
+ "quote_line": "Forwarding %1's message of %2:",
+ "disposition": "inline"
+ },
+ "sent_tags": "sent",
+ "message_id_fqdn": "",
+ "message_id_user": "",
+ "user_agent": "default",
+ "send_delay": "2",
+ "close_on_success": "false",
+ "format_flowed": "false"
+ },
+ "poll": {
+ "interval": "60",
+ "always_full_refresh": "false"
+ },
+ "attachment": {
+ "external_open_cmd": "xdg-open"
+ },
+ "thread_view": {
+ "open_html_part_external": "false",
+ "preferred_type": "plain",
+ "preferred_html_only": "false",
+ "allow_remote_when_encrypted": "false",
+ "open_external_link": "xdg-open",
+ "default_save_directory": "~",
+ "indent_messages": "false",
+ "gravatar": {
+ "enable": "true"
+ },
+ "mark_unread_delay": "0.5",
+ "expand_flagged": "true"
+ },
+ "crypto": {
+ "gpg": {
+ "path": "gpg2",
+ "always_trust": "true",
+ "enabled": "true"
+ }
+ },
+ "saved_searches": {
+ "show_on_startup": "false",
+ "save_history": "true",
+ "history_lines_to_show": "15",
+ "history_lines": "1000"
+ }
+}
diff --git a/home-manager/modules/programs/astroid.nix b/home-manager/modules/programs/astroid.nix
new file mode 100644
index 00000000000..8b3762fac0b
--- /dev/null
+++ b/home-manager/modules/programs/astroid.nix
@@ -0,0 +1,123 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+with builtins;
+
+let
+
+ cfg = config.programs.astroid;
+
+ astroidAccounts =
+ filterAttrs (n: v: v.astroid.enable) config.accounts.email.accounts;
+
+ boolOpt = b: if b then "true" else "false";
+
+ accountAttr = account:
+ with account;
+ {
+ email = address;
+ name = realName;
+ sendmail = astroid.sendMailCommand;
+ additional_sent_tags = "";
+ default = boolOpt primary;
+ save_drafts_to = "${maildir.absPath}/${folders.drafts}";
+ save_sent = "true";
+ save_sent_to = "${maildir.absPath}/${folders.sent}";
+ select_query = "";
+ } // optionalAttrs (signature.showSignature != "none") {
+ signature_attach = boolOpt (signature.showSignature == "attach");
+ signature_default_on = boolOpt (signature.showSignature != "none");
+ signature_file = pkgs.writeText "signature.txt" signature.text;
+ signature_file_markdown = "false";
+ signature_separate = "true"; # prepends '--\n' to the signature
+ } // optionalAttrs (gpg != null) {
+ always_gpg_sign = boolOpt gpg.signByDefault;
+ gpgkey = gpg.key;
+ } // astroid.extraConfig;
+
+ # See https://github.com/astroidmail/astroid/wiki/Configuration-Reference
+ configFile = mailAccounts:
+ let
+ template = fromJSON (readFile ./astroid-config-template.json);
+ astroidConfig = foldl' recursiveUpdate template [
+ {
+ astroid.notmuch_config = "${config.xdg.configHome}/notmuch/notmuchrc";
+ accounts = mapAttrs (n: accountAttr) astroidAccounts;
+ crypto.gpg.path = "${pkgs.gnupg}/bin/gpg";
+ }
+ cfg.extraConfig
+ cfg.externalEditor
+ ];
+ in builtins.toJSON astroidConfig;
+
+in {
+ options = {
+ programs.astroid = {
+ enable = mkEnableOption "Astroid";
+
+ pollScript = mkOption {
+ type = types.str;
+ default = "";
+ example = "mbsync gmail";
+ description = ''
+ Script to run to fetch/update mails.
+ '';
+ };
+
+ externalEditor = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ # Converts it into JSON that can be merged into the configuration.
+ apply = cmd:
+ optionalAttrs (cmd != null) {
+ editor = {
+ "external_editor" = "true";
+ "cmd" = cmd;
+ };
+ };
+ example =
+ "nvim-qt -- -c 'set ft=mail' '+set fileencoding=utf-8' '+set ff=unix' '+set enc=utf-8' '+set fo+=w' %1";
+ description = ''
+ You can use <code>%1</code>, <code>%2</code>, and
+ <code>%3</code> to refer respectively to:
+ <orderedlist numeration="arabic">
+ <listitem><para>file name</para></listitem>
+ <listitem><para>server name</para></listitem>
+ <listitem><para>socket ID</para></listitem>
+ </orderedlist>
+ See <link xlink:href='https://github.com/astroidmail/astroid/wiki/Customizing-editor' />.
+ '';
+ };
+
+ extraConfig = mkOption {
+ type = types.attrs;
+ default = { };
+ example = { poll.interval = 0; };
+ description = ''
+ JSON config that will override the default Astroid configuration.
+ '';
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ pkgs.astroid ];
+
+ xdg.configFile."astroid/config".source = pkgs.runCommand "out.json" {
+ json = configFile astroidAccounts;
+ preferLocalBuild = true;
+ allowSubstitutes = false;
+ } ''
+ echo -n "$json" | ${pkgs.jq}/bin/jq . > $out
+ '';
+
+ xdg.configFile."astroid/poll.sh" = {
+ executable = true;
+ text = ''
+ # Generated by Home Manager
+
+ ${cfg.pollScript}
+ '';
+ };
+ };
+}
diff --git a/home-manager/modules/programs/autorandr.nix b/home-manager/modules/programs/autorandr.nix
new file mode 100644
index 00000000000..02fc77c1e58
--- /dev/null
+++ b/home-manager/modules/programs/autorandr.nix
@@ -0,0 +1,355 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.autorandr;
+
+ matrixOf = n: m: elemType:
+ mkOptionType rec {
+ name = "matrixOf";
+ description =
+ "${toString n}×${toString m} matrix of ${elemType.description}s";
+ check = xss:
+ let listOfSize = l: xs: isList xs && length xs == l;
+ in listOfSize n xss
+ && all (xs: listOfSize m xs && all elemType.check xs) xss;
+ merge = mergeOneOption;
+ getSubOptions = prefix: elemType.getSubOptions (prefix ++ [ "*" "*" ]);
+ getSubModules = elemType.getSubModules;
+ substSubModules = mod: matrixOf n m (elemType.substSubModules mod);
+ functor = (defaultFunctor name) // { wrapped = elemType; };
+ };
+
+ profileModule = types.submodule {
+ options = {
+ fingerprint = mkOption {
+ type = types.attrsOf types.str;
+ description = ''
+ Output name to EDID mapping.
+ Use <code>autorandr --fingerprint</code> to get current setup values.
+ '';
+ default = { };
+ };
+
+ config = mkOption {
+ type = types.attrsOf configModule;
+ description = "Per output profile configuration.";
+ default = { };
+ };
+
+ hooks = mkOption {
+ type = profileHooksModule;
+ description = "Profile hook scripts.";
+ default = { };
+ };
+ };
+ };
+
+ configModule = types.submodule {
+ options = {
+ enable = mkOption {
+ type = types.bool;
+ description = "Whether to enable the output.";
+ default = true;
+ };
+
+ primary = mkOption {
+ type = types.bool;
+ description = "Whether output should be marked as primary";
+ default = false;
+ };
+
+ position = mkOption {
+ type = types.str;
+ description = "Output position";
+ default = "";
+ example = "5760x0";
+ };
+
+ mode = mkOption {
+ type = types.str;
+ description = "Output resolution.";
+ default = "";
+ example = "3840x2160";
+ };
+
+ rate = mkOption {
+ type = types.str;
+ description = "Output framerate.";
+ default = "";
+ example = "60.00";
+ };
+
+ gamma = mkOption {
+ type = types.str;
+ description = "Output gamma configuration.";
+ default = "";
+ example = "1.0:0.909:0.833";
+ };
+
+ rotate = mkOption {
+ type = types.nullOr (types.enum [ "normal" "left" "right" "inverted" ]);
+ description = "Output rotate configuration.";
+ default = null;
+ example = "left";
+ };
+
+ transform = mkOption {
+ type = types.nullOr (matrixOf 3 3 types.float);
+ default = null;
+ example = literalExample ''
+ [
+ [ 0.6 0.0 0.0 ]
+ [ 0.0 0.6 0.0 ]
+ [ 0.0 0.0 1.0 ]
+ ]
+ '';
+ description = ''
+ Refer to
+ <citerefentry>
+ <refentrytitle>xrandr</refentrytitle>
+ <manvolnum>1</manvolnum>
+ </citerefentry>
+ for the documentation of the transform matrix.
+ '';
+ };
+
+ dpi = mkOption {
+ type = types.nullOr types.ints.positive;
+ description = "Output DPI configuration.";
+ default = null;
+ example = 96;
+ };
+
+ scale = mkOption {
+ type = types.nullOr (types.submodule {
+ options = {
+ method = mkOption {
+ type = types.enum [ "factor" "pixel" ];
+ description = "Output scaling method.";
+ default = "factor";
+ example = "pixel";
+ };
+
+ x = mkOption {
+ type = types.either types.float types.ints.positive;
+ description = "Horizontal scaling factor/pixels.";
+ };
+
+ y = mkOption {
+ type = types.either types.float types.ints.positive;
+ description = "Vertical scaling factor/pixels.";
+ };
+ };
+ });
+ description = ''
+ Output scale configuration.
+ </para><para>
+ Either configure by pixels or a scaling factor. When using pixel method the
+ <citerefentry>
+ <refentrytitle>xrandr</refentrytitle>
+ <manvolnum>1</manvolnum>
+ </citerefentry>
+ option
+ <parameter class="command">--scale-from</parameter>
+ will be used; when using factor method the option
+ <parameter class="command">--scale</parameter>
+ will be used.
+ </para><para>
+ This option is a shortcut version of the transform option and they are mutually
+ exclusive.
+ '';
+ default = null;
+ example = literalExample ''
+ {
+ x = 1.25;
+ y = 1.25;
+ }
+ '';
+ };
+ };
+ };
+
+ hookType = types.lines;
+
+ globalHooksModule = types.submodule {
+ options = {
+ postswitch = mkOption {
+ type = types.attrsOf hookType;
+ description = "Postswitch hook executed after mode switch.";
+ default = { };
+ };
+
+ preswitch = mkOption {
+ type = types.attrsOf hookType;
+ description = "Preswitch hook executed before mode switch.";
+ default = { };
+ };
+
+ predetect = mkOption {
+ type = types.attrsOf hookType;
+ description = ''
+ Predetect hook executed before autorandr attempts to run xrandr.
+ '';
+ default = { };
+ };
+ };
+ };
+
+ profileHooksModule = types.submodule {
+ options = {
+ postswitch = mkOption {
+ type = hookType;
+ description = "Postswitch hook executed after mode switch.";
+ default = "";
+ };
+
+ preswitch = mkOption {
+ type = hookType;
+ description = "Preswitch hook executed before mode switch.";
+ default = "";
+ };
+
+ predetect = mkOption {
+ type = hookType;
+ description = ''
+ Predetect hook executed before autorandr attempts to run xrandr.
+ '';
+ default = "";
+ };
+ };
+ };
+
+ hookToFile = folder: name: hook:
+ nameValuePair "autorandr/${folder}/${name}" {
+ source = "${pkgs.writeShellScriptBin "hook" hook}/bin/hook";
+ };
+ profileToFiles = name: profile:
+ with profile;
+ mkMerge ([
+ {
+ "autorandr/${name}/setup".text = concatStringsSep "\n"
+ (mapAttrsToList fingerprintToString fingerprint);
+ "autorandr/${name}/config".text =
+ concatStringsSep "\n" (mapAttrsToList configToString profile.config);
+ }
+ (mkIf (hooks.postswitch != "")
+ (listToAttrs [ (hookToFile name "postswitch" hooks.postswitch) ]))
+ (mkIf (hooks.preswitch != "")
+ (listToAttrs [ (hookToFile name "preswitch" hooks.preswitch) ]))
+ (mkIf (hooks.predetect != "")
+ (listToAttrs [ (hookToFile name "predetect" hooks.predetect) ]))
+ ]);
+ fingerprintToString = name: edid: "${name} ${edid}";
+ configToString = name: config:
+ if config.enable then ''
+ output ${name}
+ ${optionalString (config.position != "") "pos ${config.position}"}
+ ${optionalString config.primary "primary"}
+ ${optionalString (config.dpi != null) "dpi ${toString config.dpi}"}
+ ${optionalString (config.gamma != "") "gamma ${config.gamma}"}
+ ${optionalString (config.mode != "") "mode ${config.mode}"}
+ ${optionalString (config.rate != "") "rate ${config.rate}"}
+ ${optionalString (config.rotate != null) "rotate ${config.rotate}"}
+ ${optionalString (config.scale != null)
+ ((if config.scale.method == "factor" then "scale" else "scale-from")
+ + " ${toString config.scale.x}x${toString config.scale.y}")}
+ ${optionalString (config.transform != null) ("transform "
+ + concatMapStringsSep "," toString (flatten config.transform))}
+ '' else ''
+ output ${name}
+ off
+ '';
+
+in {
+ options = {
+ programs.autorandr = {
+ enable = mkEnableOption "Autorandr";
+
+ hooks = mkOption {
+ type = globalHooksModule;
+ description = "Global hook scripts";
+ default = { };
+ example = literalExample ''
+ {
+ postswitch = {
+ "notify-i3" = "''${pkgs.i3}/bin/i3-msg restart";
+ "change-background" = readFile ./change-background.sh;
+ "change-dpi" = '''
+ case "$AUTORANDR_CURRENT_PROFILE" in
+ default)
+ DPI=120
+ ;;
+ home)
+ DPI=192
+ ;;
+ work)
+ DPI=144
+ ;;
+ *)
+ echo "Unknown profle: $AUTORANDR_CURRENT_PROFILE"
+ exit 1
+ esac
+
+ echo "Xft.dpi: $DPI" | ''${pkgs.xorg.xrdb}/bin/xrdb -merge
+ '''
+ };
+ }
+ '';
+ };
+
+ profiles = mkOption {
+ type = types.attrsOf profileModule;
+ description = "Autorandr profiles specification.";
+ default = { };
+ example = literalExample ''
+ {
+ "work" = {
+ fingerprint = {
+ eDP1 = "<EDID>";
+ DP1 = "<EDID>";
+ };
+ config = {
+ eDP1.enable = false;
+ DP1 = {
+ enable = true;
+ primary = true;
+ position = "0x0";
+ mode = "3840x2160";
+ gamma = "1.0:0.909:0.833";
+ rate = "60.00";
+ rotate = "left";
+ };
+ };
+ hooks.postswitch = readFile ./work-postswitch.sh;
+ };
+ }
+ '';
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ assertions = flatten (mapAttrsToList (profile:
+ { config, ... }:
+ mapAttrsToList (output: opts: {
+ assertion = opts.scale == null || opts.transform == null;
+ message = ''
+ Cannot use the profile output options 'scale' and 'transform' simultaneously.
+ Check configuration for: programs.autorandr.profiles.${profile}.config.${output}
+ '';
+ }) config) cfg.profiles);
+
+ home.packages = [ pkgs.autorandr ];
+ xdg.configFile = mkMerge ([
+ (mapAttrs' (hookToFile "postswitch.d") cfg.hooks.postswitch)
+ (mapAttrs' (hookToFile "preswitch.d") cfg.hooks.preswitch)
+ (mapAttrs' (hookToFile "predetect.d") cfg.hooks.predetect)
+ (mkMerge (mapAttrsToList profileToFiles cfg.profiles))
+ ]);
+ };
+
+ meta.maintainers = [ maintainers.uvnikita ];
+}
diff --git a/home-manager/modules/programs/bash.nix b/home-manager/modules/programs/bash.nix
new file mode 100644
index 00000000000..82a9fbe8f8b
--- /dev/null
+++ b/home-manager/modules/programs/bash.nix
@@ -0,0 +1,219 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.bash;
+
+in
+
+{
+ meta.maintainers = [ maintainers.rycee ];
+
+ options = {
+ programs.bash = {
+ enable = mkEnableOption "GNU Bourne-Again SHell";
+
+ historySize = mkOption {
+ type = types.int;
+ default = 10000;
+ description = "Number of history lines to keep in memory.";
+ };
+
+ historyFile = mkOption {
+ type = types.str;
+ default = "$HOME/.bash_history";
+ description = "Location of the bash history file.";
+ };
+
+ historyFileSize = mkOption {
+ type = types.int;
+ default = 100000;
+ description = "Number of history lines to keep on file.";
+ };
+
+ historyControl = mkOption {
+ type = types.listOf (types.enum [
+ "erasedups"
+ "ignoredups"
+ "ignorespace"
+ ]);
+ default = [];
+ description = "Controlling how commands are saved on the history list.";
+ };
+
+ historyIgnore = mkOption {
+ type = types.listOf types.str;
+ default = [];
+ example = [ "ls" "cd" "exit" ];
+ description = "List of commands that should not be saved to the history list.";
+ };
+
+ shellOptions = mkOption {
+ type = types.listOf types.str;
+ default = [
+ # Append to history file rather than replacing it.
+ "histappend"
+
+ # check the window size after each command and, if
+ # necessary, update the values of LINES and COLUMNS.
+ "checkwinsize"
+
+ # Extended globbing.
+ "extglob"
+ "globstar"
+
+ # Warn if closing shell with running jobs.
+ "checkjobs"
+ ];
+ description = "Shell options to set.";
+ };
+
+ sessionVariables = mkOption {
+ default = {};
+ type = types.attrs;
+ example = { MAILCHECK = 30; };
+ description = ''
+ Environment variables that will be set for the Bash session.
+ '';
+ };
+
+ shellAliases = mkOption {
+ default = {};
+ type = types.attrsOf types.str;
+ example = { ll = "ls -l"; ".." = "cd .."; };
+ description = ''
+ An attribute set that maps aliases (the top level attribute names in
+ this option) to command strings or directly to build outputs.
+ '';
+ };
+
+ enableAutojump = mkOption {
+ default = false;
+ type = types.bool;
+ description = "Enable the autojump navigation tool.";
+ };
+
+ profileExtra = mkOption {
+ default = "";
+ type = types.lines;
+ description = ''
+ Extra commands that should be run when initializing a login
+ shell.
+ '';
+ };
+
+ bashrcExtra = mkOption {
+ # Hide for now, may want to rename in the future.
+ visible = false;
+ default = "";
+ type = types.lines;
+ description = ''
+ Extra commands that should be added to
+ <filename>~/.bashrc</filename>.
+ '';
+ };
+
+ initExtra = mkOption {
+ default = "";
+ type = types.lines;
+ description = ''
+ Extra commands that should be run when initializing an
+ interactive shell.
+ '';
+ };
+
+ logoutExtra = mkOption {
+ default = "";
+ type = types.lines;
+ description = ''
+ Extra commands that should be run when logging out of an
+ interactive shell.
+ '';
+ };
+ };
+ };
+
+ config = (
+ let
+ aliasesStr = concatStringsSep "\n" (
+ mapAttrsToList (k: v: "alias ${k}=${escapeShellArg v}") cfg.shellAliases
+ );
+
+ shoptsStr = concatStringsSep "\n" (
+ map (v: "shopt -s ${v}") cfg.shellOptions
+ );
+
+ sessionVarsStr = config.lib.shell.exportAll cfg.sessionVariables;
+
+ historyControlStr =
+ concatStringsSep "\n" (mapAttrsToList (n: v: "${n}=${v}") (
+ {
+ HISTFILE = "\"${cfg.historyFile}\"";
+ HISTFILESIZE = toString cfg.historyFileSize;
+ HISTSIZE = toString cfg.historySize;
+ }
+ // optionalAttrs (cfg.historyControl != []) {
+ HISTCONTROL = concatStringsSep ":" cfg.historyControl;
+ }
+ // optionalAttrs (cfg.historyIgnore != []) {
+ HISTIGNORE = concatStringsSep ":" cfg.historyIgnore;
+ }
+ ));
+ in mkIf cfg.enable {
+ programs.bash.bashrcExtra = ''
+ # Commands that should be applied only for interactive shells.
+ if [[ $- == *i* ]]; then
+ ${historyControlStr}
+
+ ${shoptsStr}
+
+ ${aliasesStr}
+
+ ${cfg.initExtra}
+
+ ${optionalString cfg.enableAutojump
+ ". ${pkgs.autojump}/share/autojump/autojump.bash"}
+ fi
+ '';
+
+ home.file.".bash_profile".text = ''
+ # -*- mode: sh -*-
+
+ # include .profile if it exists
+ [[ -f ~/.profile ]] && . ~/.profile
+
+ # include .bashrc if it exists
+ [[ -f ~/.bashrc ]] && . ~/.bashrc
+ '';
+
+ home.file.".profile".text = ''
+ # -*- mode: sh -*-
+
+ . "${config.home.profileDirectory}/etc/profile.d/hm-session-vars.sh"
+
+ ${sessionVarsStr}
+
+ ${cfg.profileExtra}
+ '';
+
+ home.file.".bashrc".text = ''
+ # -*- mode: sh -*-
+
+ ${cfg.bashrcExtra}
+ '';
+
+ home.file.".bash_logout" = mkIf (cfg.logoutExtra != "") {
+ text = ''
+ # -*- mode: sh -*-
+
+ ${cfg.logoutExtra}
+ '';
+ };
+
+ home.packages =
+ optional (cfg.enableAutojump) pkgs.autojump;
+ }
+ );
+}
diff --git a/home-manager/modules/programs/bat.nix b/home-manager/modules/programs/bat.nix
new file mode 100644
index 00000000000..aa0df9abd45
--- /dev/null
+++ b/home-manager/modules/programs/bat.nix
@@ -0,0 +1,37 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.bat;
+
+in {
+ meta.maintainers = [ maintainers.marsam ];
+
+ options.programs.bat = {
+ enable = mkEnableOption "bat, a cat clone with wings";
+
+ config = mkOption {
+ type = types.attrsOf types.str;
+ default = { };
+ example = {
+ theme = "TwoDark";
+ pager = "less -FR";
+ };
+ description = ''
+ Bat configuration.
+ '';
+ };
+
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ pkgs.bat ];
+
+ xdg.configFile."bat/config" = mkIf (cfg.config != { }) {
+ text = concatStringsSep "\n"
+ (mapAttrsToList (n: v: ''--${n}="${v}"'') cfg.config);
+ };
+ };
+}
diff --git a/home-manager/modules/programs/beets.nix b/home-manager/modules/programs/beets.nix
new file mode 100644
index 00000000000..1a45bbea1c7
--- /dev/null
+++ b/home-manager/modules/programs/beets.nix
@@ -0,0 +1,58 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.beets;
+
+in {
+ meta.maintainers = [ maintainers.rycee ];
+
+ options = {
+ programs.beets = {
+ enable = mkOption {
+ type = types.bool;
+ default = if versionAtLeast config.home.stateVersion "19.03" then
+ false
+ else
+ cfg.settings != { };
+ defaultText = "false";
+ description = ''
+ Whether to enable the beets music library manager. This
+ defaults to <literal>false</literal> for state
+ version ≥ 19.03. For earlier versions beets is enabled if
+ <option>programs.beets.settings</option> is non-empty.
+ '';
+ };
+
+ package = mkOption {
+ type = types.package;
+ default = pkgs.beets;
+ defaultText = literalExample "pkgs.beets";
+ example =
+ literalExample "(pkgs.beets.override { enableCheck = true; })";
+ description = ''
+ The <literal>beets</literal> package to use.
+ Can be used to specify extensions.
+ '';
+ };
+
+ settings = mkOption {
+ type = types.attrs;
+ default = { };
+ description = ''
+ Configuration written to
+ <filename>~/.config/beets/config.yaml</filename>
+ '';
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ cfg.package ];
+
+ xdg.configFile."beets/config.yaml".text =
+ builtins.toJSON config.programs.beets.settings;
+ };
+}
diff --git a/home-manager/modules/programs/broot.nix b/home-manager/modules/programs/broot.nix
new file mode 100644
index 00000000000..eac31b56801
--- /dev/null
+++ b/home-manager/modules/programs/broot.nix
@@ -0,0 +1,256 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.broot;
+
+ configFile = config:
+ pkgs.runCommand "conf.toml" {
+ buildInputs = [ pkgs.remarshal ];
+ preferLocalBuild = true;
+ allowSubstitutes = false;
+ } ''
+ remarshal -if json -of toml \
+ < ${pkgs.writeText "verbs.json" (builtins.toJSON config)} \
+ > $out
+ '';
+
+ brootConf = {
+ verbs =
+ mapAttrsToList (name: value: value // { invocation = name; }) cfg.verbs;
+ skin = cfg.skin;
+ };
+
+in {
+ meta.maintainers = [ maintainers.aheaume ];
+
+ options.programs.broot = {
+ enable = mkEnableOption "Broot, a better way to navigate directories";
+
+ enableBashIntegration = mkOption {
+ default = true;
+ type = types.bool;
+ description = ''
+ Whether to enable Bash integration.
+ '';
+ };
+
+ enableZshIntegration = mkOption {
+ default = true;
+ type = types.bool;
+ description = ''
+ Whether to enable Zsh integration.
+ '';
+ };
+
+ enableFishIntegration = mkOption {
+ default = true;
+ type = types.bool;
+ description = ''
+ Whether to enable Fish integration.
+ '';
+ };
+
+ verbs = mkOption {
+ type = with types; attrsOf (attrsOf (either bool str));
+ default = {
+ "p" = { execution = ":parent"; };
+ "edit" = {
+ shortcut = "e";
+ execution = "$EDITOR {file}";
+ };
+ "create {subpath}" = { execution = "$EDITOR {directory}/{subpath}"; };
+ "view" = { execution = "less {file}"; };
+ };
+ example = literalExample ''
+ {
+ "p" = { execution = ":parent"; };
+ "edit" = { shortcut = "e"; execution = "$EDITOR {file}" ; };
+ "create {subpath}" = { execution = "$EDITOR {directory}/{subpath}"; };
+ "view" = { execution = "less {file}"; };
+ "blop {name}\\.{type}" = {
+ execution = "/bin/mkdir {parent}/{type} && /usr/bin/nvim {parent}/{type}/{name}.{type}";
+ from_shell = true;
+ };
+ }
+ '';
+ description = ''
+ Define new verbs. The attribute name indicates how the verb is
+ called by the user, with placeholders for arguments.
+ </para><para>
+ The possible attributes are:
+ </para>
+
+ <para>
+ <variablelist>
+ <varlistentry>
+ <term><literal>execution</literal> (mandatory)</term>
+ <listitem><para>how the verb is executed</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><literal>shortcut</literal> (optional)</term>
+ <listitem><para>an alternate way to call the verb (without
+ the arguments part)</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><literal>leave_broot</literal> (optional)</term>
+ <listitem><para>whether to quit broot on execution
+ (default: <literal>true</literal>)</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><literal>from_shell</literal> (optional)</term>
+ <listitem><para>whether the verb must be executed from the
+ parent shell (default:
+ <literal>false</literal>)</para></listitem>
+ </varlistentry>
+ </variablelist>
+ '';
+ };
+
+ skin = mkOption {
+ type = types.attrsOf types.str;
+ default = { };
+ example = literalExample ''
+ {
+ status_normal_fg = "grayscale(18)";
+ status_normal_bg = "grayscale(3)";
+ status_error_fg = "red";
+ status_error_bg = "yellow";
+ tree_fg = "red";
+ selected_line_bg = "grayscale(7)";
+ permissions_fg = "grayscale(12)";
+ size_bar_full_bg = "red";
+ size_bar_void_bg = "black";
+ directory_fg = "lightyellow";
+ input_fg = "cyan";
+ flag_value_fg = "lightyellow";
+ table_border_fg = "red";
+ code_fg = "lightyellow";
+ }
+ '';
+ description = ''
+ Color configuration.
+ </para><para>
+ Complete list of keys (expected to change before the v1 of broot):
+
+ <itemizedlist>
+ <listitem><para><literal>char_match</literal></para></listitem>
+ <listitem><para><literal>code</literal></para></listitem>
+ <listitem><para><literal>directory</literal></para></listitem>
+ <listitem><para><literal>exe</literal></para></listitem>
+ <listitem><para><literal>file</literal></para></listitem>
+ <listitem><para><literal>file_error</literal></para></listitem>
+ <listitem><para><literal>flag_label</literal></para></listitem>
+ <listitem><para><literal>flag_value</literal></para></listitem>
+ <listitem><para><literal>input</literal></para></listitem>
+ <listitem><para><literal>link</literal></para></listitem>
+ <listitem><para><literal>permissions</literal></para></listitem>
+ <listitem><para><literal>selected_line</literal></para></listitem>
+ <listitem><para><literal>size_bar_full</literal></para></listitem>
+ <listitem><para><literal>size_bar_void</literal></para></listitem>
+ <listitem><para><literal>size_text</literal></para></listitem>
+ <listitem><para><literal>spinner</literal></para></listitem>
+ <listitem><para><literal>status_error</literal></para></listitem>
+ <listitem><para><literal>status_normal</literal></para></listitem>
+ <listitem><para><literal>table_border</literal></para></listitem>
+ <listitem><para><literal>tree</literal></para></listitem>
+ <listitem><para><literal>unlisted</literal></para></listitem>
+ </itemizedlist></para>
+
+ <para>
+ Add <literal>_fg</literal> for a foreground color and
+ <literal>_bg</literal> for a background colors.
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ pkgs.broot ];
+
+ xdg.configFile."broot/conf.toml".source = configFile brootConf;
+
+ # Dummy file to prevent broot from trying to reinstall itself
+ xdg.configFile."broot/launcher/installed".text = "";
+
+ programs.bash.initExtra = mkIf cfg.enableBashIntegration (
+ # Using mkAfter to make it more likely to appear after other
+ # manipulations of the prompt.
+ mkAfter ''
+ # This script was automatically generated by the broot function
+ # More information can be found in https://github.com/Canop/broot
+ # This function starts broot and executes the command
+ # it produces, if any.
+ # It's needed because some shell commands, like `cd`,
+ # have no useful effect if executed in a subshell.
+ function br {
+ f=$(mktemp)
+ (
+ set +e
+ broot --outcmd "$f" "$@"
+ code=$?
+ if [ "$code" != 0 ]; then
+ rm -f "$f"
+ exit "$code"
+ fi
+ )
+ code=$?
+ if [ "$code" != 0 ]; then
+ return "$code"
+ fi
+ d=$(cat "$f")
+ rm -f "$f"
+ eval "$d"
+ }
+ '');
+
+ programs.zsh.initExtra = mkIf cfg.enableZshIntegration ''
+ # This script was automatically generated by the broot function
+ # More information can be found in https://github.com/Canop/broot
+ # This function starts broot and executes the command
+ # it produces, if any.
+ # It's needed because some shell commands, like `cd`,
+ # have no useful effect if executed in a subshell.
+ function br {
+ f=$(mktemp)
+ (
+ set +e
+ broot --outcmd "$f" "$@"
+ code=$?
+ if [ "$code" != 0 ]; then
+ rm -f "$f"
+ exit "$code"
+ fi
+ )
+ code=$?
+ if [ "$code" != 0 ]; then
+ return "$code"
+ fi
+ d=$(cat "$f")
+ rm -f "$f"
+ eval "$d"
+ }
+ '';
+
+ programs.fish.shellInit = mkIf cfg.enableFishIntegration ''
+ # This script was automatically generated by the broot function
+ # More information can be found in https://github.com/Canop/broot
+ # This function starts broot and executes the command
+ # it produces, if any.
+ # It's needed because some shell commands, like `cd`,
+ # have no useful effect if executed in a subshell.
+ function br
+ set f (mktemp)
+ broot --outcmd $f $argv
+ if test $status -ne 0
+ rm -f "$f"
+ return "$code"
+ end
+ set d (cat "$f")
+ rm -f "$f"
+ eval "$d"
+ end
+ '';
+ };
+}
diff --git a/home-manager/modules/programs/browserpass.nix b/home-manager/modules/programs/browserpass.nix
new file mode 100644
index 00000000000..10a2883c871
--- /dev/null
+++ b/home-manager/modules/programs/browserpass.nix
@@ -0,0 +1,76 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let browsers = [ "chrome" "chromium" "firefox" "vivaldi" ];
+in {
+ options = {
+ programs.browserpass = {
+ enable = mkEnableOption "the browserpass extension host application";
+
+ browsers = mkOption {
+ type = types.listOf (types.enum browsers);
+ default = browsers;
+ example = [ "firefox" ];
+ description = "Which browsers to install browserpass for";
+ };
+ };
+ };
+
+ config = mkIf config.programs.browserpass.enable {
+ home.file = foldl' (a: b: a // b) { } (concatMap (x:
+ with pkgs.stdenv;
+ if x == "chrome" then
+ let
+ dir = if isDarwin then
+ "Library/Application Support/Google/Chrome/NativeMessagingHosts"
+ else
+ ".config/google-chrome/NativeMessagingHosts";
+ in [{
+ "${dir}/com.github.browserpass.native.json".source =
+ "${pkgs.browserpass}/lib/browserpass/hosts/chromium/com.github.browserpass.native.json";
+ "${dir}/../policies/managed/com.github.browserpass.native.json".source =
+ "${pkgs.browserpass}/lib/browserpass/policies/chromium/com.github.browserpass.native.json";
+ }]
+ else if x == "chromium" then
+ let
+ dir = if isDarwin then
+ "Library/Application Support/Chromium/NativeMessagingHosts"
+ else
+ ".config/chromium/NativeMessagingHosts";
+ in [
+ {
+ "${dir}/com.github.browserpass.native.json".source =
+ "${pkgs.browserpass}/lib/browserpass/hosts/chromium/com.github.browserpass.native.json";
+ }
+ {
+ "${dir}/../policies/managed/com.github.browserpass.native.json".source =
+ "${pkgs.browserpass}/lib/browserpass/policies/chromium/com.github.browserpass.native.json";
+ }
+ ]
+ else if x == "firefox" then
+ let
+ dir = if isDarwin then
+ "Library/Application Support/Mozilla/NativeMessagingHosts"
+ else
+ ".mozilla/native-messaging-hosts";
+ in [{
+ "${dir}/com.github.browserpass.native.json".source =
+ "${pkgs.browserpass}/lib/browserpass/hosts/firefox/com.github.browserpass.native.json";
+ }]
+ else if x == "vivaldi" then
+ let
+ dir = if isDarwin then
+ "Library/Application Support/Vivaldi/NativeMessagingHosts"
+ else
+ ".config/vivaldi/NativeMessagingHosts";
+ in [{
+ "${dir}/com.github.browserpass.native.json".source =
+ "${pkgs.browserpass}/lib/browserpass/hosts/chromium/com.github.browserpass.native.json";
+ "${dir}/../policies/managed/com.github.browserpass.native.json".source =
+ "${pkgs.browserpass}/lib/browserpass/policies/chromium/com.github.browserpass.native.json";
+ }]
+ else
+ throw "unknown browser ${x}") config.programs.browserpass.browsers);
+ };
+}
diff --git a/home-manager/modules/programs/chromium.nix b/home-manager/modules/programs/chromium.nix
new file mode 100644
index 00000000000..4e35c07b90c
--- /dev/null
+++ b/home-manager/modules/programs/chromium.nix
@@ -0,0 +1,92 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ browserModule = defaultPkg: name: visible:
+ let browser = (builtins.parseDrvName defaultPkg.name).name;
+ in {
+ enable = mkOption {
+ inherit visible;
+ default = false;
+ example = true;
+ description = "Whether to enable ${name}.";
+ type = lib.types.bool;
+ };
+
+ package = mkOption {
+ inherit visible;
+ type = types.package;
+ default = defaultPkg;
+ defaultText = literalExample "pkgs.${browser}";
+ description = "The ${name} package to use.";
+ };
+
+ extensions = mkOption {
+ inherit visible;
+ type = types.listOf types.str;
+ default = [ ];
+ example = literalExample ''
+ [
+ "chlffgpmiacpedhhbkiomidkjlcfhogd" # pushbullet
+ "mbniclmhobmnbdlbpiphghaielnnpgdp" # lightshot
+ "gcbommkclmclpchllfjekcdonpmejbdp" # https everywhere
+ "cjpalhdlnbpafiamejdnhcphjbkeiagm" # ublock origin
+ ]
+ '';
+ description = ''
+ List of ${name} extensions to install.
+ To find the extension ID, check its URL on the
+ <link xlink:href="https://chrome.google.com/webstore/category/extensions">Chrome Web Store</link>.
+ '';
+ };
+ };
+
+ browserConfig = cfg:
+ let
+
+ browser = (builtins.parseDrvName cfg.package.name).name;
+
+ darwinDirs = {
+ chromium = "Chromium";
+ google-chrome = "Google/Chrome";
+ google-chrome-beta = "Google/Chrome Beta";
+ google-chrome-dev = "Google/Chrome Dev";
+ };
+
+ configDir = if pkgs.stdenv.isDarwin then
+ "Library/Application Support/${getAttr browser darwinDirs}"
+ else
+ "${config.xdg.configHome}/${browser}";
+
+ extensionJson = ext: {
+ name = "${configDir}/External Extensions/${ext}.json";
+ value.text = builtins.toJSON {
+ external_update_url =
+ "https://clients2.google.com/service/update2/crx";
+ };
+ };
+
+ in mkIf cfg.enable {
+ home.packages = [ cfg.package ];
+ home.file = listToAttrs (map extensionJson cfg.extensions);
+ };
+
+in {
+ options.programs = {
+ chromium = browserModule pkgs.chromium "Chromium" true;
+ google-chrome = browserModule pkgs.google-chrome "Google Chrome" false;
+ google-chrome-beta =
+ browserModule pkgs.google-chrome-beta "Google Chrome Beta" false;
+ google-chrome-dev =
+ browserModule pkgs.google-chrome-dev "Google Chrome Dev" false;
+ };
+
+ config = mkMerge [
+ (browserConfig config.programs.chromium)
+ (browserConfig config.programs.google-chrome)
+ (browserConfig config.programs.google-chrome-beta)
+ (browserConfig config.programs.google-chrome-dev)
+ ];
+}
diff --git a/home-manager/modules/programs/command-not-found/command-not-found.nix b/home-manager/modules/programs/command-not-found/command-not-found.nix
new file mode 100644
index 00000000000..b79fde0f619
--- /dev/null
+++ b/home-manager/modules/programs/command-not-found/command-not-found.nix
@@ -0,0 +1,59 @@
+# Adapted from Nixpkgs.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+ cfg = config.programs.command-not-found;
+ commandNotFound = pkgs.substituteAll {
+ name = "command-not-found";
+ dir = "bin";
+ src = ./command-not-found.pl;
+ isExecutable = true;
+ inherit (pkgs) perl;
+ inherit (cfg) dbPath;
+ perlFlags = concatStrings (map (path: "-I ${path}/lib/perl5/site_perl ") [
+ pkgs.perlPackages.DBI
+ pkgs.perlPackages.DBDSQLite
+ pkgs.perlPackages.StringShellQuote
+ ]);
+ };
+
+ shInit = commandNotFoundHandlerName: ''
+ # This function is called whenever a command is not found.
+ ${commandNotFoundHandlerName}() {
+ local p=${commandNotFound}/bin/command-not-found
+ if [ -x $p -a -f ${cfg.dbPath} ]; then
+ # Run the helper program.
+ $p "$@"
+ else
+ echo "$1: command not found" >&2
+ return 127
+ fi
+ }
+ '';
+
+in {
+ options.programs.command-not-found = {
+ enable = mkEnableOption "command-not-found hook for interactive shell";
+
+ dbPath = mkOption {
+ default =
+ "/nix/var/nix/profiles/per-user/root/channels/nixos/programs.sqlite";
+ description = ''
+ Absolute path to <filename>programs.sqlite</filename>. By
+ default this file will be provided by your channel
+ (nixexprs.tar.xz).
+ '';
+ type = types.path;
+ };
+ };
+
+ config = mkIf cfg.enable {
+ programs.bash.initExtra = shInit "command_not_found_handle";
+ programs.zsh.initExtra = shInit "command_not_found_handler";
+
+ home.packages = [ commandNotFound ];
+ };
+}
diff --git a/home-manager/modules/programs/command-not-found/command-not-found.pl b/home-manager/modules/programs/command-not-found/command-not-found.pl
new file mode 100644
index 00000000000..997dfec649b
--- /dev/null
+++ b/home-manager/modules/programs/command-not-found/command-not-found.pl
@@ -0,0 +1,44 @@
+#! @perl@/bin/perl -w @perlFlags@
+
+use strict;
+use DBI;
+use DBD::SQLite;
+use String::ShellQuote;
+use Config;
+
+my $program = $ARGV[0];
+
+my $dbPath = "@dbPath@";
+
+my $dbh = DBI->connect("dbi:SQLite:dbname=$dbPath", "", "")
+ or die "cannot open database `$dbPath'";
+$dbh->{RaiseError} = 0;
+$dbh->{PrintError} = 0;
+
+my $system = $ENV{"NIX_SYSTEM"} // $Config{myarchname};
+
+my $res = $dbh->selectall_arrayref(
+ "select package from Programs where system = ? and name = ?",
+ { Slice => {} }, $system, $program);
+
+if (!defined $res || scalar @$res == 0) {
+ print STDERR "$program: command not found\n";
+} elsif (scalar @$res == 1) {
+ my $package = @$res[0]->{package};
+ if ($ENV{"NIX_AUTO_RUN"} // "") {
+ exec("nix-shell", "-p", $package, "--run", shell_quote("exec", @ARGV));
+ } else {
+ print STDERR <<EOF;
+The program ‘$program’ is currently not installed. You can install it by typing:
+ nix-env -iA nixos.$package
+EOF
+ }
+} else {
+ print STDERR <<EOF;
+The program ‘$program’ is currently not installed. It is provided by
+several packages. You can install it by typing one of the following:
+EOF
+ print STDERR " nix-env -iA nixos.$_->{package}\n" foreach @$res;
+}
+
+exit 127;
diff --git a/home-manager/modules/programs/direnv.nix b/home-manager/modules/programs/direnv.nix
new file mode 100644
index 00000000000..beb40a96261
--- /dev/null
+++ b/home-manager/modules/programs/direnv.nix
@@ -0,0 +1,99 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.direnv;
+ configFile = config:
+ pkgs.runCommand "config.toml" {
+ buildInputs = [ pkgs.remarshal ];
+ preferLocalBuild = true;
+ allowSubstitutes = false;
+ } ''
+ remarshal -if json -of toml \
+ < ${pkgs.writeText "config.json" (builtins.toJSON config)} \
+ > $out
+ '';
+
+in {
+ meta.maintainers = [ maintainers.rycee ];
+
+ options.programs.direnv = {
+ enable = mkEnableOption "direnv, the environment switcher";
+
+ config = mkOption {
+ type = types.attrs;
+ default = { };
+ description = ''
+ Configuration written to
+ <filename>~/.config/direnv/config.toml</filename>.
+ </para><para>
+ See
+ <citerefentry>
+ <refentrytitle>direnv.toml</refentrytitle>
+ <manvolnum>1</manvolnum>
+ </citerefentry>.
+ for the full list of options.
+ '';
+ };
+
+ stdlib = mkOption {
+ type = types.lines;
+ default = "";
+ description = ''
+ Custom stdlib written to
+ <filename>~/.config/direnv/direnvrc</filename>.
+ '';
+ };
+
+ enableBashIntegration = mkOption {
+ default = true;
+ type = types.bool;
+ description = ''
+ Whether to enable Bash integration.
+ '';
+ };
+
+ enableZshIntegration = mkOption {
+ default = true;
+ type = types.bool;
+ description = ''
+ Whether to enable Zsh integration.
+ '';
+ };
+
+ enableFishIntegration = mkOption {
+ default = true;
+ type = types.bool;
+ description = ''
+ Whether to enable Fish integration.
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ pkgs.direnv ];
+
+ xdg.configFile."direnv/config.toml" =
+ mkIf (cfg.config != { }) { source = configFile cfg.config; };
+
+ xdg.configFile."direnv/direnvrc" =
+ mkIf (cfg.stdlib != "") { text = cfg.stdlib; };
+
+ programs.bash.initExtra = mkIf cfg.enableBashIntegration (
+ # Using mkAfter to make it more likely to appear after other
+ # manipulations of the prompt.
+ mkAfter ''
+ eval "$(${pkgs.direnv}/bin/direnv hook bash)"
+ '');
+
+ programs.zsh.initExtra = mkIf cfg.enableZshIntegration ''
+ eval "$(${pkgs.direnv}/bin/direnv hook zsh)"
+ '';
+
+ programs.fish.shellInit = mkIf cfg.enableFishIntegration ''
+ eval (${pkgs.direnv}/bin/direnv hook fish)
+ '';
+ };
+}
diff --git a/home-manager/modules/programs/eclipse.nix b/home-manager/modules/programs/eclipse.nix
new file mode 100644
index 00000000000..8ce605b106a
--- /dev/null
+++ b/home-manager/modules/programs/eclipse.nix
@@ -0,0 +1,50 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.eclipse;
+
+in {
+ meta.maintainers = [ maintainers.rycee ];
+
+ options = {
+ programs.eclipse = {
+ enable = mkEnableOption "Eclipse";
+
+ enableLombok = mkOption {
+ type = types.bool;
+ default = false;
+ example = true;
+ description = ''
+ Whether to enable the Lombok Java Agent in Eclipse. This is
+ necessary to use the Lombok class annotations.
+ '';
+ };
+
+ jvmArgs = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ description = "JVM arguments to use for the Eclipse process.";
+ };
+
+ plugins = mkOption {
+ type = types.listOf types.package;
+ default = [ ];
+ description = "Plugins that should be added to Eclipse.";
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [
+ (pkgs.eclipses.eclipseWithPlugins {
+ eclipse = pkgs.eclipses.eclipse-platform;
+ jvmArgs = cfg.jvmArgs ++ optional cfg.enableLombok
+ "-javaagent:${pkgs.lombok}/share/java/lombok.jar";
+ plugins = cfg.plugins;
+ })
+ ];
+ };
+}
diff --git a/home-manager/modules/programs/emacs.nix b/home-manager/modules/programs/emacs.nix
new file mode 100644
index 00000000000..987a9f2431e
--- /dev/null
+++ b/home-manager/modules/programs/emacs.nix
@@ -0,0 +1,77 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.emacs;
+
+ # Copied from all-packages.nix, with modifications to support
+ # overrides.
+ emacsPackages =
+ let
+ epkgs = pkgs.emacsPackagesGen cfg.package;
+ in
+ epkgs.overrideScope' cfg.overrides;
+ emacsWithPackages = emacsPackages.emacsWithPackages;
+
+in
+
+{
+ meta.maintainers = [ maintainers.rycee ];
+
+ options = {
+ programs.emacs = {
+ enable = mkEnableOption "Emacs";
+
+ package = mkOption {
+ type = types.package;
+ default = pkgs.emacs;
+ defaultText = literalExample "pkgs.emacs";
+ example = literalExample "pkgs.emacs25-nox";
+ description = "The Emacs package to use.";
+ };
+
+ extraPackages = mkOption {
+ default = self: [];
+ type = hm.types.selectorFunction;
+ defaultText = "epkgs: []";
+ example = literalExample "epkgs: [ epkgs.emms epkgs.magit ]";
+ description = ''
+ Extra packages available to Emacs. To get a list of
+ available packages run:
+ <command>nix-env -f '&lt;nixpkgs&gt;' -qaP -A emacsPackages</command>.
+ '';
+ };
+
+ overrides = mkOption {
+ default = self: super: {};
+ type = hm.types.overlayFunction;
+ defaultText = "self: super: {}";
+ example = literalExample ''
+ self: super: rec {
+ haskell-mode = self.melpaPackages.haskell-mode;
+ # ...
+ };
+ '';
+ description = ''
+ Allows overriding packages within the Emacs package set.
+ '';
+ };
+
+ finalPackage = mkOption {
+ type = types.package;
+ visible = false;
+ readOnly = true;
+ description = ''
+ The Emacs package including any overrides and extra packages.
+ '';
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ cfg.finalPackage ];
+ programs.emacs.finalPackage = emacsWithPackages cfg.extraPackages;
+ };
+}
diff --git a/home-manager/modules/programs/feh.nix b/home-manager/modules/programs/feh.nix
new file mode 100644
index 00000000000..b1b33697e95
--- /dev/null
+++ b/home-manager/modules/programs/feh.nix
@@ -0,0 +1,70 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.feh;
+
+ disableBinding = func: key: func;
+ enableBinding = func: key: "${func} ${toString key}";
+
+in {
+ options.programs.feh = {
+ enable = mkEnableOption "feh - a fast and light image viewer";
+
+ buttons = mkOption {
+ default = { };
+ type = with types; attrsOf (nullOr (either str int));
+ example = {
+ zoom_in = 4;
+ zoom_out = "C-4";
+ };
+ description = ''
+ Override feh's default mouse button mapping. If you want to disable an
+ action, set its value to null.
+ See <link xlink:href="https://man.finalrewind.org/1/feh/#x425554544f4e53"/> for
+ default bindings and available commands.
+ '';
+ };
+
+ keybindings = mkOption {
+ default = { };
+ type = types.attrsOf (types.nullOr types.str);
+ example = {
+ zoom_in = "plus";
+ zoom_out = "minus";
+ };
+ description = ''
+ Override feh's default keybindings. If you want to disable a keybinding
+ set its value to null.
+ See <link xlink:href="https://man.finalrewind.org/1/feh/#x4b455953"/> for
+ default bindings and available commands.
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+ assertions = [{
+ assertion = ((filterAttrs (n: v: v == "") cfg.keybindings) == { });
+ message =
+ "To disable a keybinding, use `null` instead of an empty string.";
+ }];
+
+ home.packages = [ pkgs.feh ];
+
+ xdg.configFile."feh/buttons".text = ''
+ ${concatStringsSep "\n" (mapAttrsToList disableBinding
+ (filterAttrs (n: v: v == null) cfg.buttons))}
+ ${concatStringsSep "\n" (mapAttrsToList enableBinding
+ (filterAttrs (n: v: v != null) cfg.buttons))}
+ '';
+
+ xdg.configFile."feh/keys".text = ''
+ ${concatStringsSep "\n" (mapAttrsToList disableBinding
+ (filterAttrs (n: v: v == null) cfg.keybindings))}
+ ${concatStringsSep "\n" (mapAttrsToList enableBinding
+ (filterAttrs (n: v: v != null) cfg.keybindings))}
+ '';
+ };
+}
diff --git a/home-manager/modules/programs/firefox.nix b/home-manager/modules/programs/firefox.nix
new file mode 100644
index 00000000000..17c64752d66
--- /dev/null
+++ b/home-manager/modules/programs/firefox.nix
@@ -0,0 +1,305 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ inherit (pkgs.stdenv.hostPlatform) isDarwin;
+
+ cfg = config.programs.firefox;
+
+ mozillaConfigPath =
+ if isDarwin
+ then "Library/Application Support/Mozilla"
+ else ".mozilla";
+
+ firefoxConfigPath =
+ if isDarwin
+ then "Library/Application Support/Firefox"
+ else "${mozillaConfigPath}/firefox";
+
+ profilesPath =
+ if isDarwin
+ then "${firefoxConfigPath}/Profiles"
+ else firefoxConfigPath;
+
+ extensionPath = "extensions/{ec8030f7-c20a-464f-9b0e-13a3a9e97384}";
+
+ profiles =
+ flip mapAttrs' cfg.profiles (_: profile:
+ nameValuePair "Profile${toString profile.id}" {
+ Name = profile.name;
+ Path =
+ if isDarwin
+ then "Profiles/${profile.path}"
+ else profile.path;
+ IsRelative = 1;
+ Default = if profile.isDefault then 1 else 0;
+ }
+ ) // {
+ General = {
+ StartWithLastProfile = 1;
+ };
+ };
+
+ profilesIni = generators.toINI {} profiles;
+
+ mkUserJs = prefs: extraPrefs: ''
+ // Generated by Home Manager.
+
+ ${concatStrings (mapAttrsToList (name: value: ''
+ user_pref("${name}", ${builtins.toJSON value});
+ '') prefs)}
+
+ ${extraPrefs}
+ '';
+
+in
+
+{
+ meta.maintainers = [ maintainers.rycee ];
+
+ options = {
+ programs.firefox = {
+ enable = mkEnableOption "Firefox";
+
+ package = mkOption {
+ type = types.package;
+ default =
+ if versionAtLeast config.home.stateVersion "19.09"
+ then pkgs.firefox
+ else pkgs.firefox-unwrapped;
+ defaultText = literalExample "pkgs.firefox";
+ description = ''
+ The Firefox package to use. If state version ≥ 19.09 then
+ this should be a wrapped Firefox package. For earlier state
+ versions it should be an unwrapped Firefox package.
+ '';
+ };
+
+ extensions = mkOption {
+ type = types.listOf types.package;
+ default = [];
+ example = literalExample ''
+ with pkgs.nur.repos.rycee.firefox-addons; [
+ https-everywhere
+ privacy-badger
+ ]
+ '';
+ description = ''
+ List of Firefox add-on packages to install. Note, it is
+ necessary to manually enable these extensions inside Firefox
+ after the first installation.
+ '';
+ };
+
+ profiles = mkOption {
+ type = types.attrsOf (types.submodule ({config, name, ...}: {
+ options = {
+ name = mkOption {
+ type = types.str;
+ default = name;
+ description = "Profile name.";
+ };
+
+ id = mkOption {
+ type = types.ints.unsigned;
+ default = 0;
+ description = ''
+ Profile ID. This should be set to a unique number per profile.
+ '';
+ };
+
+ settings = mkOption {
+ type = with types; attrsOf (either bool (either int str));
+ default = {};
+ example = literalExample ''
+ {
+ "browser.startup.homepage" = "https://nixos.org";
+ "browser.search.region" = "GB";
+ "browser.search.isUS" = false;
+ "distribution.searchplugins.defaultLocale" = "en-GB";
+ "general.useragent.locale" = "en-GB";
+ "browser.bookmarks.showMobileBookmarks" = true;
+ }
+ '';
+ description = "Attribute set of Firefox preferences.";
+ };
+
+ extraConfig = mkOption {
+ type = types.lines;
+ default = "";
+ description = ''
+ Extra preferences to add to <filename>user.js</filename>.
+ '';
+ };
+
+ userChrome = mkOption {
+ type = types.lines;
+ default = "";
+ description = "Custom Firefox CSS.";
+ example = ''
+ /* Hide tab bar in FF Quantum */
+ @-moz-document url("chrome://browser/content/browser.xul") {
+ #TabsToolbar {
+ visibility: collapse !important;
+ margin-bottom: 21px !important;
+ }
+
+ #sidebar-box[sidebarcommand="treestyletab_piro_sakura_ne_jp-sidebar-action"] #sidebar-header {
+ visibility: collapse !important;
+ }
+ }
+ '';
+ };
+
+ path = mkOption {
+ type = types.str;
+ default = name;
+ description = "Profile path.";
+ };
+
+ isDefault = mkOption {
+ type = types.bool;
+ default = config.id == 0;
+ defaultText = "true if profile ID is 0";
+ description = "Whether this is a default profile.";
+ };
+ };
+ }));
+ default = {};
+ description = "Attribute set of Firefox profiles.";
+ };
+
+ enableAdobeFlash = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Whether to enable the unfree Adobe Flash plugin.";
+ };
+
+ enableGoogleTalk = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Whether to enable the unfree Google Talk plugin. This option
+ is <emphasis>deprecated</emphasis> and will only work if
+
+ <programlisting language="nix">
+ programs.firefox.package = pkgs.firefox-esr-52-unwrapped;
+ </programlisting>
+
+ and the <option>plugin.load_flash_only</option> Firefox
+ option has been disabled.
+ '';
+ };
+
+ enableIcedTea = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Whether to enable the Java applet plugin. This option is
+ <emphasis>deprecated</emphasis> and will only work if
+
+ <programlisting language="nix">
+ programs.firefox.package = pkgs.firefox-esr-52-unwrapped;
+ </programlisting>
+
+ and the <option>plugin.load_flash_only</option> Firefox
+ option has been disabled.
+ '';
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ assertions = [
+ (
+ let
+ defaults =
+ catAttrs "name" (filter (a: a.isDefault) (attrValues cfg.profiles));
+ in {
+ assertion = cfg.profiles == {} || length defaults == 1;
+ message =
+ "Must have exactly one default Firefox profile but found "
+ + toString (length defaults)
+ + optionalString (length defaults > 1)
+ (", namely " + concatStringsSep ", " defaults);
+ }
+ )
+
+ (
+ let
+ duplicates =
+ filterAttrs (_: v: length v != 1)
+ (zipAttrs
+ (mapAttrsToList (n: v: { "${toString v.id}" = n; })
+ (cfg.profiles)));
+
+ mkMsg = n: v: " - ID ${n} is used by ${concatStringsSep ", " v}";
+ in {
+ assertion = duplicates == {};
+ message =
+ "Must not have Firefox profiles with duplicate IDs but\n"
+ + concatStringsSep "\n" (mapAttrsToList mkMsg duplicates);
+ }
+ )
+ ];
+
+ home.packages =
+ let
+ # The configuration expected by the Firefox wrapper.
+ fcfg = {
+ enableAdobeFlash = cfg.enableAdobeFlash;
+ enableGoogleTalkPlugin = cfg.enableGoogleTalk;
+ icedtea = cfg.enableIcedTea;
+ };
+
+ # A bit of hackery to force a config into the wrapper.
+ browserName = cfg.package.browserName
+ or (builtins.parseDrvName cfg.package.name).name;
+
+ # The configuration expected by the Firefox wrapper builder.
+ bcfg = setAttrByPath [browserName] fcfg;
+
+ package =
+ if isDarwin then
+ cfg.package
+ else if versionAtLeast config.home.stateVersion "19.09" then
+ cfg.package.override { cfg = fcfg; }
+ else
+ (pkgs.wrapFirefox.override { config = bcfg; }) cfg.package { };
+ in
+ [ package ];
+
+ home.file = mkMerge (
+ [{
+ "${mozillaConfigPath}/${extensionPath}" = mkIf (cfg.extensions != []) (
+ let
+ extensionsEnv = pkgs.buildEnv {
+ name = "hm-firefox-extensions";
+ paths = cfg.extensions;
+ };
+ in {
+ source = "${extensionsEnv}/share/mozilla/${extensionPath}";
+ recursive = true;
+ }
+ );
+
+ "${firefoxConfigPath}/profiles.ini" = mkIf (cfg.profiles != {}) {
+ text = profilesIni;
+ };
+ }]
+ ++ flip mapAttrsToList cfg.profiles (_: profile: {
+ "${profilesPath}/${profile.path}/chrome/userChrome.css" =
+ mkIf (profile.userChrome != "") {
+ text = profile.userChrome;
+ };
+
+ "${profilesPath}/${profile.path}/user.js" =
+ mkIf (profile.settings != {} || profile.extraConfig != "") {
+ text = mkUserJs profile.settings profile.extraConfig;
+ };
+ })
+ );
+ };
+}
diff --git a/home-manager/modules/programs/fish.nix b/home-manager/modules/programs/fish.nix
new file mode 100644
index 00000000000..87a17b85507
--- /dev/null
+++ b/home-manager/modules/programs/fish.nix
@@ -0,0 +1,191 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.fish;
+
+ abbrsStr = concatStringsSep "\n" (
+ mapAttrsToList (k: v: "abbr --add --global ${k} '${v}'") cfg.shellAbbrs
+ );
+
+ aliasesStr = concatStringsSep "\n" (
+ mapAttrsToList (k: v: "alias ${k}='${v}'") cfg.shellAliases
+ );
+
+in
+
+{
+ options = {
+ programs.fish = {
+ enable = mkEnableOption "fish friendly interactive shell";
+
+ package = mkOption {
+ default = pkgs.fish;
+ defaultText = literalExample "pkgs.fish";
+ description = ''
+ The fish package to install. May be used to change the version.
+ '';
+ type = types.package;
+ };
+
+ shellAliases = mkOption {
+ default = {};
+ description = ''
+ Set of aliases for fish shell. See
+ <option>environment.shellAliases</option> for an option
+ format description.
+ '';
+ type = types.attrs;
+ };
+
+ shellAbbrs = mkOption {
+ default = {};
+ description = ''
+ Set of abbreviations for fish shell.
+ '';
+ type = types.attrs;
+ };
+
+ shellInit = mkOption {
+ default = "";
+ description = ''
+ Shell script code called during fish shell initialisation.
+ '';
+ type = types.lines;
+ };
+
+ loginShellInit = mkOption {
+ default = "";
+ description = ''
+ Shell script code called during fish login shell initialisation.
+ '';
+ type = types.lines;
+ };
+
+ interactiveShellInit = mkOption {
+ default = "";
+ description = ''
+ Shell script code called during interactive fish shell initialisation.
+ '';
+ type = types.lines;
+ };
+
+ promptInit = mkOption {
+ default = "";
+ description = ''
+ Shell script code used to initialise fish prompt.
+ '';
+ type = types.lines;
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ cfg.package ];
+
+ xdg.dataFile."fish/home-manager_generated_completions".source =
+ let
+ # paths later in the list will overwrite those already linked
+ destructiveSymlinkJoin =
+ args_@{ name
+ , paths
+ , preferLocalBuild ? true
+ , allowSubstitutes ? false
+ , postBuild ? ""
+ , ...
+ }:
+ let
+ args = removeAttrs args_ [ "name" "postBuild" ]
+ // { inherit preferLocalBuild allowSubstitutes; }; # pass the defaults
+ in pkgs.runCommand name args
+ ''
+ mkdir -p $out
+ for i in $paths; do
+ if [ -z "$(find $i -prune -empty)" ]; then
+ cp -srf $i/* $out
+ fi
+ done
+ ${postBuild}
+ '';
+ generateCompletions = package: pkgs.runCommand
+ "${package.name}-fish-completions"
+ {
+ src = package;
+ nativeBuildInputs = [ pkgs.python2 ];
+ buildInputs = [ cfg.package ];
+ preferLocalBuild = true;
+ allowSubstitutes = false;
+ }
+ ''
+ mkdir -p $out
+ if [ -d $src/share/man ]; then
+ find $src/share/man -type f \
+ | xargs python ${cfg.package}/share/fish/tools/create_manpage_completions.py --directory $out \
+ > /dev/null
+ fi
+ '';
+ in
+ destructiveSymlinkJoin {
+ name = "${config.home.username}-fish-completions";
+ paths =
+ let
+ cmp = (a: b: (a.meta.priority or 0) > (b.meta.priority or 0));
+ in
+ map generateCompletions (sort cmp config.home.packages);
+ };
+
+ programs.fish.interactiveShellInit = ''
+ # add completions generated by Home Manager to $fish_complete_path
+ begin
+ set -l joined (string join " " $fish_complete_path)
+ set -l prev_joined (string replace --regex "[^\s]*generated_completions.*" "" $joined)
+ set -l post_joined (string replace $prev_joined "" $joined)
+ set -l prev (string split " " (string trim $prev_joined))
+ set -l post (string split " " (string trim $post_joined))
+ set fish_complete_path $prev "${config.xdg.dataHome}/fish/home-manager_generated_completions" $post
+ end
+ '';
+
+ xdg.configFile."fish/config.fish".text = ''
+ # ~/.config/fish/config.fish: DO NOT EDIT -- this file has been generated automatically.
+ # if we haven't sourced the general config, do it
+ if not set -q __fish_general_config_sourced
+ set fish_function_path ${pkgs.fish-foreign-env}/share/fish-foreign-env/functions $fish_function_path
+ fenv source ${config.home.profileDirectory}/etc/profile.d/hm-session-vars.sh > /dev/null
+ set -e fish_function_path[1]
+
+ ${cfg.shellInit}
+ # and leave a note so we don't source this config section again from
+ # this very shell (children will source the general config anew)
+ set -g __fish_general_config_sourced 1
+ end
+ # if we haven't sourced the login config, do it
+ status --is-login; and not set -q __fish_login_config_sourced
+ and begin
+
+ ${cfg.loginShellInit}
+ # and leave a note so we don't source this config section again from
+ # this very shell (children will source the general config anew)
+ set -g __fish_login_config_sourced 1
+ end
+ # if we haven't sourced the interactive config, do it
+ status --is-interactive; and not set -q __fish_interactive_config_sourced
+ and begin
+ # Abbrs
+ ${abbrsStr}
+
+ # Aliases
+ ${aliasesStr}
+
+ ${cfg.promptInit}
+ ${cfg.interactiveShellInit}
+ # and leave a note so we don't source this config section again from
+ # this very shell (children will source the general config anew,
+ # allowing configuration changes in, e.g, aliases, to propagate)
+ set -g __fish_interactive_config_sourced 1
+ end
+ '';
+ };
+}
diff --git a/home-manager/modules/programs/fzf.nix b/home-manager/modules/programs/fzf.nix
new file mode 100644
index 00000000000..36eb3a1cdba
--- /dev/null
+++ b/home-manager/modules/programs/fzf.nix
@@ -0,0 +1,134 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.fzf;
+
+in {
+ options.programs.fzf = {
+ enable = mkEnableOption "fzf - a command-line fuzzy finder";
+
+ defaultCommand = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ example = "fd --type f";
+ description = ''
+ The command that gets executed as the default source for fzf
+ when running.
+ '';
+ };
+
+ defaultOptions = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ example = [ "--height 40%" "--border" ];
+ description = ''
+ Extra command line options given to fzf by default.
+ '';
+ };
+
+ fileWidgetCommand = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ example = "fd --type f";
+ description = ''
+ The command that gets executed as the source for fzf for the
+ CTRL-T keybinding.
+ '';
+ };
+
+ fileWidgetOptions = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ example = [ "--preview 'head {}'" ];
+ description = ''
+ Command line options for the CTRL-T keybinding.
+ '';
+ };
+
+ changeDirWidgetCommand = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ example = "fd --type d";
+ description = ''
+ The command that gets executed as the source for fzf for the
+ ALT-C keybinding.
+ '';
+ };
+
+ changeDirWidgetOptions = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ example = [ "--preview 'tree -C {} | head -200'" ];
+ description = ''
+ Command line options for the ALT-C keybinding.
+ '';
+ };
+
+ historyWidgetCommand = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = ''
+ The command that gets executed as the source for fzf for the
+ CTRL-R keybinding.
+ '';
+ };
+
+ historyWidgetOptions = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ example = [ "--sort" "--exact" ];
+ description = ''
+ Command line options for the CTRL-R keybinding.
+ '';
+ };
+
+ enableBashIntegration = mkOption {
+ default = true;
+ type = types.bool;
+ description = ''
+ Whether to enable Bash integration.
+ '';
+ };
+
+ enableZshIntegration = mkOption {
+ default = true;
+ type = types.bool;
+ description = ''
+ Whether to enable Zsh integration.
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ pkgs.fzf ];
+
+ home.sessionVariables = mapAttrs (n: v: toString v)
+ (filterAttrs (n: v: v != [ ] && v != null) {
+ FZF_ALT_C_COMMAND = cfg.changeDirWidgetCommand;
+ FZF_ALT_C_OPTS = cfg.changeDirWidgetOptions;
+ FZF_CTRL_R_COMMAND = cfg.historyWidgetCommand;
+ FZF_CTRL_R_OPTS = cfg.historyWidgetOptions;
+ FZF_CTRL_T_COMMAND = cfg.fileWidgetCommand;
+ FZF_CTRL_T_OPTS = cfg.fileWidgetOptions;
+ FZF_DEFAULT_COMMAND = cfg.defaultCommand;
+ FZF_DEFAULT_OPTS = cfg.defaultOptions;
+ });
+
+ programs.bash.initExtra = mkIf cfg.enableBashIntegration ''
+ if [[ :$SHELLOPTS: =~ :(vi|emacs): ]]; then
+ . ${pkgs.fzf}/share/fzf/completion.bash
+ . ${pkgs.fzf}/share/fzf/key-bindings.bash
+ fi
+ '';
+
+ programs.zsh.initExtra = mkIf cfg.enableZshIntegration ''
+ if [[ $options[zle] = on ]]; then
+ . ${pkgs.fzf}/share/fzf/completion.zsh
+ . ${pkgs.fzf}/share/fzf/key-bindings.zsh
+ fi
+ '';
+ };
+}
diff --git a/home-manager/modules/programs/getmail-accounts.nix b/home-manager/modules/programs/getmail-accounts.nix
new file mode 100644
index 00000000000..24eb4fb588a
--- /dev/null
+++ b/home-manager/modules/programs/getmail-accounts.nix
@@ -0,0 +1,49 @@
+{ config, lib, ... }:
+
+with lib;
+
+{
+ options.getmail = {
+ enable = mkEnableOption "the getmail mail retriever for this account";
+
+ destinationCommand = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ example = "\${pkgs.maildrop}/bin/maildrop";
+ description = ''
+ Specify a command delivering the incoming mail to your maildir.
+ '';
+ };
+
+ mailboxes = mkOption {
+ type = types.nonEmptyListOf types.str;
+ default = [ ];
+ example = [ "INBOX" "INBOX.spam" ];
+ description = ''
+ A non-empty list of mailboxes. To download all mail you can
+ use the <literal>ALL</literal> mailbox.
+ '';
+ };
+
+ delete = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Enable if you want to delete read messages from the server. Most
+ users should either enable <literal>delete</literal> or disable
+ <literal>readAll</literal>.
+ '';
+ };
+
+ readAll = mkOption {
+ type = types.bool;
+ default = true;
+ description = ''
+ Enable if you want to fetch all, even the read messages from the
+ server. Most users should either enable <literal>delete</literal> or
+ disable <literal>readAll</literal>.
+ '';
+ };
+
+ };
+}
diff --git a/home-manager/modules/programs/getmail.nix b/home-manager/modules/programs/getmail.nix
new file mode 100644
index 00000000000..2c3919dcf2f
--- /dev/null
+++ b/home-manager/modules/programs/getmail.nix
@@ -0,0 +1,57 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ accounts =
+ filter (a: a.getmail.enable) (attrValues config.accounts.email.accounts);
+
+ renderAccountConfig = account:
+ with account;
+ let
+ passCmd = concatMapStringsSep ", " (x: "'${x}'") passwordCommand;
+ renderedMailboxes =
+ concatMapStringsSep ", " (x: "'${x}'") getmail.mailboxes;
+ retrieverType = if imap.tls.enable then
+ "SimpleIMAPSSLRetriever"
+ else
+ "SimpleIMAPRetriever";
+ destination = if getmail.destinationCommand != null then {
+ destinationType = "MDA_external";
+ destinationPath = getmail.destinationCommand;
+ } else {
+ destinationType = "Maildir";
+ destinationPath = "${maildir.absPath}/";
+ };
+ renderGetmailBoolean = v: if v then "true" else "false";
+ in ''
+ # Generated by Home-Manager.
+ [retriever]
+ type = ${retrieverType}
+ server = ${imap.host}
+ ${optionalString (imap.port != null) "port = ${toString imap.port}"}
+ username = ${userName}
+ password_command = (${passCmd})
+ mailboxes = ( ${renderedMailboxes} )
+
+ [destination]
+ type = ${destination.destinationType}
+ path = ${destination.destinationPath}
+
+ [options]
+ delete = ${renderGetmailBoolean getmail.delete}
+ read_all = ${renderGetmailBoolean getmail.readAll}
+ '';
+ getmailEnabled = length (filter (a: a.getmail.enable) accounts) > 0;
+ # Watch out! This is used by the getmail.service too!
+ renderConfigFilepath = a:
+ ".getmail/getmail${if a.primary then "rc" else a.name}";
+
+in {
+ config = mkIf getmailEnabled {
+ home.file = foldl' (a: b: a // b) { }
+ (map (a: { "${renderConfigFilepath a}".text = renderAccountConfig a; })
+ accounts);
+ };
+}
diff --git a/home-manager/modules/programs/git.nix b/home-manager/modules/programs/git.nix
new file mode 100644
index 00000000000..a56aa10d50e
--- /dev/null
+++ b/home-manager/modules/programs/git.nix
@@ -0,0 +1,303 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.git;
+
+ # create [section "subsection"] keys from "section.subsection" attrset names
+ mkSectionName = name:
+ let
+ containsQuote = strings.hasInfix ''"'' name;
+ sections = splitString "." name;
+ section = head sections;
+ subsections = tail sections;
+ subsection = concatStringsSep "." subsections;
+ in if containsQuote || subsections == [ ] then
+ name
+ else
+ ''${section} "${subsection}"'';
+
+ # generation for multiple ini values
+ mkKeyValue = k: v:
+ let mkKeyValue = generators.mkKeyValueDefault { } "=" k;
+ in concatStringsSep "\n" (map mkKeyValue (toList v));
+
+ # converts { a.b.c = 5; } to { "a.b".c = 5; } for toINI
+ gitFlattenAttrs = let
+ recurse = path: value:
+ if isAttrs value then
+ mapAttrsToList (name: value: recurse ([ name ] ++ path) value) value
+ else if length path > 1 then {
+ ${concatStringsSep "." (reverseList (tail path))}.${head path} = value;
+ } else {
+ ${head path} = value;
+ };
+ in attrs: foldl recursiveUpdate { } (flatten (recurse [ ] attrs));
+
+ gitToIni = attrs:
+ let toIni = generators.toINI { inherit mkKeyValue mkSectionName; };
+ in toIni (gitFlattenAttrs attrs);
+
+ gitIniType = with types;
+ let
+ primitiveType = either str (either bool int);
+ multipleType = either primitiveType (listOf primitiveType);
+ sectionType = attrsOf multipleType;
+ supersectionType = attrsOf (either multipleType sectionType);
+ in attrsOf supersectionType;
+
+ signModule = types.submodule {
+ options = {
+ key = mkOption {
+ type = types.str;
+ description = "The default GPG signing key fingerprint.";
+ };
+
+ signByDefault = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Whether commits should be signed by default.";
+ };
+
+ gpgPath = mkOption {
+ type = types.str;
+ default = "${pkgs.gnupg}/bin/gpg2";
+ defaultText = "\${pkgs.gnupg}/bin/gpg2";
+ description = "Path to GnuPG binary to use.";
+ };
+ };
+ };
+
+ includeModule = types.submodule ({ config, ... }: {
+ options = {
+ condition = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = ''
+ Include this configuration only when <varname>condition</varname>
+ matches. Allowed conditions are described in
+ <citerefentry>
+ <refentrytitle>git-config</refentrytitle>
+ <manvolnum>1</manvolnum>
+ </citerefentry>.
+ '';
+ };
+
+ path = mkOption {
+ type = with types; either str path;
+ description = "Path of the configuration file to include.";
+ };
+
+ contents = mkOption {
+ type = types.attrs;
+ default = { };
+ description = ''
+ Configuration to include. If empty then a path must be given.
+ '';
+ };
+ };
+
+ config.path = mkIf (config.contents != { })
+ (mkDefault (pkgs.writeText "contents" (gitToIni config.contents)));
+ });
+
+in {
+ meta.maintainers = [ maintainers.rycee ];
+
+ options = {
+ programs.git = {
+ enable = mkEnableOption "Git";
+
+ package = mkOption {
+ type = types.package;
+ default = pkgs.git;
+ defaultText = literalExample "pkgs.git";
+ description = ''
+ Git package to install. Use <varname>pkgs.gitAndTools.gitFull</varname>
+ to gain access to <command>git send-email</command> for instance.
+ '';
+ };
+
+ userName = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = "Default user name to use.";
+ };
+
+ userEmail = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = "Default user email to use.";
+ };
+
+ aliases = mkOption {
+ type = types.attrsOf types.str;
+ default = { };
+ example = { co = "checkout"; };
+ description = "Git aliases to define.";
+ };
+
+ signing = mkOption {
+ type = types.nullOr signModule;
+ default = null;
+ description = "Options related to signing commits using GnuPG.";
+ };
+
+ extraConfig = mkOption {
+ type = types.either types.lines gitIniType;
+ default = { };
+ example = {
+ core = { whitespace = "trailing-space,space-before-tab"; };
+ url."ssh://git@host".insteadOf = "otherhost";
+ };
+ description = ''
+ Additional configuration to add. The use of string values is
+ deprecated and will be removed in the future.
+ '';
+ };
+
+ iniContent = mkOption {
+ type = gitIniType;
+ internal = true;
+ };
+
+ ignores = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ example = [ "*~" "*.swp" ];
+ description = "List of paths that should be globally ignored.";
+ };
+
+ attributes = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ example = [ "*.pdf diff=pdf" ];
+ description = "List of defining attributes set globally.";
+ };
+
+ includes = mkOption {
+ type = types.listOf includeModule;
+ default = [ ];
+ example = literalExample ''
+ [
+ { path = "~/path/to/config.inc"; }
+ {
+ path = "~/path/to/conditional.inc";
+ condition = "gitdir:~/src/dir";
+ }
+ ]
+ '';
+ description = "List of configuration files to include.";
+ };
+
+ lfs = {
+ enable = mkEnableOption "Git Large File Storage";
+
+ skipSmudge = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Skip automatic downloading of objects on clone or pull.
+ This requires a manual <command>git lfs pull</command>
+ every time a new commit is checked out on your repository.
+ '';
+ };
+ };
+ };
+ };
+
+ config = mkIf cfg.enable (mkMerge [
+ {
+ home.packages = [ cfg.package ];
+
+ programs.git.iniContent.user = {
+ name = mkIf (cfg.userName != null) cfg.userName;
+ email = mkIf (cfg.userEmail != null) cfg.userEmail;
+ };
+
+ xdg.configFile = {
+ "git/config".text = gitToIni cfg.iniContent;
+
+ "git/ignore" = mkIf (cfg.ignores != [ ]) {
+ text = concatStringsSep "\n" cfg.ignores + "\n";
+ };
+
+ "git/attributes" = mkIf (cfg.attributes != [ ]) {
+ text = concatStringsSep "\n" cfg.attributes + "\n";
+ };
+ };
+ }
+
+ {
+ programs.git.iniContent = let
+ hasSmtp = name: account: account.smtp != null;
+
+ genIdentity = name: account:
+ with account;
+ nameValuePair "sendemail.${name}" ({
+ smtpEncryption = if smtp.tls.enable then "tls" else "";
+ smtpServer = smtp.host;
+ smtpUser = userName;
+ from = address;
+ } // optionalAttrs (smtp.port != null) {
+ smtpServerPort = smtp.port;
+ });
+ in mapAttrs' genIdentity
+ (filterAttrs hasSmtp config.accounts.email.accounts);
+ }
+
+ (mkIf (cfg.signing != null) {
+ programs.git.iniContent = {
+ user.signingKey = cfg.signing.key;
+ commit.gpgSign = cfg.signing.signByDefault;
+ gpg.program = cfg.signing.gpgPath;
+ };
+ })
+
+ (mkIf (cfg.aliases != { }) { programs.git.iniContent.alias = cfg.aliases; })
+
+ (mkIf (lib.isAttrs cfg.extraConfig) {
+ programs.git.iniContent = cfg.extraConfig;
+ })
+
+ (mkIf (lib.isString cfg.extraConfig) {
+ warnings = [''
+ Using programs.git.extraConfig as a string option is
+ deprecated and will be removed in the future. Please
+ change to using it as an attribute set instead.
+ ''];
+
+ xdg.configFile."git/config".text = cfg.extraConfig;
+ })
+
+ (mkIf (cfg.includes != [ ]) {
+ xdg.configFile."git/config".text = let
+ include = i:
+ with i;
+ if condition != null then {
+ includeIf.${condition}.path = "${path}";
+ } else {
+ include.path = "${path}";
+ };
+ in mkAfter
+ (concatStringsSep "\n" (map gitToIni (map include cfg.includes)));
+ })
+
+ (mkIf cfg.lfs.enable {
+ home.packages = [ pkgs.git-lfs ];
+
+ programs.git.iniContent.filter.lfs =
+ let skipArg = optional cfg.lfs.skipSmudge "--skip";
+ in {
+ clean = "git-lfs clean -- %f";
+ process =
+ concatStringsSep " " ([ "git-lfs" "filter-process" ] ++ skipArg);
+ required = true;
+ smudge = concatStringsSep " "
+ ([ "git-lfs" "smudge" ] ++ skipArg ++ [ "--" "%f" ]);
+ };
+ })
+ ]);
+}
diff --git a/home-manager/modules/programs/gnome-terminal.nix b/home-manager/modules/programs/gnome-terminal.nix
new file mode 100644
index 00000000000..570a1fc7df0
--- /dev/null
+++ b/home-manager/modules/programs/gnome-terminal.nix
@@ -0,0 +1,216 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.gnome-terminal;
+
+ vteInitStr = ''
+ # gnome-terminal: Show current directory in the terminal window title.
+ . ${pkgs.gnome3.vte}/etc/profile.d/vte.sh
+ '';
+
+ backForeSubModule = types.submodule ({ ... }: {
+ options = {
+ foreground = mkOption {
+ type = types.str;
+ description = "The foreground color.";
+ };
+
+ background = mkOption {
+ type = types.str;
+ description = "The background color.";
+ };
+ };
+ });
+
+ profileColorsSubModule = types.submodule ({ ... }: {
+ options = {
+ foregroundColor = mkOption {
+ type = types.str;
+ description = "The foreground color.";
+ };
+
+ backgroundColor = mkOption {
+ type = types.str;
+ description = "The background color.";
+ };
+
+ boldColor = mkOption {
+ default = null;
+ type = types.nullOr types.str;
+ description = "The bold color, null to use same as foreground.";
+ };
+
+ palette = mkOption {
+ type = types.listOf types.str;
+ description = "The terminal palette.";
+ };
+
+ cursor = mkOption {
+ default = null;
+ type = types.nullOr backForeSubModule;
+ description = "The color for the terminal cursor.";
+ };
+
+ highlight = mkOption {
+ default = null;
+ type = types.nullOr backForeSubModule;
+ description = "The colors for the terminal’s highlighted area.";
+ };
+ };
+ });
+
+ profileSubModule = types.submodule ({ name, config, ... }: {
+ options = {
+ default = mkOption {
+ default = false;
+ type = types.bool;
+ description = "Whether this should be the default profile.";
+ };
+
+ visibleName = mkOption {
+ type = types.str;
+ description = "The profile name.";
+ };
+
+ colors = mkOption {
+ default = null;
+ type = types.nullOr profileColorsSubModule;
+ description = "The terminal colors, null to use system default.";
+ };
+
+ cursorShape = mkOption {
+ default = "block";
+ type = types.enum [ "block" "ibeam" "underline" ];
+ description = "The cursor shape.";
+ };
+
+ font = mkOption {
+ default = null;
+ type = types.nullOr types.str;
+ description = "The font name, null to use system default.";
+ };
+
+ allowBold = mkOption {
+ default = null;
+ type = types.nullOr types.bool;
+ description = ''
+ If <literal>true</literal>, allow applications in the
+ terminal to make text boldface.
+ '';
+ };
+
+ scrollOnOutput = mkOption {
+ default = true;
+ type = types.bool;
+ description = "Whether to scroll when output is written.";
+ };
+
+ showScrollbar = mkOption {
+ default = true;
+ type = types.bool;
+ description = "Whether the scroll bar should be visible.";
+ };
+
+ scrollbackLines = mkOption {
+ default = 10000;
+ type = types.nullOr types.int;
+ description = ''
+ The number of scrollback lines to keep, null for infinite.
+ '';
+ };
+ };
+ });
+
+ buildProfileSet = pcfg:
+ {
+ visible-name = pcfg.visibleName;
+ scrollbar-policy = if pcfg.showScrollbar then "always" else "never";
+ scrollback-lines = pcfg.scrollbackLines;
+ cursor-shape = pcfg.cursorShape;
+ } // (if (pcfg.font == null) then {
+ use-system-font = true;
+ } else {
+ use-system-font = false;
+ font = pcfg.font;
+ }) // (if (pcfg.colors == null) then {
+ use-theme-colors = true;
+ } else
+ ({
+ use-theme-colors = false;
+ foreground-color = pcfg.colors.foregroundColor;
+ background-color = pcfg.colors.backgroundColor;
+ palette = pcfg.colors.palette;
+ } // optionalAttrs (pcfg.allowBold != null) {
+ allow-bold = pcfg.allowBold;
+ } // (if (pcfg.colors.boldColor == null) then {
+ bold-color-same-as-fg = true;
+ } else {
+ bold-color-same-as-fg = false;
+ bold-color = pcfg.colors.boldColor;
+ }) // (if (pcfg.colors.cursor != null) then {
+ cursor-colors-set = true;
+ cursor-foreground-color = pcfg.colors.cursor.foreground;
+ cursor-background-color = pcfg.colors.cursor.background;
+ } else {
+ cursor-colors-set = false;
+ }) // (if (pcfg.colors.highlight != null) then {
+ highlight-colors-set = true;
+ highlight-foreground-color = pcfg.colors.highlight.foreground;
+ highlight-background-color = pcfg.colors.highlight.background;
+ } else {
+ highlight-colors-set = false;
+ })));
+
+in {
+ meta.maintainers = [ maintainers.rycee ];
+
+ options = {
+ programs.gnome-terminal = {
+ enable = mkEnableOption "Gnome Terminal";
+
+ showMenubar = mkOption {
+ default = true;
+ type = types.bool;
+ description = "Whether to show the menubar by default";
+ };
+
+ themeVariant = mkOption {
+ default = "default";
+ type = types.enum [ "default" "light" "dark" ];
+ description = "The theme variation to request";
+ };
+
+ profile = mkOption {
+ default = { };
+ type = types.attrsOf profileSubModule;
+ description = "A set of Gnome Terminal profiles.";
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ pkgs.gnome3.gnome_terminal ];
+
+ dconf.settings = let dconfPath = "org/gnome/terminal/legacy";
+ in {
+ "${dconfPath}" = {
+ default-show-menubar = cfg.showMenubar;
+ theme-variant = cfg.themeVariant;
+ schema-version = 3;
+ };
+
+ "${dconfPath}/profiles:" = {
+ default = head (attrNames (filterAttrs (n: v: v.default) cfg.profile));
+ list = attrNames cfg.profile;
+ };
+ } // mapAttrs'
+ (n: v: nameValuePair ("${dconfPath}/profiles:/:${n}") (buildProfileSet v))
+ cfg.profile;
+
+ programs.bash.initExtra = mkBefore vteInitStr;
+ programs.zsh.initExtra = vteInitStr;
+ };
+}
diff --git a/home-manager/modules/programs/go.nix b/home-manager/modules/programs/go.nix
new file mode 100644
index 00000000000..983769d26af
--- /dev/null
+++ b/home-manager/modules/programs/go.nix
@@ -0,0 +1,89 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.go;
+
+in {
+ meta.maintainers = [ maintainers.rvolosatovs ];
+
+ options = {
+ programs.go = {
+ enable = mkEnableOption "Go";
+
+ package = mkOption {
+ type = types.package;
+ default = pkgs.go;
+ defaultText = literalExample "pkgs.go";
+ description = "The Go package to use.";
+ };
+
+ packages = mkOption {
+ type = with types; attrsOf path;
+ default = { };
+ example = literalExample ''
+ {
+ "golang.org/x/text" = builtins.fetchGit "https://go.googlesource.com/text";
+ "golang.org/x/time" = builtins.fetchGit "https://go.googlesource.com/time";
+ }
+ '';
+ description = "Packages to add to GOPATH.";
+ };
+
+ goPath = mkOption {
+ type = with types; nullOr str;
+ default = null;
+ example = "go";
+ description = ''
+ Primary <envar>GOPATH</envar> relative to
+ <envar>HOME</envar>. It will be exported first and therefore
+ used by default by the Go tooling.
+ '';
+ };
+
+ extraGoPaths = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ example = [ "extraGoPath1" "extraGoPath2" ];
+ description = let goPathOpt = "programs.go.goPath";
+ in ''
+ Extra <envar>GOPATH</envar>s relative to <envar>HOME</envar> appended
+ after
+ <varname><link linkend="opt-${goPathOpt}">${goPathOpt}</link></varname>,
+ if that option is set.
+ '';
+ };
+
+ goBin = mkOption {
+ type = with types; nullOr str;
+ default = null;
+ example = ".local/bin.go";
+ description = "GOBIN relative to HOME";
+ };
+ };
+ };
+
+ config = mkIf cfg.enable (mkMerge [
+ {
+ home.packages = [ cfg.package ];
+
+ home.file = let
+ goPath = if cfg.goPath != null then cfg.goPath else "go";
+ mkSrc = n: v: { "${goPath}/src/${n}".source = v; };
+ in foldl' (a: b: a // b) { } (mapAttrsToList mkSrc cfg.packages);
+ }
+
+ (mkIf (cfg.goPath != null) {
+ home.sessionVariables.GOPATH = concatStringsSep ":" (map builtins.toPath
+ (map (path: "${config.home.homeDirectory}/${path}")
+ ([ cfg.goPath ] ++ cfg.extraGoPaths)));
+ })
+
+ (mkIf (cfg.goBin != null) {
+ home.sessionVariables.GOBIN =
+ builtins.toPath "${config.home.homeDirectory}/${cfg.goBin}";
+ })
+ ]);
+}
diff --git a/home-manager/modules/programs/gpg.nix b/home-manager/modules/programs/gpg.nix
new file mode 100644
index 00000000000..4588c59c882
--- /dev/null
+++ b/home-manager/modules/programs/gpg.nix
@@ -0,0 +1,61 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+ cfg = config.programs.gpg;
+
+ cfgText =
+ concatStringsSep "\n"
+ (attrValues
+ (mapAttrs (key: value:
+ if isString value
+ then "${key} ${value}"
+ else optionalString value key)
+ cfg.settings));
+
+in {
+ options.programs.gpg = {
+ enable = mkEnableOption "GnuPG";
+
+ settings = mkOption {
+ type = types.attrsOf (types.either types.str types.bool);
+ example = {
+ no-comments = false;
+ s2k-cipher-algo = "AES128";
+ };
+ description = ''
+ GnuPG configuration options. Available options are described
+ in the gpg manpage:
+ <link xlink:href="https://gnupg.org/documentation/manpage.html"/>.
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+ programs.gpg.settings = {
+ personal-cipher-preferences = mkDefault "AES256 AES192 AES";
+ personal-digest-preferences = mkDefault "SHA512 SHA384 SHA256";
+ personal-compress-preferences = mkDefault "ZLIB BZIP2 ZIP Uncompressed";
+ default-preference-list = mkDefault "SHA512 SHA384 SHA256 AES256 AES192 AES ZLIB BZIP2 ZIP Uncompressed";
+ cert-digest-algo = mkDefault "SHA512";
+ s2k-digest-algo = mkDefault "SHA512";
+ s2k-cipher-algo = mkDefault "AES256";
+ charset = mkDefault "utf-8";
+ fixed-list-mode = mkDefault true;
+ no-comments = mkDefault true;
+ no-emit-version = mkDefault true;
+ keyid-format = mkDefault "0xlong";
+ list-options = mkDefault "show-uid-validity";
+ verify-options = mkDefault "show-uid-validity";
+ with-fingerprint = mkDefault true;
+ require-cross-certification = mkDefault true;
+ no-symkey-cache = mkDefault true;
+ use-agent = mkDefault true;
+ };
+
+ home.packages = [ pkgs.gnupg ];
+
+ home.file.".gnupg/gpg.conf".text = cfgText;
+ };
+}
diff --git a/home-manager/modules/programs/home-manager.nix b/home-manager/modules/programs/home-manager.nix
new file mode 100644
index 00000000000..9039a59d7c5
--- /dev/null
+++ b/home-manager/modules/programs/home-manager.nix
@@ -0,0 +1,37 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.home-manager;
+
+ dag = config.lib.dag;
+
+in {
+ meta.maintainers = [ maintainers.rycee ];
+
+ options = {
+ programs.home-manager = {
+ enable = mkEnableOption "Home Manager";
+
+ path = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ example = "$HOME/devel/home-manager";
+ description = ''
+ The default path to use for Home Manager. If this path does
+ not exist then
+ <filename>$HOME/.config/nixpkgs/home-manager</filename> and
+ <filename>$HOME/.nixpkgs/home-manager</filename> will be
+ attempted.
+ '';
+ };
+ };
+ };
+
+ config = mkIf (cfg.enable && !config.submoduleSupport.enable) {
+ home.packages =
+ [ (pkgs.callPackage ../../home-manager { inherit (cfg) path; }) ];
+ };
+}
diff --git a/home-manager/modules/programs/htop.nix b/home-manager/modules/programs/htop.nix
new file mode 100644
index 00000000000..84966040534
--- /dev/null
+++ b/home-manager/modules/programs/htop.nix
@@ -0,0 +1,370 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.htop;
+
+ list = xs: concatMapStrings (x: "${toString x} ") xs;
+
+ bool = b: if b then "1" else "0";
+
+ fields = {
+ PID = 0;
+ COMM = 1;
+ STATE = 2;
+ PPID = 3;
+ PGRP = 4;
+ SESSION = 5;
+ TTY_NR = 6;
+ TPGID = 7;
+ MINFLT = 9;
+ MAJFLT = 11;
+ PRIORITY = 17;
+ NICE = 18;
+ STARTTIME = 20;
+ PROCESSOR = 37;
+ M_SIZE = 38;
+ M_RESIDENT = 39;
+ ST_UID = 45;
+ PERCENT_CPU = 46;
+ PERCENT_MEM = 47;
+ USER = 48;
+ TIME = 49;
+ NLWP = 50;
+ TGID = 51;
+ CMINFLT = 10;
+ CMAJFLT = 12;
+ UTIME = 13;
+ STIME = 14;
+ CUTIME = 15;
+ CSTIME = 16;
+ M_SHARE = 40;
+ M_TRS = 41;
+ M_DRS = 42;
+ M_LRS = 43;
+ M_DT = 44;
+ CTID = 99;
+ VPID = 100;
+ VXID = 102;
+ RCHAR = 102;
+ WCHAR = 103;
+ SYSCR = 104;
+ SYSCW = 105;
+ RBYTES = 106;
+ WBYTES = 107;
+ CNCLWB = 108;
+ IO_READ_RATE = 109;
+ IO_WRITE_RATE = 110;
+ IO_RATE = 111;
+ CGROUP = 112;
+ OOM = 113;
+ IO_PRIORITY = 114;
+ };
+
+ # Mapping from names to defaults
+ meters = {
+ Clock = 2;
+ LoadAverage = 2;
+ Load = 2;
+ Memory = 1;
+ Swap = 1;
+ Tasks = 2;
+ Uptime = 2;
+ Battery = 2;
+ Hostname = 2;
+ AllCPUs = 1;
+ AllCPUs2 = 1;
+ LeftCPUs = 1;
+ RightCPUs = 1;
+ LeftCPUs2 = 1;
+ RightCPUs2 = 1;
+ Blank = 2;
+ CPU = 1;
+ "CPU(1)" = 1;
+ "CPU(2)" = 1;
+ "CPU(3)" = 1;
+ "CPU(4)" = 1;
+ };
+
+ singleMeterType = let
+ meterEnum = types.enum (attrNames meters);
+ meterSubmodule = types.submodule {
+ options = {
+ kind = mkOption {
+ type = types.enum (attrNames meters);
+ example = "AllCPUs";
+ description = "What kind of meter.";
+ };
+
+ mode = mkOption {
+ type = types.enum [ 1 2 3 4 ];
+ example = 2;
+ description =
+ "Which mode the meter should use, one of 1(Bar) 2(Text) 3(Graph) 4(LED).";
+ };
+ };
+ };
+ in types.coercedTo meterEnum (m: {
+ kind = m;
+ mode = meters.${m};
+ }) meterSubmodule;
+
+ meterType = types.submodule {
+ options = {
+ left = mkOption {
+ description = "Meters shown in the left header.";
+ default = [ "AllCPUs" "Memory" "Swap" ];
+ example = [
+ "Memory"
+ "LeftCPUs2"
+ "RightCPUs2"
+ {
+ kind = "CPU";
+ mode = 3;
+ }
+ ];
+ type = types.listOf singleMeterType;
+ };
+ right = mkOption {
+ description = "Meters shown in the right header.";
+ default = [ "Tasks" "LoadAverage" "Uptime" ];
+ example = [
+ {
+ kind = "Clock";
+ mode = 4;
+ }
+ "Uptime"
+ "Tasks"
+ ];
+ type = types.listOf singleMeterType;
+ };
+ };
+ };
+
+in {
+ options.programs.htop = {
+ enable = mkEnableOption "htop";
+
+ fields = mkOption {
+ type = types.listOf (types.enum (attrNames fields));
+ default = [
+ "PID"
+ "USER"
+ "PRIORITY"
+ "NICE"
+ "M_SIZE"
+ "M_RESIDENT"
+ "M_SHARE"
+ "STATE"
+ "PERCENT_CPU"
+ "PERCENT_MEM"
+ "TIME"
+ "COMM"
+ ];
+ example = [
+ "PID"
+ "USER"
+ "PRIORITY"
+ "PERCENT_CPU"
+ "M_RESIDENT"
+ "PERCENT_MEM"
+ "TIME"
+ "COMM"
+ ];
+ description = "Active fields shown in the table.";
+ };
+
+ sortKey = mkOption {
+ type = types.enum (attrNames fields);
+ default = "PERCENT_CPU";
+ example = "TIME";
+ description = "Which field to use for sorting.";
+ };
+
+ sortDescending = mkOption {
+ type = types.bool;
+ default = true;
+ description = "Whether to sort descending or not.";
+ };
+
+ hideThreads = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Hide threads.";
+ };
+
+ hideKernelThreads = mkOption {
+ type = types.bool;
+ default = true;
+ description = "Hide kernel threads.";
+ };
+
+ hideUserlandThreads = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Hide userland process threads.";
+ };
+
+ shadowOtherUsers = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Shadow other users' processes.";
+ };
+
+ showThreadNames = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Show custom thread names.";
+ };
+
+ showProgramPath = mkOption {
+ type = types.bool;
+ default = true;
+ description = "Show program path.";
+ };
+
+ highlightBaseName = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Highlight program <quote>basename</quote>.";
+ };
+
+ highlightMegabytes = mkOption {
+ type = types.bool;
+ default = true;
+ description = "Highlight large numbers in memory counters.";
+ };
+
+ highlightThreads = mkOption {
+ type = types.bool;
+ default = true;
+ description = "Display threads in a different color.";
+ };
+
+ treeView = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Tree view.";
+ };
+
+ headerMargin = mkOption {
+ type = types.bool;
+ default = true;
+ description = "Leave a margin around header.";
+ };
+
+ detailedCpuTime = mkOption {
+ type = types.bool;
+ default = false;
+ description =
+ "Detailed CPU time (System/IO-Wait/Hard-IRQ/Soft-IRQ/Steal/Guest).";
+ };
+
+ cpuCountFromZero = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Count CPUs from 0 instead of 1.";
+ };
+
+ updateProcessNames = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Update process names on every refresh.";
+ };
+
+ accountGuestInCpuMeter = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Add guest time in CPU meter percentage.";
+ };
+
+ colorScheme = mkOption {
+ type = types.enum [ 0 1 2 3 4 5 6 ];
+ default = 0;
+ example = 6;
+ description = "Which color scheme to use.";
+ };
+
+ delay = mkOption {
+ type = types.int;
+ default = 15;
+ example = 2;
+ description = "Set the delay between updates, in tenths of seconds.";
+ };
+
+ meters = mkOption {
+ description = "Meters shown in the header.";
+ default = {
+ left = [ "AllCPUs" "Memory" "Swap" ];
+ right = [ "Tasks" "LoadAverage" "Uptime" ];
+ };
+ example = {
+ left = [
+ "Memory"
+ "CPU"
+ "LeftCPUs2"
+ "RightCPUs2"
+ {
+ kind = "CPU";
+ mode = 3;
+ }
+ ];
+ right = [
+ {
+ kind = "Clock";
+ mode = 4;
+ }
+ "Uptime"
+ "Tasks"
+ "LoadAverage"
+ {
+ kind = "Battery";
+ mode = 1;
+ }
+ ];
+ };
+ type = meterType;
+ };
+
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ pkgs.htop ];
+
+ xdg.configFile."htop/htoprc".text = let
+ leftMeters = map (m: m.kind) cfg.meters.left;
+ leftModes = map (m: m.mode) cfg.meters.left;
+ rightMeters = map (m: m.kind) cfg.meters.right;
+ rightModes = map (m: m.mode) cfg.meters.right;
+ in ''
+ # This file is regenerated by home-manager
+ # when options are changed in the config
+ fields=${list (map (n: fields.${n}) cfg.fields)}
+ sort_key=${toString (fields.${cfg.sortKey})}
+ sort_direction=${bool cfg.sortDescending}
+ hide_threads=${bool cfg.hideThreads}
+ hide_kernel_threads=${bool cfg.hideKernelThreads}
+ hide_userland_threads=${bool cfg.hideUserlandThreads}
+ shadow_other_users=${bool cfg.shadowOtherUsers}
+ show_thread_names=${bool cfg.showThreadNames}
+ show_program_path=${bool cfg.showProgramPath}
+ highlight_base_name=${bool cfg.highlightBaseName}
+ highlight_megabytes=${bool cfg.highlightMegabytes}
+ highlight_threads=${bool cfg.highlightThreads}
+ tree_view=${bool cfg.treeView}
+ header_margin=${bool cfg.headerMargin}
+ detailed_cpu_time=${bool cfg.detailedCpuTime}
+ cpu_count_from_zero=${bool cfg.cpuCountFromZero}
+ update_process_names=${bool cfg.updateProcessNames}
+ account_guest_in_cpu_meter=${bool cfg.accountGuestInCpuMeter}
+ color_scheme=${toString cfg.colorScheme}
+ delay=${toString cfg.delay}
+ left_meters=${list leftMeters}
+ left_meter_modes=${list leftModes}
+ right_meters=${list rightMeters}
+ right_meter_modes=${list rightModes}
+ '';
+ };
+}
diff --git a/home-manager/modules/programs/info.nix b/home-manager/modules/programs/info.nix
new file mode 100644
index 00000000000..9e4a5d4aaff
--- /dev/null
+++ b/home-manager/modules/programs/info.nix
@@ -0,0 +1,75 @@
+# info.nix -- install texinfo, set INFOPATH, create `dir` file
+
+# This is a helper for the GNU info documentation system. By default,
+# the `info` command (and the Info subsystem within Emacs) gives easy
+# access to the info files stored system-wide, but not info files in
+# your ~/.nix-profile.
+
+# We set $INFOPATH to include `/run/current-system/sw/share/info` and
+# `~/.nix-profile/share/info` but it's not enough. Although info can
+# then find files when you explicitly ask for them, it doesn't show
+# them to you in the table of contents on startup. To do that requires
+# a `dir` file. NixOS keeps the system-wide `dir` file up to date, but
+# ignores home-installed packages.
+
+# So this module contains an activation script that generates the
+# `dir` for your home profile. Then when you start info (and both
+# `dir` files are in your $INFOPATH), it will *merge* the contents of
+# the two files, showing you a unified table of contents for all
+# packages. This is really nice.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.info;
+
+ # Indexes info files found in this location
+ homeInfoPath = "${config.home.profileDirectory}/share/info";
+
+ # Installs this package -- the interactive just means that it
+ # includes the curses `info` program. We also use `install-info`
+ # from this package in the activation script.
+ infoPkg = pkgs.texinfoInteractive;
+
+in {
+ options = {
+ programs.info = {
+ enable = mkEnableOption "GNU Info";
+
+ homeInfoDirLocation = mkOption {
+ default = "\${XDG_CACHE_HOME:-$HOME/.cache}/info";
+ description = ''
+ Directory in which to store the info <filename>dir</filename>
+ file within your home.
+ '';
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.sessionVariables.INFOPATH =
+ "${cfg.homeInfoDirLocation}\${INFOPATH:+:}\${INFOPATH}";
+
+ home.activation.createHomeInfoDir =
+ hm.dag.entryAfter [ "installPackages" ] ''
+ oPATH=$PATH
+ export PATH="${lib.makeBinPath [ pkgs.gzip ]}''${PATH:+:}$PATH"
+ $DRY_RUN_CMD mkdir -p "${cfg.homeInfoDirLocation}"
+ $DRY_RUN_CMD rm -f "${cfg.homeInfoDirLocation}/dir"
+ if [[ -d "${homeInfoPath}" ]]; then
+ find -L "${homeInfoPath}" \( -name '*.info' -o -name '*.info.gz' \) \
+ -exec $DRY_RUN_CMD ${infoPkg}/bin/install-info '{}' \
+ "${cfg.homeInfoDirLocation}/dir" \;
+ fi
+ export PATH="$oPATH"
+ unset oPATH
+ '';
+
+ home.packages = [ infoPkg ];
+
+ home.extraOutputsToInstall = [ "info" ];
+ };
+}
diff --git a/home-manager/modules/programs/irssi.nix b/home-manager/modules/programs/irssi.nix
new file mode 100644
index 00000000000..fc8fa8e6132
--- /dev/null
+++ b/home-manager/modules/programs/irssi.nix
@@ -0,0 +1,211 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.irssi;
+
+ boolStr = b: if b then "yes" else "no";
+ quoteStr = s: escape ["\""] s;
+
+ assignFormat = set:
+ concatStringsSep "\n"
+ (mapAttrsToList (k: v: " ${k} = \"${quoteStr v}\";") set);
+
+ chatnetString =
+ concatStringsSep "\n"
+ (flip mapAttrsToList cfg.networks
+ (k: v: ''
+ ${k} = {
+ type = "${v.type}";
+ nick = "${quoteStr v.nick}";
+ autosendcmd = "${concatMapStringsSep ";" quoteStr v.autoCommands}";
+ };
+ ''));
+
+ serversString =
+ concatStringsSep ",\n"
+ (flip mapAttrsToList cfg.networks
+ (k: v: ''
+ {
+ chatnet = "${k}";
+ address = "${v.server.address}";
+ port = "${toString v.server.port}";
+ use_ssl = "${boolStr v.server.ssl.enable}";
+ ssl_verify = "${boolStr v.server.ssl.verify}";
+ autoconnect = "${boolStr v.server.autoConnect}";
+ }
+ ''));
+
+ channelString =
+ concatStringsSep ",\n"
+ (flip mapAttrsToList cfg.networks
+ (k: v:
+ concatStringsSep ",\n"
+ (flip mapAttrsToList v.channels
+ (c: cv: ''
+ {
+ chatnet = "${k}";
+ name = "${c}";
+ autojoin = "${boolStr cv.autoJoin}";
+ }
+ ''))));
+
+ channelType = types.submodule {
+ options = {
+ name = mkOption {
+ type = types.nullOr types.str;
+ visible = false;
+ default = null;
+ description = "Name of the channel.";
+ };
+
+ autoJoin = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Whether to join this channel on connect.";
+ };
+ };
+ };
+
+ networkType = types.submodule ({ name, ...}: {
+ options = {
+ name = mkOption {
+ visible = false;
+ default = name;
+ type = types.str;
+ };
+
+ nick = mkOption {
+ type = types.str;
+ description = "Nickname in that network.";
+ };
+
+ type = mkOption {
+ type = types.str;
+ description = "Type of the network.";
+ default = "IRC";
+ };
+
+ autoCommands = mkOption {
+ type = types.listOf types.str;
+ default = [];
+ description = "List of commands to execute on connect.";
+ };
+
+ server = {
+ address = mkOption {
+ type = types.str;
+ description = "Address of the chat server.";
+ };
+
+ port = mkOption {
+ type = types.port;
+ default = 6667;
+ description = "Port of the chat server.";
+ };
+
+ ssl = {
+ enable = mkOption {
+ type = types.bool;
+ default = true;
+ description = "Whether SSL should be used.";
+ };
+
+ verify = mkOption {
+ type = types.bool;
+ default = true;
+ description = "Whether the SSL certificate should be verified.";
+ };
+ };
+
+ autoConnect = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Whether Irssi connects to the server on launch.";
+ };
+ };
+
+ channels = mkOption {
+ description = "Channels for the given network.";
+ type = types.attrsOf channelType;
+ default = {};
+ };
+ };
+ });
+
+in
+
+{
+
+ options = {
+ programs.irssi = {
+ enable = mkEnableOption "the Irssi chat client";
+
+ extraConfig = mkOption {
+ default = "";
+ description = "These lines are appended to the Irssi configuration.";
+ type = types.str;
+ };
+
+ aliases = mkOption {
+ default = {};
+ example = { J = "join"; BYE = "quit";};
+ description = "An attribute set that maps aliases to commands.";
+ type = types.attrsOf types.str;
+ };
+
+ networks = mkOption {
+ default = {};
+ example = literalExample ''
+ {
+ freenode = {
+ nick = "hmuser";
+ server = {
+ address = "chat.freenode.net";
+ port = 6697;
+ autoConnect = true;
+ };
+ channels = {
+ nixos.autoJoin = true;
+ };
+ };
+ }
+ '';
+ description = "An attribute set of chat networks.";
+ type = types.attrsOf networkType;
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ pkgs.irssi ];
+
+ home.file.".irssi/config".text = ''
+ settings = {
+ core = {
+ settings_autosave = "no";
+ };
+ };
+
+ aliases = {
+ ${assignFormat cfg.aliases}
+ };
+
+ chatnets = {
+ ${chatnetString}
+ };
+
+ servers = (
+ ${serversString}
+ );
+
+ channels = (
+ ${channelString}
+ );
+
+ ${cfg.extraConfig}
+ '';
+ };
+}
diff --git a/home-manager/modules/programs/jq.nix b/home-manager/modules/programs/jq.nix
new file mode 100644
index 00000000000..6c89df0df93
--- /dev/null
+++ b/home-manager/modules/programs/jq.nix
@@ -0,0 +1,76 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.jq;
+
+ colorType = mkOption {
+ type = types.str;
+ description = "ANSI color definition";
+ example = "1;31";
+ visible = false;
+ };
+
+ colorsType = types.submodule {
+ options = {
+ null = colorType;
+ false = colorType;
+ true = colorType;
+ numbers = colorType;
+ strings = colorType;
+ arrays = colorType;
+ objects = colorType;
+ };
+ };
+
+in {
+ options = {
+ programs.jq = {
+ enable = mkEnableOption "the jq command-line JSON processor";
+
+ colors = mkOption {
+ description = ''
+ The colors used in colored JSON output.</para>
+
+ <para>See <link xlink:href="https://stedolan.github.io/jq/manual/#Colors"/>.
+ '';
+
+ example = literalExample ''
+ {
+ null = "1;30";
+ false = "0;31";
+ true = "0;32";
+ numbers = "0;36";
+ strings = "0;33";
+ arrays = "1;35";
+ objects = "1;37";
+ }
+ '';
+
+ default = {
+ null = "1;30";
+ false = "0;39";
+ true = "0;39";
+ numbers = "0;39";
+ strings = "0;32";
+ arrays = "1;39";
+ objects = "1;39";
+ };
+
+ type = colorsType;
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ pkgs.jq ];
+
+ home.sessionVariables = let c = cfg.colors;
+ in {
+ JQ_COLORS =
+ "${c.null}:${c.false}:${c.true}:${c.numbers}:${c.strings}:${c.arrays}:${c.objects}";
+ };
+ };
+}
diff --git a/home-manager/modules/programs/kakoune.nix b/home-manager/modules/programs/kakoune.nix
new file mode 100644
index 00000000000..faf2542dc70
--- /dev/null
+++ b/home-manager/modules/programs/kakoune.nix
@@ -0,0 +1,615 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.kakoune;
+
+ hook = types.submodule {
+ options = {
+ name = mkOption {
+ type = types.enum [
+ "NormalBegin"
+ "NormalIdle"
+ "NormalEnd"
+ "NormalKey"
+ "InsertBegin"
+ "InsertIdle"
+ "InsertEnd"
+ "InsertKey"
+ "InsertChar"
+ "InsertDelete"
+ "InsertMove"
+ "WinCreate"
+ "WinClose"
+ "WinResize"
+ "WinDisplay"
+ "WinSetOption"
+ "BufSetOption"
+ "BufNewFile"
+ "BufOpenFile"
+ "BufCreate"
+ "BufWritePre"
+ "BufWritePost"
+ "BufReload"
+ "BufClose"
+ "BufOpenFifo"
+ "BufReadFifo"
+ "BufCloseFifo"
+ "RuntimeError"
+ "ModeChange"
+ "PromptIdle"
+ "GlobalSetOption"
+ "KakBegin"
+ "KakEnd"
+ "FocusIn"
+ "FocusOut"
+ "RawKey"
+ "InsertCompletionShow"
+ "InsertCompletionHide"
+ "InsertCompletionSelect"
+ ];
+ example = "SetOption";
+ description = ''
+ The name of the hook. For a description, see
+ <link xlink:href="https://github.com/mawww/kakoune/blob/master/doc/pages/hooks.asciidoc#default-hooks"/>.
+ '';
+ };
+
+ once = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Remove the hook after running it once.
+ '';
+ };
+
+ group = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = ''
+ Add the hook to the named group.
+ '';
+ };
+
+ option = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ example = "filetype=latex";
+ description = ''
+ Additional option to pass to the hook.
+ '';
+ };
+
+ commands = mkOption {
+ type = types.lines;
+ default = "";
+ example = "set-option window indentwidth 2";
+ description = ''
+ Commands to run when the hook is activated.
+ '';
+ };
+ };
+ };
+
+ keyMapping = types.submodule {
+ options = {
+ mode = mkOption {
+ type = types.enum [
+ "insert"
+ "normal"
+ "prompt"
+ "menu"
+ "user"
+ "goto"
+ "view"
+ "object"
+ ];
+ example = "user";
+ description = ''
+ The mode in which the mapping takes effect.
+ '';
+ };
+
+ docstring = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = ''
+ Optional documentation text to display in info boxes.
+ '';
+ };
+
+ key = mkOption {
+ type = types.str;
+ example = "<a-x>";
+ description = ''
+ The key to be mapped. See
+ <link xlink:href="https://github.com/mawww/kakoune/blob/master/doc/pages/mapping.asciidoc#mappable-keys"/>
+ for possible values.
+ '';
+ };
+
+ effect = mkOption {
+ type = types.str;
+ example = ":wq<ret>";
+ description = ''
+ The sequence of keys to be mapped.
+ '';
+ };
+ };
+ };
+
+ configModule = types.submodule {
+ options = {
+ colorScheme = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = ''
+ Set the color scheme. To see available schemes, enter
+ <command>colorscheme</command> at the kakoune prompt.
+ '';
+ };
+
+ tabStop = mkOption {
+ type = types.nullOr types.ints.unsigned;
+ default = null;
+ description = ''
+ The width of a tab in spaces. The kakoune default is
+ <literal>6</literal>.
+ '';
+ };
+
+ indentWidth = mkOption {
+ type = types.nullOr types.ints.unsigned;
+ default = null;
+ description = ''
+ The width of an indentation in spaces.
+ The kakoune default is <literal>4</literal>.
+ If <literal>0</literal>, a tab will be used instead.
+ '';
+ };
+
+ incrementalSearch = mkOption {
+ type = types.bool;
+ default = true;
+ description = ''
+ Execute a search as it is being typed.
+ '';
+ };
+
+ alignWithTabs = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Use tabs for the align command.
+ '';
+ };
+
+ autoInfo = mkOption {
+ type = types.nullOr
+ (types.listOf (types.enum [ "command" "onkey" "normal" ]));
+ default = null;
+ example = [ "command" "normal" ];
+ description = ''
+ Contexts in which to display automatic information box.
+ The kakoune default is <literal>[ "command" "onkey" ]</literal>.
+ '';
+ };
+
+ autoComplete = mkOption {
+ type = types.nullOr (types.listOf (types.enum [ "insert" "prompt" ]));
+ default = null;
+ description = ''
+ Modes in which to display possible completions.
+ The kakoune default is <literal>[ "insert" "prompt" ]</literal>.
+ '';
+ };
+
+ autoReload = mkOption {
+ type = types.nullOr (types.enum [ "yes" "no" "ask" ]);
+ default = null;
+ description = ''
+ Reload buffers when an external modification is detected.
+ The kakoune default is <literal>"ask"</literal>.
+ '';
+ };
+
+ scrollOff = mkOption {
+ type = types.nullOr (types.submodule {
+ options = {
+ lines = mkOption {
+ type = types.ints.unsigned;
+ default = 0;
+ description = ''
+ The number of lines to keep visible around the cursor.
+ '';
+ };
+
+ columns = mkOption {
+ type = types.ints.unsigned;
+ default = 0;
+ description = ''
+ The number of columns to keep visible around the cursor.
+ '';
+ };
+ };
+ });
+ default = null;
+ description = ''
+ How many lines and columns to keep visible around the cursor.
+ '';
+ };
+
+ ui = mkOption {
+ type = types.nullOr (types.submodule {
+ options = {
+ setTitle = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Change the title of the terminal emulator.
+ '';
+ };
+
+ statusLine = mkOption {
+ type = types.enum [ "top" "bottom" ];
+ default = "bottom";
+ description = ''
+ Where to display the status line.
+ '';
+ };
+
+ assistant = mkOption {
+ type = types.enum [ "clippy" "cat" "dilbert" "none" ];
+ default = "clippy";
+ description = ''
+ The assistant displayed in info boxes.
+ '';
+ };
+
+ enableMouse = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Whether to enable mouse support.
+ '';
+ };
+
+ changeColors = mkOption {
+ type = types.bool;
+ default = true;
+ description = ''
+ Change color palette.
+ '';
+ };
+
+ wheelDownButton = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = ''
+ Button to send for wheel down events.
+ '';
+ };
+
+ wheelUpButton = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = ''
+ Button to send for wheel up events.
+ '';
+ };
+
+ shiftFunctionKeys = mkOption {
+ type = types.nullOr types.ints.unsigned;
+ default = null;
+ description = ''
+ Amount by which shifted function keys are offset. That
+ is, if the terminal sends F13 for Shift-F1, this
+ should be <literal>12</literal>.
+ '';
+ };
+
+ useBuiltinKeyParser = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Bypass ncurses key parser and use an internal one.
+ '';
+ };
+ };
+ });
+ default = null;
+ description = ''
+ Settings for the ncurses interface.
+ '';
+ };
+
+ showMatching = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Highlight the matching char of the character under the
+ selections' cursor using the <literal>MatchingChar</literal>
+ face.
+ '';
+ };
+
+ wrapLines = mkOption {
+ type = types.nullOr (types.submodule {
+ options = {
+ enable = mkEnableOption "the wrap lines highlighter";
+
+ word = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Wrap at word boundaries instead of codepoint boundaries.
+ '';
+ };
+
+ indent = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Preserve line indentation when wrapping.
+ '';
+ };
+
+ maxWidth = mkOption {
+ type = types.nullOr types.ints.unsigned;
+ default = null;
+ description = ''
+ Wrap text at maxWidth, even if the window is wider.
+ '';
+ };
+
+ marker = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ example = "⏎";
+ description = ''
+ Prefix wrapped lines with marker text.
+ If not <literal>null</literal>,
+ the marker text will be displayed in the indentation if possible.
+ '';
+ };
+ };
+ });
+ default = null;
+ description = ''
+ Settings for the wrap lines highlighter.
+ '';
+ };
+
+ numberLines = mkOption {
+ type = types.nullOr (types.submodule {
+ options = {
+ enable = mkEnableOption "the number lines highlighter";
+
+ relative = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Show line numbers relative to the main cursor line.
+ '';
+ };
+
+ highlightCursor = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Highlight the cursor line with a separate face.
+ '';
+ };
+
+ separator = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = ''
+ String that separates the line number column from the
+ buffer contents. The kakoune default is
+ <literal>"|"</literal>.
+ '';
+ };
+ };
+ });
+ default = null;
+ description = ''
+ Settings for the number lines highlighter.
+ '';
+ };
+
+ showWhitespace = mkOption {
+ type = types.nullOr (types.submodule {
+ options = {
+ enable = mkEnableOption "the show whitespace highlighter";
+
+ lineFeed = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = ''
+ The character to display for line feeds.
+ The kakoune default is <literal>"¬"</literal>.
+ '';
+ };
+
+ space = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = ''
+ The character to display for spaces.
+ The kakoune default is <literal>"·"</literal>.
+ '';
+ };
+
+ nonBreakingSpace = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = ''
+ The character to display for non-breaking spaces.
+ The kakoune default is <literal>"⍽"</literal>.
+ '';
+ };
+
+ tab = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = ''
+ The character to display for tabs.
+ The kakoune default is <literal>"→"</literal>.
+ '';
+ };
+
+ tabStop = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = ''
+ The character to append to tabs to reach the width of a tabstop.
+ The kakoune default is <literal>" "</literal>.
+ '';
+ };
+ };
+ });
+ default = null;
+ description = ''
+ Settings for the show whitespaces highlighter.
+ '';
+ };
+
+ keyMappings = mkOption {
+ type = types.listOf keyMapping;
+ default = [ ];
+ description = ''
+ User-defined key mappings. For documentation, see
+ <link xlink:href="https://github.com/mawww/kakoune/blob/master/doc/pages/mapping.asciidoc"/>.
+ '';
+ };
+
+ hooks = mkOption {
+ type = types.listOf hook;
+ default = [ ];
+ description = ''
+ Global hooks. For documentation, see
+ <link xlink:href="https://github.com/mawww/kakoune/blob/master/doc/pages/hooks.asciidoc"/>.
+ '';
+ };
+ };
+ };
+
+ configFile = let
+ wrapOptions = with cfg.config.wrapLines;
+ concatStrings [
+ "${optionalString word " -word"}"
+ "${optionalString indent " -indent"}"
+ "${optionalString (marker != null) " -marker ${marker}"}"
+ "${optionalString (maxWidth != null) " -width ${toString maxWidth}"}"
+ ];
+
+ numberLinesOptions = with cfg.config.numberLines;
+ concatStrings [
+ "${optionalString relative " -relative "}"
+ "${optionalString highlightCursor " -hlcursor"}"
+ "${optionalString (separator != null) " -separator ${separator}"}"
+ ];
+
+ uiOptions = with cfg.config.ui;
+ concatStringsSep " " [
+ "ncurses_set_title=${if setTitle then "true" else "false"}"
+ "ncurses_status_on_top=${
+ if (statusLine == "top") then "true" else "false"
+ }"
+ "ncurses_assistant=${assistant}"
+ "ncurses_enable_mouse=${if enableMouse then "true" else "false"}"
+ "ncurses_change_colors=${if changeColors then "true" else "false"}"
+ "${optionalString (wheelDownButton != null)
+ "ncurses_wheel_down_button=${wheelDownButton}"}"
+ "${optionalString (wheelUpButton != null)
+ "ncurses_wheel_up_button=${wheelUpButton}"}"
+ "${optionalString (shiftFunctionKeys != null)
+ "ncurses_shift_function_key=${toString shiftFunctionKeys}"}"
+ "ncurses_builtin_key_parser=${
+ if useBuiltinKeyParser then "true" else "false"
+ }"
+ ];
+
+ keyMappingString = km:
+ concatStringsSep " " [
+ "map global"
+ "${km.mode} ${km.key} '${km.effect}'"
+ "${optionalString (km.docstring != null)
+ "-docstring '${km.docstring}'"}"
+ ];
+
+ hookString = h:
+ concatStringsSep " " [
+ "hook"
+ "${optionalString (h.group != null) "-group ${group}"}"
+ "${optionalString (h.once) "-once"}"
+ "global"
+ "${h.name}"
+ "${optionalString (h.option != null) h.option}"
+ "%{ ${h.commands} }"
+ ];
+
+ cfgStr = with cfg.config;
+ concatStringsSep "\n" ([ "# Generated by home-manager" ]
+ ++ optional (colorScheme != null) "colorscheme ${colorScheme}"
+ ++ optional (tabStop != null)
+ "set-option global tabstop ${toString tabStop}"
+ ++ optional (indentWidth != null)
+ "set-option global indentwidth ${toString indentWidth}"
+ ++ optional (!incrementalSearch) "set-option global incsearch false"
+ ++ optional (alignWithTabs) "set-option global aligntab true"
+ ++ optional (autoInfo != null)
+ "set-option global autoinfo ${concatStringsSep "|" autoInfo}"
+ ++ optional (autoComplete != null)
+ "set-option global autocomplete ${concatStringsSep "|" autoComplete}"
+ ++ optional (autoReload != null)
+ "set-option global/ autoreload ${autoReload}"
+ ++ optional (wrapLines != null && wrapLines.enable)
+ "add-highlighter global/ wrap${wrapOptions}"
+ ++ optional (numberLines != null && numberLines.enable)
+ "add-highlighter global/ number-lines${numberLinesOptions}"
+ ++ optional showMatching "add-highlighter global/ show-matching"
+ ++ optional (scrollOff != null)
+ "set-option global scrolloff ${toString scrollOff.lines},${
+ toString scrollOff.columns
+ }"
+
+ ++ [ "# UI options" ]
+ ++ optional (ui != null) "set-option global ui_options ${uiOptions}"
+
+ ++ [ "# Key mappings" ] ++ map keyMappingString keyMappings
+
+ ++ [ "# Hooks" ] ++ map hookString hooks);
+ in pkgs.writeText "kakrc"
+ (optionalString (cfg.config != null) cfgStr + "\n" + cfg.extraConfig);
+
+in {
+ options = {
+ programs.kakoune = {
+ enable = mkEnableOption "the kakoune text editor";
+
+ config = mkOption {
+ type = types.nullOr configModule;
+ default = { };
+ description = "kakoune configuration options.";
+ };
+
+ extraConfig = mkOption {
+ type = types.lines;
+ default = "";
+ description = ''
+ Extra configuration lines to add to
+ <filename>~/.config/kak/kakrc</filename>.
+ '';
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ pkgs.kakoune ];
+ xdg.configFile."kak/kakrc".source = configFile;
+ };
+}
diff --git a/home-manager/modules/programs/keychain.nix b/home-manager/modules/programs/keychain.nix
new file mode 100644
index 00000000000..6e26bd232ce
--- /dev/null
+++ b/home-manager/modules/programs/keychain.nix
@@ -0,0 +1,115 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.keychain;
+
+ flags = cfg.extraFlags ++ optional (cfg.agents != [ ])
+ "--agents ${concatStringsSep "," cfg.agents}"
+ ++ optional (cfg.inheritType != null) "--inherit ${cfg.inheritType}";
+
+ shellCommand =
+ "${cfg.package}/bin/keychain --eval ${concatStringsSep " " flags} ${
+ concatStringsSep " " cfg.keys
+ }";
+
+in {
+ meta.maintainers = [ maintainers.marsam ];
+
+ options.programs.keychain = {
+ enable = mkEnableOption "keychain";
+
+ package = mkOption {
+ type = types.package;
+ default = pkgs.keychain;
+ defaultText = literalExample "pkgs.keychain";
+ description = ''
+ Keychain package to install.
+ '';
+ };
+
+ keys = mkOption {
+ type = types.listOf types.str;
+ default = [ "id_rsa" ];
+ description = ''
+ Keys to add to keychain.
+ '';
+ };
+
+ agents = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ description = ''
+ Agents to add.
+ '';
+ };
+
+ inheritType = mkOption {
+ type =
+ types.nullOr (types.enum [ "local" "any" "local-once" "any-once" ]);
+ default = null;
+ description = ''
+ Inherit type to attempt from agent variables from the environment.
+ '';
+ };
+
+ extraFlags = mkOption {
+ type = types.listOf types.str;
+ default = [ "--quiet" ];
+ description = ''
+ Extra flags to pass to keychain.
+ '';
+ };
+
+ enableBashIntegration = mkOption {
+ default = true;
+ type = types.bool;
+ description = ''
+ Whether to enable Bash integration.
+ '';
+ };
+
+ enableFishIntegration = mkOption {
+ default = true;
+ type = types.bool;
+ description = ''
+ Whether to enable Fish integration.
+ '';
+ };
+
+ enableZshIntegration = mkOption {
+ default = true;
+ type = types.bool;
+ description = ''
+ Whether to enable Zsh integration.
+ '';
+ };
+
+ enableXsessionIntegration = mkOption {
+ default = true;
+ type = types.bool;
+ visible = pkgs.stdenv.hostPlatform.isLinux;
+ description = ''
+ Whether to run keychain from your <filename>~/.xsession</filename>.
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ cfg.package ];
+ programs.bash.initExtra = mkIf cfg.enableBashIntegration ''
+ eval "$(${shellCommand})"
+ '';
+ programs.fish.interactiveShellInit = mkIf cfg.enableFishIntegration ''
+ eval (${shellCommand})
+ '';
+ programs.zsh.initExtra = mkIf cfg.enableZshIntegration ''
+ eval "$(${shellCommand})"
+ '';
+ xsession.initExtra = mkIf cfg.enableXsessionIntegration ''
+ eval "$(${shellCommand})"
+ '';
+ };
+}
diff --git a/home-manager/modules/programs/lesspipe.nix b/home-manager/modules/programs/lesspipe.nix
new file mode 100644
index 00000000000..a7a51ffe2a2
--- /dev/null
+++ b/home-manager/modules/programs/lesspipe.nix
@@ -0,0 +1,19 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ meta.maintainers = [ maintainers.rycee ];
+
+ options = {
+ programs.lesspipe = {
+ enable = mkEnableOption "lesspipe preprocessor for less";
+ };
+ };
+
+ config = mkIf config.programs.lesspipe.enable {
+ home.sessionVariables = {
+ LESSOPEN = "|${pkgs.lesspipe}/bin/lesspipe.sh %s";
+ };
+ };
+}
diff --git a/home-manager/modules/programs/lsd.nix b/home-manager/modules/programs/lsd.nix
new file mode 100644
index 00000000000..ab1880ff828
--- /dev/null
+++ b/home-manager/modules/programs/lsd.nix
@@ -0,0 +1,41 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.lsd;
+
+ aliases = {
+ ls = "${pkgs.lsd}/bin/lsd";
+ ll = "ls -l";
+ la = "ls -a";
+ lt = "ls --tree";
+ lla = "ls -la";
+ };
+
+in {
+ meta.maintainers = [ maintainers.marsam ];
+
+ options.programs.lsd = {
+ enable = mkEnableOption "lsd";
+
+ enableAliases = mkOption {
+ default = false;
+ type = types.bool;
+ description = ''
+ Whether to enable recommended lsd aliases.
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ pkgs.lsd ];
+
+ programs.bash.shellAliases = mkIf cfg.enableAliases aliases;
+
+ programs.zsh.shellAliases = mkIf cfg.enableAliases aliases;
+
+ programs.fish.shellAliases = mkIf cfg.enableAliases aliases;
+ };
+}
diff --git a/home-manager/modules/programs/man.nix b/home-manager/modules/programs/man.nix
new file mode 100644
index 00000000000..0ed376780d4
--- /dev/null
+++ b/home-manager/modules/programs/man.nix
@@ -0,0 +1,22 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ options = {
+ programs.man.enable = mkOption {
+ type = types.bool;
+ default = true;
+ description = ''
+ Whether to enable manual pages and the <command>man</command>
+ command. This also includes "man" outputs of all
+ <literal>home.packages</literal>.
+ '';
+ };
+ };
+
+ config = mkIf config.programs.man.enable {
+ home.packages = [ pkgs.man ];
+ home.extraOutputsToInstall = [ "man" ];
+ };
+}
diff --git a/home-manager/modules/programs/matplotlib.nix b/home-manager/modules/programs/matplotlib.nix
new file mode 100644
index 00000000000..da80c116770
--- /dev/null
+++ b/home-manager/modules/programs/matplotlib.nix
@@ -0,0 +1,59 @@
+{ config, lib, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.matplotlib;
+
+ formatLine = o: n: v:
+ let
+ formatValue = v:
+ if isBool v then (if v then "True" else "False") else toString v;
+ in if isAttrs v then
+ concatStringsSep "\n" (mapAttrsToList (formatLine "${o}${n}.") v)
+ else
+ (if v == "" then "" else "${o}${n}: ${formatValue v}");
+
+in {
+ meta.maintainers = [ maintainers.rprospero ];
+
+ options.programs.matplotlib = {
+ enable = mkEnableOption "matplotlib, a plotting library for python";
+
+ config = mkOption {
+ default = { };
+ type = types.attrs;
+ description = ''
+ Add terms to the <filename>matplotlibrc</filename> file to
+ control the default matplotlib behavior.
+ '';
+ example = literalExample ''
+ {
+ backend = "Qt5Agg";
+ axes = {
+ grid = true;
+ facecolor = "black";
+ edgecolor = "FF9900";
+ };
+ grid.color = "FF9900";
+ }
+ '';
+ };
+
+ extraConfig = mkOption {
+ type = types.lines;
+ default = "";
+ description = ''
+ Additional commands for matplotlib that will be added to the
+ <filename>matplotlibrc</filename> file.
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+ xdg.configFile."matplotlib/matplotlibrc".text = concatStringsSep "\n" ([ ]
+ ++ mapAttrsToList (formatLine "") cfg.config
+ ++ optional (cfg.extraConfig != "") cfg.extraConfig) + "\n";
+ };
+}
diff --git a/home-manager/modules/programs/mbsync-accounts.nix b/home-manager/modules/programs/mbsync-accounts.nix
new file mode 100644
index 00000000000..4de1965fe3f
--- /dev/null
+++ b/home-manager/modules/programs/mbsync-accounts.nix
@@ -0,0 +1,105 @@
+{ lib, ... }:
+
+with lib;
+
+let
+
+ extraConfigType = with lib.types; attrsOf (either (either str int) bool);
+
+in {
+ options.mbsync = {
+ enable = mkEnableOption "synchronization using mbsync";
+
+ flatten = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ example = ".";
+ description = ''
+ If set, flattens the hierarchy within the maildir by
+ substituting the canonical hierarchy delimiter
+ <literal>/</literal> with this value.
+ '';
+ };
+
+ create = mkOption {
+ type = types.enum [ "none" "maildir" "imap" "both" ];
+ default = "none";
+ example = "maildir";
+ description = ''
+ Automatically create missing mailboxes within the
+ given mail store.
+ '';
+ };
+
+ remove = mkOption {
+ type = types.enum [ "none" "maildir" "imap" "both" ];
+ default = "none";
+ example = "imap";
+ description = ''
+ Propagate mailbox deletions to the given mail store.
+ '';
+ };
+
+ expunge = mkOption {
+ type = types.enum [ "none" "maildir" "imap" "both" ];
+ default = "none";
+ example = "both";
+ description = ''
+ Permanently remove messages marked for deletion from
+ the given mail store.
+ '';
+ };
+
+ patterns = mkOption {
+ type = types.listOf types.str;
+ default = [ "*" ];
+ description = ''
+ Pattern of mailboxes to synchronize.
+ '';
+ };
+
+ extraConfig.channel = mkOption {
+ type = extraConfigType;
+ default = { };
+ example = literalExample ''
+ {
+ MaxMessages = 10000;
+ MaxSize = "1m";
+ };
+ '';
+ description = ''
+ Per channel extra configuration.
+ '';
+ };
+
+ extraConfig.local = mkOption {
+ type = extraConfigType;
+ default = { };
+ description = ''
+ Local store extra configuration.
+ '';
+ };
+
+ extraConfig.remote = mkOption {
+ type = extraConfigType;
+ default = { };
+ description = ''
+ Remote store extra configuration.
+ '';
+ };
+
+ extraConfig.account = mkOption {
+ type = extraConfigType;
+ default = { };
+ example = literalExample ''
+ {
+ PipelineDepth = 10;
+ Timeout = 60;
+ };
+ '';
+ description = ''
+ Account section extra configuration.
+ '';
+ };
+ };
+}
diff --git a/home-manager/modules/programs/mbsync.nix b/home-manager/modules/programs/mbsync.nix
new file mode 100644
index 00000000000..6ade10986fe
--- /dev/null
+++ b/home-manager/modules/programs/mbsync.nix
@@ -0,0 +1,164 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.mbsync;
+
+ # Accounts for which mbsync is enabled.
+ mbsyncAccounts =
+ filter (a: a.mbsync.enable) (attrValues config.accounts.email.accounts);
+
+ genTlsConfig = tls:
+ {
+ SSLType = if !tls.enable then
+ "None"
+ else if tls.useStartTls then
+ "STARTTLS"
+ else
+ "IMAPS";
+ } // optionalAttrs (tls.enable && tls.certificatesFile != null) {
+ CertificateFile = toString tls.certificatesFile;
+ };
+
+ masterSlaveMapping = {
+ none = "None";
+ imap = "Master";
+ maildir = "Slave";
+ both = "Both";
+ };
+
+ genSection = header: entries:
+ let
+ escapeValue = escape [ ''"'' ];
+ hasSpace = v: builtins.match ".* .*" v != null;
+ genValue = n: v:
+ if isList v then
+ concatMapStringsSep " " (genValue n) v
+ else if isBool v then
+ (if v then "yes" else "no")
+ else if isInt v then
+ toString v
+ else if isString v && hasSpace v then
+ ''"${escapeValue v}"''
+ else if isString v then
+ v
+ else
+ let prettyV = lib.generators.toPretty { } v;
+ in throw "mbsync: unexpected value for option ${n}: '${prettyV}'";
+ in ''
+ ${header}
+ ${concatStringsSep "\n"
+ (mapAttrsToList (n: v: "${n} ${genValue n v}") entries)}
+ '';
+
+ genAccountConfig = account:
+ with account;
+ genSection "IMAPAccount ${name}" ({
+ Host = imap.host;
+ User = userName;
+ PassCmd = toString passwordCommand;
+ } // genTlsConfig imap.tls
+ // optionalAttrs (imap.port != null) { Port = toString imap.port; }
+ // mbsync.extraConfig.account) + "\n"
+ + genSection "IMAPStore ${name}-remote"
+ ({ Account = name; } // mbsync.extraConfig.remote) + "\n"
+ + genSection "MaildirStore ${name}-local" ({
+ Path = "${maildir.absPath}/";
+ Inbox = "${maildir.absPath}/${folders.inbox}";
+ SubFolders = "Verbatim";
+ } // optionalAttrs (mbsync.flatten != null) { Flatten = mbsync.flatten; }
+ // mbsync.extraConfig.local) + "\n" + genSection "Channel ${name}" ({
+ Master = ":${name}-remote:";
+ Slave = ":${name}-local:";
+ Patterns = mbsync.patterns;
+ Create = masterSlaveMapping.${mbsync.create};
+ Remove = masterSlaveMapping.${mbsync.remove};
+ Expunge = masterSlaveMapping.${mbsync.expunge};
+ SyncState = "*";
+ } // mbsync.extraConfig.channel) + "\n";
+
+ genGroupConfig = name: channels:
+ let
+ genGroupChannel = n: boxes: "Channel ${n}:${concatStringsSep "," boxes}";
+ in concatStringsSep "\n"
+ ([ "Group ${name}" ] ++ mapAttrsToList genGroupChannel channels);
+
+in {
+ options = {
+ programs.mbsync = {
+ enable = mkEnableOption "mbsync IMAP4 and Maildir mailbox synchronizer";
+
+ package = mkOption {
+ type = types.package;
+ default = pkgs.isync;
+ defaultText = literalExample "pkgs.isync";
+ example = literalExample "pkgs.isync";
+ description = "The package to use for the mbsync binary.";
+ };
+
+ groups = mkOption {
+ type = types.attrsOf (types.attrsOf (types.listOf types.str));
+ default = { };
+ example = literalExample ''
+ {
+ inboxes = {
+ account1 = [ "Inbox" ];
+ account2 = [ "Inbox" ];
+ };
+ }
+ '';
+ description = ''
+ Definition of groups.
+ '';
+ };
+
+ extraConfig = mkOption {
+ type = types.lines;
+ default = "";
+ description = ''
+ Extra configuration lines to add to the mbsync configuration.
+ '';
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ assertions = let
+ checkAccounts = pred: msg:
+ let badAccounts = filter pred mbsyncAccounts;
+ in {
+ assertion = badAccounts == [ ];
+ message = "mbsync: ${msg} for accounts: "
+ + concatMapStringsSep ", " (a: a.name) badAccounts;
+ };
+ in [
+ (checkAccounts (a: a.maildir == null) "Missing maildir configuration")
+ (checkAccounts (a: a.imap == null) "Missing IMAP configuration")
+ (checkAccounts (a: a.passwordCommand == null) "Missing passwordCommand")
+ (checkAccounts (a: a.userName == null) "Missing username")
+ ];
+
+ home.packages = [ cfg.package ];
+
+ programs.notmuch.new.ignore = [ ".uidvalidity" ".mbsyncstate" ];
+
+ home.file.".mbsyncrc".text = let
+ accountsConfig = map genAccountConfig mbsyncAccounts;
+ groupsConfig = mapAttrsToList genGroupConfig cfg.groups;
+ in concatStringsSep "\n" ([''
+ # Generated by Home Manager.
+ ''] ++ optional (cfg.extraConfig != "") cfg.extraConfig ++ accountsConfig
+ ++ groupsConfig) + "\n";
+
+ home.activation = mkIf (mbsyncAccounts != [ ]) {
+ createMaildir =
+ hm.dag.entryBetween [ "linkGeneration" ] [ "writeBoundary" ] ''
+ $DRY_RUN_CMD mkdir -m700 -p $VERBOSE_ARG ${
+ concatMapStringsSep " " (a: a.maildir.absPath) mbsyncAccounts
+ }
+ '';
+ };
+ };
+}
diff --git a/home-manager/modules/programs/mercurial.nix b/home-manager/modules/programs/mercurial.nix
new file mode 100644
index 00000000000..8e9a3befbaf
--- /dev/null
+++ b/home-manager/modules/programs/mercurial.nix
@@ -0,0 +1,100 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.mercurial;
+
+in {
+
+ options = {
+ programs.mercurial = {
+ enable = mkEnableOption "Mercurial";
+
+ package = mkOption {
+ type = types.package;
+ default = pkgs.mercurial;
+ defaultText = literalExample "pkgs.mercurial";
+ description = "Mercurial package to install.";
+ };
+
+ userName = mkOption {
+ type = types.str;
+ description = "Default user name to use.";
+ };
+
+ userEmail = mkOption {
+ type = types.str;
+ description = "Default user email to use.";
+ };
+
+ aliases = mkOption {
+ type = types.attrs;
+ default = { };
+ description = "Mercurial aliases to define.";
+ };
+
+ extraConfig = mkOption {
+ type = types.either types.attrs types.lines;
+ default = { };
+ description = "Additional configuration to add.";
+ };
+
+ iniContent = mkOption {
+ type = types.attrsOf types.attrs;
+ internal = true;
+ };
+
+ ignores = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ example = [ "*~" "*.swp" ];
+ description = "List of globs for files to be globally ignored.";
+ };
+
+ ignoresRegexp = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ example = [ "^.*~$" "^.*\\.swp$" ];
+ description =
+ "List of regular expressions for files to be globally ignored.";
+ };
+ };
+ };
+
+ config = mkIf cfg.enable (mkMerge [
+ {
+ home.packages = [ cfg.package ];
+
+ programs.mercurial.iniContent.ui = {
+ username = cfg.userName + " <" + cfg.userEmail + ">";
+ };
+
+ xdg.configFile."hg/hgrc".text = generators.toINI { } cfg.iniContent;
+ }
+
+ (mkIf (cfg.ignores != [ ] || cfg.ignoresRegexp != [ ]) {
+ programs.mercurial.iniContent.ui.ignore =
+ "${config.xdg.configHome}/hg/hgignore_global";
+
+ xdg.configFile."hg/hgignore_global".text = ''
+ syntax: glob
+ '' + concatStringsSep "\n" cfg.ignores + "\n" + ''
+ syntax: regexp
+ '' + concatStringsSep "\n" cfg.ignoresRegexp + "\n";
+ })
+
+ (mkIf (cfg.aliases != { }) {
+ programs.mercurial.iniContent.alias = cfg.aliases;
+ })
+
+ (mkIf (lib.isAttrs cfg.extraConfig) {
+ programs.mercurial.iniContent = cfg.extraConfig;
+ })
+
+ (mkIf (lib.isString cfg.extraConfig) {
+ xdg.configFile."hg/hgrc".text = cfg.extraConfig;
+ })
+ ]);
+}
diff --git a/home-manager/modules/programs/mpv.nix b/home-manager/modules/programs/mpv.nix
new file mode 100644
index 00000000000..3a4e5092f9a
--- /dev/null
+++ b/home-manager/modules/programs/mpv.nix
@@ -0,0 +1,143 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+ inherit (builtins) typeOf stringLength;
+
+ cfg = config.programs.mpv;
+
+ mpvOption = with types; either str (either int (either bool float));
+ mpvOptions = with types; attrsOf mpvOption;
+ mpvProfiles = with types; attrsOf mpvOptions;
+ mpvBindings = with types; attrsOf str;
+
+ renderOption = option:
+ rec {
+ int = toString option;
+ float = int;
+
+ bool = if option then "yes" else "no";
+
+ string = option;
+ }.${typeOf option};
+
+ renderOptions = options:
+ concatStringsSep "\n" (mapAttrsToList (name: value:
+ let
+ rendered = renderOption value;
+ length = toString (stringLength rendered);
+ in "${name}=%${length}%${rendered}") options);
+
+ renderProfiles = profiles:
+ concatStringsSep "\n" (mapAttrsToList (name: value: ''
+ [${name}]
+ ${renderOptions value}
+ '') profiles);
+
+ renderBindings = bindings:
+ concatStringsSep "\n"
+ (mapAttrsToList (name: value: "${name} ${value}") bindings);
+
+in {
+ options = {
+ programs.mpv = {
+ enable = mkEnableOption "mpv";
+
+ scripts = mkOption {
+ type = with types; listOf (either package str);
+ default = [ ];
+ example = literalExample "[ pkgs.mpvScripts.mpris ]";
+ description = ''
+ List of scripts to use with mpv.
+ '';
+ };
+
+ config = mkOption {
+ description = ''
+ Configuration written to
+ <filename>~/.config/mpv/mpv.conf</filename>. See
+ <citerefentry>
+ <refentrytitle>mpv</refentrytitle>
+ <manvolnum>1</manvolnum>
+ </citerefentry>
+ for the full list of options.
+ '';
+ type = mpvOptions;
+ default = { };
+ example = literalExample ''
+ {
+ profile = "gpu-hq";
+ force-window = "yes";
+ ytdl-format = "bestvideo+bestaudio";
+ cache-default = 4000000;
+ }
+ '';
+ };
+
+ profiles = mkOption {
+ description = ''
+ Sub-configuration options for specific profiles written to
+ <filename>~/.config/mpv/mpv.conf</filename>. See
+ <option>programs.mpv.config</option> for more information.
+ '';
+ type = mpvProfiles;
+ default = { };
+ example = literalExample ''
+ {
+ fast = {
+ vo = "vdpau";
+ };
+ "protocol.dvd" = {
+ profile-desc = "profile for dvd:// streams";
+ alang = "en";
+ };
+ }
+ '';
+ };
+
+ bindings = mkOption {
+ description = ''
+ Input configuration written to
+ <filename>~/.config/mpv/input.conf</filename>. See
+ <citerefentry>
+ <refentrytitle>mpv</refentrytitle>
+ <manvolnum>1</manvolnum>
+ </citerefentry>
+ for the full list of options.
+ '';
+ type = mpvBindings;
+ default = { };
+ example = literalExample ''
+ {
+ WHEEL_UP = "seek 10";
+ WHEEL_DOWN = "seek -10";
+ "Alt+0" = "set window-scale 0.5";
+ }
+ '';
+ };
+ };
+ };
+
+ config = mkIf cfg.enable (mkMerge [
+ {
+ home.packages = [
+ (if cfg.scripts == [ ] then
+ pkgs.mpv
+ else
+ pkgs.mpv-with-scripts.override { scripts = cfg.scripts; })
+ ];
+ }
+ (mkIf (cfg.config != { } || cfg.profiles != { }) {
+ xdg.configFile."mpv/mpv.conf".text = ''
+ ${optionalString (cfg.config != { }) (renderOptions cfg.config)}
+ ${optionalString (cfg.profiles != { }) (renderProfiles cfg.profiles)}
+ '';
+ })
+ (mkIf (cfg.bindings != { }) {
+ xdg.configFile."mpv/input.conf".text = renderBindings cfg.bindings;
+ })
+ ]);
+
+ meta.maintainers = with maintainers; [ tadeokondrak ];
+}
diff --git a/home-manager/modules/programs/msmtp-accounts.nix b/home-manager/modules/programs/msmtp-accounts.nix
new file mode 100644
index 00000000000..894cef51742
--- /dev/null
+++ b/home-manager/modules/programs/msmtp-accounts.nix
@@ -0,0 +1,48 @@
+{ config, lib, ... }:
+
+with lib;
+
+{
+ options.msmtp = {
+ enable = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Whether to enable msmtp.
+ </para><para>
+ If enabled then it is possible to use the
+ <parameter class="command">--account</parameter> command line
+ option to send a message for a given account using the
+ <command>msmtp</command> or <command>msmtpq</command> tool.
+ For example, <command>msmtp --account=private</command> would
+ send using the account defined in
+ <option>accounts.email.accounts.private</option>. If the
+ <parameter class="command">--account</parameter> option is not
+ given then the primary account will be used.
+ '';
+ };
+
+ tls.fingerprint = mkOption {
+ type =
+ types.nullOr (types.strMatching "([[:alnum:]]{2}:)+[[:alnum:]]{2}");
+ default = null;
+ example = "my:SH:a2:56:ha:sh";
+ description = ''
+ Fingerprint of a trusted TLS certificate.
+ The fingerprint can be obtained by executing
+ <command>msmtp --serverinfo --tls --tls-certcheck=off</command>.
+ '';
+ };
+
+ extraConfig = mkOption {
+ type = types.attrsOf types.str;
+ default = { };
+ example = { auth = "login"; };
+ description = ''
+ Extra configuration options to add to <filename>~/.msmtprc</filename>.
+ See <link xlink:href="https://marlam.de/msmtp/msmtprc.txt"/> for
+ examples.
+ '';
+ };
+ };
+}
diff --git a/home-manager/modules/programs/msmtp.nix b/home-manager/modules/programs/msmtp.nix
new file mode 100644
index 00000000000..f34fd72f8b1
--- /dev/null
+++ b/home-manager/modules/programs/msmtp.nix
@@ -0,0 +1,71 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.msmtp;
+
+ msmtpAccounts =
+ filter (a: a.msmtp.enable) (attrValues config.accounts.email.accounts);
+
+ onOff = p: if p then "on" else "off";
+
+ accountStr = account:
+ with account;
+ concatStringsSep "\n" ([ "account ${name}" ]
+ ++ mapAttrsToList (n: v: n + " " + v) ({
+ host = smtp.host;
+ from = address;
+ auth = "on";
+ user = userName;
+ tls = onOff smtp.tls.enable;
+ tls_starttls = onOff smtp.tls.useStartTls;
+ tls_trust_file = smtp.tls.certificatesFile;
+ } // optionalAttrs (msmtp.tls.fingerprint != null) {
+ tls_fingerprint = msmtp.tls.fingerprint;
+ } // optionalAttrs (smtp.port != null) { port = toString smtp.port; }
+ // optionalAttrs (passwordCommand != null) {
+ # msmtp requires the password to finish with a newline.
+ passwordeval =
+ ''${pkgs.bash}/bin/bash -c "${toString passwordCommand}; echo"'';
+ } // msmtp.extraConfig) ++ optional primary ''
+
+ account default : ${name}'');
+
+ configFile = mailAccounts: ''
+ # Generated by Home Manager.
+
+ ${cfg.extraConfig}
+
+ ${concatStringsSep "\n\n" (map accountStr mailAccounts)}
+ '';
+
+in {
+
+ options = {
+ programs.msmtp = {
+ enable = mkEnableOption "msmtp";
+
+ extraConfig = mkOption {
+ type = types.lines;
+ default = "";
+ description = ''
+ Extra configuration lines to add to <filename>~/.msmtprc</filename>.
+ See <link xlink:href="https://marlam.de/msmtp/msmtprc.txt"/> for examples.
+ '';
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ pkgs.msmtp ];
+
+ xdg.configFile."msmtp/config".text = configFile msmtpAccounts;
+
+ home.sessionVariables = {
+ MSMTP_QUEUE = "${config.xdg.dataHome}/msmtp/queue";
+ MSMTP_LOG = "${config.xdg.dataHome}/msmtp/queue.log";
+ };
+ };
+}
diff --git a/home-manager/modules/programs/neomutt-accounts.nix b/home-manager/modules/programs/neomutt-accounts.nix
new file mode 100644
index 00000000000..033db38eb0a
--- /dev/null
+++ b/home-manager/modules/programs/neomutt-accounts.nix
@@ -0,0 +1,34 @@
+{ config, lib, ... }:
+
+with lib;
+
+{
+ options.neomutt = {
+ enable = mkEnableOption "NeoMutt";
+
+ sendMailCommand = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ example = "msmtpq --read-envelope-from --read-recipients";
+ description = ''
+ Command to send a mail. If not set, neomutt will be in charge of sending mails.
+ '';
+ };
+
+ extraConfig = mkOption {
+ type = types.lines;
+ default = "";
+ example = "color status cyan default";
+ description = ''
+ Extra lines to add to the folder hook for this account.
+ '';
+ };
+ };
+
+ config = mkIf config.neomutt.enable {
+ neomutt.sendMailCommand = mkOptionDefault (if config.msmtp.enable then
+ "msmtpq --read-envelope-from --read-recipients"
+ else
+ null);
+ };
+}
diff --git a/home-manager/modules/programs/neomutt.nix b/home-manager/modules/programs/neomutt.nix
new file mode 100644
index 00000000000..85af0353b6c
--- /dev/null
+++ b/home-manager/modules/programs/neomutt.nix
@@ -0,0 +1,302 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.neomutt;
+
+ neomuttAccounts =
+ filter (a: a.neomutt.enable) (attrValues config.accounts.email.accounts);
+
+ sidebarModule = types.submodule {
+ options = {
+ enable = mkEnableOption "sidebar support";
+
+ width = mkOption {
+ type = types.int;
+ default = 22;
+ description = "Width of the sidebar";
+ };
+
+ shortPath = mkOption {
+ type = types.bool;
+ default = true;
+ description = ''
+ By default sidebar shows the full path of the mailbox, but
+ with this enabled only the relative name is shown.
+ '';
+ };
+
+ format = mkOption {
+ type = types.str;
+ default = "%B%?F? [%F]?%* %?N?%N/?%S";
+ description = ''
+ Sidebar format. Check neomutt documentation for details.
+ '';
+ };
+ };
+ };
+
+ bindModule = types.submodule {
+ options = {
+ map = mkOption {
+ type = types.enum [
+ "alias"
+ "attach"
+ "browser"
+ "compose"
+ "editor"
+ "generic"
+ "index"
+ "mix"
+ "pager"
+ "pgp"
+ "postpone"
+ "query"
+ "smime"
+ ];
+ default = "index";
+ description = "Select the menu to bind the command to.";
+ };
+
+ key = mkOption {
+ type = types.str;
+ example = "<left>";
+ description = "The key to bind.";
+ };
+
+ action = mkOption {
+ type = types.str;
+ example = "<enter-command>toggle sidebar_visible<enter><refresh>";
+ description = "Specify the action to take.";
+ };
+ };
+ };
+
+ yesno = x: if x then "yes" else "no";
+ setOption = n: v: if v == null then "unset ${n}" else "set ${n}=${v}";
+ escape = replaceStrings [ "%" ] [ "%25" ];
+
+ accountFilename = account: config.xdg.configHome + "/neomutt/" + account.name;
+
+ genCommonFolderHooks = account:
+ with account; {
+ from = "'${address}'";
+ realname = "'${realName}'";
+ spoolfile = "'+${folders.inbox}'";
+ record = if folders.sent == null then null else "'+${folders.sent}'";
+ postponed = "'+${folders.drafts}'";
+ trash = "'+${folders.trash}'";
+ };
+
+ mtaSection = account:
+ with account;
+ let passCmd = concatStringsSep " " passwordCommand;
+ in if neomutt.sendMailCommand != null then {
+ sendmail = "'${neomutt.sendMailCommand}'";
+ } else
+ let
+ smtpProto = if smtp.tls.enable then "smtps" else "smtp";
+ smtpBaseUrl = "${smtpProto}://${escape userName}@${smtp.host}";
+ in {
+ smtp_url = "'${smtpBaseUrl}'";
+ smtp_pass = "'`${passCmd}`'";
+ };
+
+ genMaildirAccountConfig = account:
+ with account;
+ let
+ folderHook = mapAttrsToList setOption (genCommonFolderHooks account // {
+ folder = "'${account.maildir.absPath}'";
+ }) ++ optional (neomutt.extraConfig != "") neomutt.extraConfig;
+ in ''
+ ${concatStringsSep "\n" folderHook}
+ '';
+
+ registerAccount = account:
+ with account; ''
+ # register account ${name}
+ mailboxes "${account.maildir.absPath}/${folders.inbox}"
+ folder-hook ${account.maildir.absPath}/ " \
+ source ${accountFilename account} "
+ '';
+
+ mraSection = account:
+ with account;
+ if account.maildir != null then
+ genMaildirAccountConfig account
+ else
+ throw "Only maildir is supported at the moment";
+
+ optionsStr = attrs: concatStringsSep "\n" (mapAttrsToList setOption attrs);
+
+ sidebarSection = ''
+ # Sidebar
+ set sidebar_visible = yes
+ set sidebar_short_path = ${yesno cfg.sidebar.shortPath}
+ set sidebar_width = ${toString cfg.sidebar.width}
+ set sidebar_format = '${cfg.sidebar.format}'
+ '';
+
+ bindSection = concatMapStringsSep "\n"
+ (bind: ''bind ${bind.map} ${bind.key} "${bind.action}"'') cfg.binds;
+
+ macroSection = concatMapStringsSep "\n"
+ (bind: ''macro ${bind.map} ${bind.key} "${bind.action}"'') cfg.macros;
+
+ mailCheckSection = ''
+ set mail_check_stats
+ set mail_check_stats_interval = ${toString cfg.checkStatsInterval}
+ '';
+
+ notmuchSection = account:
+ with account; ''
+ # notmuch section
+ set nm_default_uri = "notmuch://${config.accounts.email.maildirBasePath}"
+ virtual-mailboxes "My INBOX" "notmuch://?query=tag:inbox"
+ '';
+
+ accountStr = account:
+ with account;
+ ''
+ # Generated by Home Manager.
+ set ssl_force_tls = yes
+ set certificate_file=${config.accounts.email.certificatesFile}
+
+ # GPG section
+ set crypt_use_gpgme = yes
+ set crypt_autosign = ${yesno (gpg.signByDefault or false)}
+ set pgp_use_gpg_agent = yes
+ set mbox_type = ${if maildir != null then "Maildir" else "mbox"}
+ set sort = "${cfg.sort}"
+
+ # MTA section
+ ${optionsStr (mtaSection account)}
+
+ ${optionalString (cfg.checkStatsInterval != null) mailCheckSection}
+
+ ${optionalString cfg.sidebar.enable sidebarSection}
+
+ # MRA section
+ ${mraSection account}
+
+ # Extra configuration
+ ${account.neomutt.extraConfig}
+ '' + optionalString (account.signature.showSignature != "none") ''
+ set signature = ${pkgs.writeText "signature.txt" account.signature.text}
+ '' + optionalString account.notmuch.enable (notmuchSection account);
+
+in {
+ options = {
+ programs.neomutt = {
+ enable = mkEnableOption "the NeoMutt mail client";
+
+ sidebar = mkOption {
+ type = sidebarModule;
+ default = { };
+ description = "Options related to the sidebar.";
+ };
+
+ binds = mkOption {
+ type = types.listOf bindModule;
+ default = [ ];
+ description = "List of keybindings.";
+ };
+
+ macros = mkOption {
+ type = types.listOf bindModule;
+ default = [ ];
+ description = "List of macros.";
+ };
+
+ sort = mkOption {
+ type = types.enum [
+ "date"
+ "date-received"
+ "from"
+ "mailbox-order"
+ "score"
+ "size"
+ "spam"
+ "subject"
+ "threads"
+ "to"
+ ];
+ default = "threads";
+ description = "Sorting method on messages.";
+ };
+
+ vimKeys = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Enable vim-like bindings.";
+ };
+
+ checkStatsInterval = mkOption {
+ type = types.nullOr types.int;
+ default = null;
+ example = 60;
+ description = "Enable and set the interval of automatic mail check.";
+ };
+
+ editor = mkOption {
+ type = types.str;
+ default = "$EDITOR";
+ description = "Select the editor used for writing mail.";
+ };
+
+ settings = mkOption {
+ type = types.attrsOf types.str;
+ default = { };
+ description = "Extra configuration appended to the end.";
+ };
+
+ extraConfig = mkOption {
+ type = types.lines;
+ default = "";
+ description = "Extra configuration appended to the end.";
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ pkgs.neomutt ];
+ home.file = let
+ rcFile = account: {
+ "${accountFilename account}".text = accountStr account;
+ };
+ in foldl' (a: b: a // b) { } (map rcFile neomuttAccounts);
+
+ xdg.configFile."neomutt/neomuttrc" = mkIf (neomuttAccounts != [ ]) {
+ text = let primary = filter (a: a.primary) neomuttAccounts;
+ in ''
+ # Generated by Home Manager.
+ set header_cache = "${config.xdg.cacheHome}/neomutt/headers/"
+ set message_cachedir = "${config.xdg.cacheHome}/neomutt/messages/"
+ set editor = "${cfg.editor}"
+ set implicit_autoview = yes
+
+ alternative_order text/enriched text/plain text
+
+ set delete = yes
+
+ # Binds
+ ${bindSection}
+
+ # Macros
+ ${macroSection}
+
+ ${optionalString cfg.vimKeys
+ "source ${pkgs.neomutt}/share/doc/neomutt/vim-keys/vim-keys.rc"}
+
+ # Extra configuration
+ ${optionsStr cfg.settings}
+
+ ${cfg.extraConfig}
+ '' + concatMapStringsSep "\n" registerAccount neomuttAccounts +
+ # source primary account
+ "source ${accountFilename (builtins.head primary)}";
+ };
+ };
+}
diff --git a/home-manager/modules/programs/neovim.nix b/home-manager/modules/programs/neovim.nix
new file mode 100644
index 00000000000..4101dc0f4e7
--- /dev/null
+++ b/home-manager/modules/programs/neovim.nix
@@ -0,0 +1,207 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.neovim;
+
+ extraPythonPackageType = mkOptionType {
+ name = "extra-python-packages";
+ description = "python packages in python.withPackages format";
+ check = with types; (x: if isFunction x
+ then isList (x pkgs.pythonPackages)
+ else false);
+ merge = mergeOneOption;
+ };
+
+ extraPython3PackageType = mkOptionType {
+ name = "extra-python3-packages";
+ description = "python3 packages in python.withPackages format";
+ check = with types; (x: if isFunction x
+ then isList (x pkgs.python3Packages)
+ else false);
+ merge = mergeOneOption;
+ };
+
+ moduleConfigure =
+ optionalAttrs (cfg.extraConfig != "") {
+ customRC = cfg.extraConfig;
+ }
+ // optionalAttrs (cfg.plugins != []) {
+ packages.home-manager.start = cfg.plugins;
+ };
+
+in
+
+{
+ options = {
+ programs.neovim = {
+ enable = mkEnableOption "Neovim";
+
+ viAlias = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Symlink `vi` to `nvim` binary.
+ '';
+ };
+
+ vimAlias = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Symlink `vim` to `nvim` binary.
+ '';
+ };
+
+ withNodeJs = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Enable node provider. Set to <literal>true</literal> to
+ use Node plugins.
+ '';
+ };
+
+ withPython = mkOption {
+ type = types.bool;
+ default = true;
+ description = ''
+ Enable Python 2 provider. Set to <literal>true</literal> to
+ use Python 2 plugins.
+ '';
+ };
+
+ extraPythonPackages = mkOption {
+ type = with types; either extraPythonPackageType (listOf package);
+ default = (_: []);
+ defaultText = "ps: []";
+ example = literalExample "(ps: with ps; [ pandas jedi ])";
+ description = ''
+ A function in python.withPackages format, which returns a
+ list of Python 2 packages required for your plugins to work.
+ '';
+ };
+
+ withRuby = mkOption {
+ type = types.nullOr types.bool;
+ default = true;
+ description = ''
+ Enable ruby provider.
+ '';
+ };
+
+ withPython3 = mkOption {
+ type = types.bool;
+ default = true;
+ description = ''
+ Enable Python 3 provider. Set to <literal>true</literal> to
+ use Python 3 plugins.
+ '';
+ };
+
+ extraPython3Packages = mkOption {
+ type = with types; either extraPython3PackageType (listOf package);
+ default = (_: []);
+ defaultText = "ps: []";
+ example = literalExample "(ps: with ps; [ python-language-server ])";
+ description = ''
+ A function in python.withPackages format, which returns a
+ list of Python 3 packages required for your plugins to work.
+ '';
+ };
+
+ package = mkOption {
+ type = types.package;
+ default = pkgs.neovim-unwrapped;
+ defaultText = literalExample "pkgs.neovim-unwrapped";
+ description = "The package to use for the neovim binary.";
+ };
+
+ finalPackage = mkOption {
+ type = types.package;
+ visible = false;
+ readOnly = true;
+ description = "Resulting customized neovim package.";
+ };
+
+ configure = mkOption {
+ type = types.attrs;
+ default = {};
+ example = literalExample ''
+ configure = {
+ customRC = $''''
+ " here your custom configuration goes!
+ $'''';
+ packages.myVimPackage = with pkgs.vimPlugins; {
+ # loaded on launch
+ start = [ fugitive ];
+ # manually loadable by calling `:packadd $plugin-name`
+ opt = [ ];
+ };
+ };
+ '';
+ description = ''
+ Generate your init file from your list of plugins and custom commands,
+ and loads it from the store via <command>nvim -u /nix/store/hash-vimrc</command>
+
+ </para><para>
+
+ This option is mutually exclusive with <varname>extraConfig</varname>
+ and <varname>plugins</varname>.
+ '';
+ };
+
+ extraConfig = mkOption {
+ type = types.lines;
+ default = "";
+ example = ''
+ set nocompatible
+ set nobackup
+ '';
+ description = ''
+ Custom vimrc lines.
+
+ </para><para>
+
+ This option is mutually exclusive with <varname>configure</varname>.
+ '';
+ };
+
+ plugins = mkOption {
+ type = with types; listOf package;
+ default = [ ];
+ example = literalExample "[ pkgs.vimPlugins.yankring ]";
+ description = ''
+ List of vim plugins to install.
+
+ </para><para>
+
+ This option is mutually exclusive with <varname>configure</varname>.
+ '';
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ assertions = [
+ {
+ assertion = cfg.configure == { } || moduleConfigure == { };
+ message = "The programs.neovim option configure is mutually exclusive"
+ + " with extraConfig and plugins.";
+ }
+ ];
+
+ home.packages = [ cfg.finalPackage ];
+
+ programs.neovim.finalPackage = pkgs.wrapNeovim cfg.package {
+ inherit (cfg)
+ extraPython3Packages withPython3
+ extraPythonPackages withPython
+ withNodeJs withRuby viAlias vimAlias;
+
+ configure = cfg.configure // moduleConfigure;
+ };
+ };
+}
diff --git a/home-manager/modules/programs/newsboat.nix b/home-manager/modules/programs/newsboat.nix
new file mode 100644
index 00000000000..6b59ed713d8
--- /dev/null
+++ b/home-manager/modules/programs/newsboat.nix
@@ -0,0 +1,119 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+ cfg = config.programs.newsboat;
+ wrapQuote = x: ''"${x}"'';
+
+in {
+ options = {
+ programs.newsboat = {
+ enable = mkEnableOption "the Newsboat feed reader";
+
+ urls = mkOption {
+ type = types.listOf (types.submodule {
+ options = {
+ url = mkOption {
+ type = types.str;
+ example = "http://example.com";
+ description = "Feed URL.";
+ };
+
+ tags = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ example = [ "foo" "bar" ];
+ description = "Feed tags.";
+ };
+
+ title = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ example = "ORF News";
+ description = "Feed title.";
+ };
+ };
+ });
+ default = [ ];
+ example = [{
+ url = "http://example.com";
+ tags = [ "foo" "bar" ];
+ }];
+ description = "List of news feeds.";
+ };
+
+ maxItems = mkOption {
+ type = types.int;
+ default = 0;
+ description = "Maximum number of items per feed, 0 for infinite.";
+ };
+
+ reloadThreads = mkOption {
+ type = types.int;
+ default = 5;
+ description = "How many threads to use for updating the feeds.";
+ };
+
+ autoReload = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Whether to enable automatic reloading while newsboat is running.
+ '';
+ };
+
+ reloadTime = mkOption {
+ type = types.nullOr types.int;
+ default = 60;
+ description = "Time in minutes between reloads.";
+ };
+
+ browser = mkOption {
+ type = types.str;
+ default = "${pkgs.xdg_utils}/bin/xdg-open";
+ description = "External browser to use.";
+ };
+
+ queries = mkOption {
+ type = types.attrsOf types.str;
+ default = { };
+ example = { "foo" = ''rssurl =~ "example.com"''; };
+ description = "A list of queries to use.";
+ };
+
+ extraConfig = mkOption {
+ type = types.lines;
+ default = "";
+ description = ''
+ Extra configuration values that will be appended to the end.
+ '';
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ pkgs.newsboat ];
+ home.file.".newsboat/urls".text = let
+ mkUrlEntry = u:
+ concatStringsSep " " ([ u.url ] ++ map wrapQuote u.tags
+ ++ optional (u.title != null) (wrapQuote "~${u.title}"));
+ urls = map mkUrlEntry cfg.urls;
+
+ mkQueryEntry = n: v: ''"query:${n}:${escape [ ''"'' ] v}"'';
+ queries = mapAttrsToList mkQueryEntry cfg.queries;
+ in concatStringsSep "\n" (urls ++ queries) + "\n";
+
+ home.file.".newsboat/config".text = ''
+ max-items ${toString cfg.maxItems}
+ browser ${cfg.browser}
+ reload-threads ${toString cfg.reloadThreads}
+ auto-reload ${if cfg.autoReload then "yes" else "no"}
+ ${optionalString (cfg.reloadTime != null)
+ (toString "reload-time ${toString cfg.reloadTime}")}
+ prepopulate-query-feeds yes
+
+ ${cfg.extraConfig}
+ '';
+ };
+}
diff --git a/home-manager/modules/programs/noti.nix b/home-manager/modules/programs/noti.nix
new file mode 100644
index 00000000000..348555eef51
--- /dev/null
+++ b/home-manager/modules/programs/noti.nix
@@ -0,0 +1,50 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.noti;
+
+in {
+ meta.maintainers = [ maintainers.marsam ];
+
+ options.programs.noti = {
+ enable = mkEnableOption "Noti";
+
+ settings = mkOption {
+ type = types.attrsOf (types.attrsOf types.str);
+ default = { };
+ description = ''
+ Configuration written to
+ <filename>~/.config/noti/noti.yaml</filename>.
+ </para><para>
+ See
+ <citerefentry>
+ <refentrytitle>noti.yaml</refentrytitle>
+ <manvolnum>5</manvolnum>
+ </citerefentry>.
+ for the full list of options.
+ '';
+ example = literalExample ''
+ {
+ say = {
+ voice = "Alex";
+ };
+ slack = {
+ token = "1234567890abcdefg";
+ channel = "@jaime";
+ };
+ }
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ pkgs.noti ];
+
+ xdg.configFile."noti/noti.yaml" =
+ mkIf (cfg.settings != { }) { text = generators.toYAML { } cfg.settings; };
+ };
+
+}
diff --git a/home-manager/modules/programs/notmuch-accounts.nix b/home-manager/modules/programs/notmuch-accounts.nix
new file mode 100644
index 00000000000..fd4a811d73d
--- /dev/null
+++ b/home-manager/modules/programs/notmuch-accounts.nix
@@ -0,0 +1,5 @@
+{ lib, ... }:
+
+{
+ options.notmuch = { enable = lib.mkEnableOption "notmuch indexing"; };
+}
diff --git a/home-manager/modules/programs/notmuch.nix b/home-manager/modules/programs/notmuch.nix
new file mode 100644
index 00000000000..7e7a140b20c
--- /dev/null
+++ b/home-manager/modules/programs/notmuch.nix
@@ -0,0 +1,191 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.notmuch;
+
+ mkIniKeyValue = key: value:
+ let
+ tweakVal = v:
+ if isString v then
+ v
+ else if isList v then
+ concatMapStringsSep ";" tweakVal v
+ else if isBool v then
+ (if v then "true" else "false")
+ else
+ toString v;
+ in "${key}=${tweakVal value}";
+
+ notmuchIni = recursiveUpdate {
+ database = { path = config.accounts.email.maildirBasePath; };
+
+ maildir = { synchronize_flags = cfg.maildir.synchronizeFlags; };
+
+ new = {
+ ignore = cfg.new.ignore;
+ tags = cfg.new.tags;
+ };
+
+ user = let
+ accounts = filter (a: a.notmuch.enable)
+ (attrValues config.accounts.email.accounts);
+ primary = filter (a: a.primary) accounts;
+ secondaries = filter (a: !a.primary) accounts;
+ in {
+ name = catAttrs "realName" primary;
+ primary_email = catAttrs "address" primary;
+ other_email = catAttrs "aliases" primary ++ catAttrs "address" secondaries
+ ++ catAttrs "aliases" secondaries;
+ };
+
+ search = { exclude_tags = cfg.search.excludeTags; };
+ } cfg.extraConfig;
+
+in {
+ options = {
+ programs.notmuch = {
+ enable = mkEnableOption "Notmuch mail indexer";
+
+ new = mkOption {
+ type = types.submodule {
+ options = {
+ ignore = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ description = ''
+ A list to specify files and directories that will not be
+ searched for messages by <command>notmuch new</command>.
+ '';
+ };
+
+ tags = mkOption {
+ type = types.listOf types.str;
+ default = [ "unread" "inbox" ];
+ example = [ "new" ];
+ description = ''
+ A list of tags that will be added to all messages
+ incorporated by <command>notmuch new</command>.
+ '';
+ };
+ };
+ };
+ default = { };
+ description = ''
+ Options related to email processing performed by
+ <command>notmuch new</command>.
+ '';
+ };
+
+ extraConfig = mkOption {
+ type = types.attrsOf (types.attrsOf types.str);
+ default = { };
+ description = ''
+ Options that should be appended to the notmuch configuration file.
+ '';
+ };
+
+ hooks = {
+ preNew = mkOption {
+ type = types.lines;
+ default = "";
+ example = "mbsync --all";
+ description = ''
+ Bash statements run before scanning or importing new
+ messages into the database.
+ '';
+ };
+
+ postNew = mkOption {
+ type = types.lines;
+ default = "";
+ example = ''
+ notmuch tag +nixos -- tag:new and from:nixos1@discoursemail.com
+ '';
+ description = ''
+ Bash statements run after new messages have been imported
+ into the database and initial tags have been applied.
+ '';
+ };
+
+ postInsert = mkOption {
+ type = types.lines;
+ default = "";
+ description = ''
+ Bash statements run after a message has been inserted
+ into the database and initial tags have been applied.
+ '';
+ };
+ };
+
+ maildir = {
+ synchronizeFlags = mkOption {
+ type = types.bool;
+ default = true;
+ description = ''
+ Whether to synchronize Maildir flags.
+ '';
+ };
+ };
+
+ search = {
+ excludeTags = mkOption {
+ type = types.listOf types.str;
+ default = [ "deleted" "spam" ];
+ example = [ "trash" "spam" ];
+ description = ''
+ A list of tags that will be excluded from search results by
+ default. Using an excluded tag in a query will override that
+ exclusion.
+ '';
+ };
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ assertions = [
+ {
+ assertion = notmuchIni.user.name != [ ];
+ message = "notmuch: Must have a user name set.";
+ }
+ {
+ assertion = notmuchIni.user.primary_email != [ ];
+ message = "notmuch: Must have a user primary email address set.";
+ }
+ ];
+
+ home.packages = [ pkgs.notmuch ];
+
+ home.sessionVariables = {
+ NOTMUCH_CONFIG = "${config.xdg.configHome}/notmuch/notmuchrc";
+ NMBGIT = "${config.xdg.dataHome}/notmuch/nmbug";
+ };
+
+ xdg.configFile."notmuch/notmuchrc".text =
+ let toIni = generators.toINI { mkKeyValue = mkIniKeyValue; };
+ in ''
+ # Generated by Home Manager.
+
+ '' + toIni notmuchIni;
+
+ home.file = let
+ hook = name: cmds: {
+ "${notmuchIni.database.path}/.notmuch/hooks/${name}".source =
+ pkgs.writeShellScript name ''
+ export PATH="${pkgs.notmuch}/bin''${PATH:+:}$PATH"
+ export NOTMUCH_CONFIG="${config.xdg.configHome}/notmuch/notmuchrc"
+ export NMBGIT="${config.xdg.dataHome}/notmuch/nmbug"
+
+ ${cmds}
+ '';
+ };
+ in optionalAttrs (cfg.hooks.preNew != "") (hook "pre-new" cfg.hooks.preNew)
+ // optionalAttrs (cfg.hooks.postNew != "")
+ (hook "post-new" cfg.hooks.postNew)
+ // optionalAttrs (cfg.hooks.postInsert != "")
+ (hook "post-insert" cfg.hooks.postInsert);
+ };
+}
diff --git a/home-manager/modules/programs/obs-studio.nix b/home-manager/modules/programs/obs-studio.nix
new file mode 100644
index 00000000000..6df5978384c
--- /dev/null
+++ b/home-manager/modules/programs/obs-studio.nix
@@ -0,0 +1,47 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.obs-studio;
+ package = pkgs.obs-studio;
+
+ mkPluginEnv = packages:
+ let
+ pluginDirs = map (pkg: "${pkg}/share/obs/obs-plugins") packages;
+ plugins = concatMapStringsSep " " (p: "${p}/*") pluginDirs;
+ in pkgs.runCommand "obs-studio-plugins" {
+ preferLocalBuild = true;
+ allowSubstitutes = false;
+ } ''
+ mkdir $out
+ [[ '${plugins}' ]] || exit 0
+ for plugin in ${plugins}; do
+ ln -s "$plugin" $out/
+ done
+ '';
+
+in {
+ meta.maintainers = [ maintainers.adisbladis ];
+
+ options = {
+ programs.obs-studio = {
+ enable = mkEnableOption "obs-studio";
+
+ plugins = mkOption {
+ default = [ ];
+ example = literalExample "[ pkgs.obs-linuxbrowser ]";
+ description = "Optional OBS plugins.";
+ type = types.listOf types.package;
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ package ];
+
+ xdg.configFile."obs-studio/plugins" =
+ mkIf (cfg.plugins != [ ]) { source = mkPluginEnv cfg.plugins; };
+ };
+}
diff --git a/home-manager/modules/programs/offlineimap-accounts.nix b/home-manager/modules/programs/offlineimap-accounts.nix
new file mode 100644
index 00000000000..afc7a019972
--- /dev/null
+++ b/home-manager/modules/programs/offlineimap-accounts.nix
@@ -0,0 +1,51 @@
+{ config, lib, ... }:
+
+with lib;
+
+let
+
+ extraConfigType = with types; attrsOf (either (either str int) bool);
+
+in {
+ options.offlineimap = {
+ enable = mkEnableOption "OfflineIMAP";
+
+ extraConfig.account = mkOption {
+ type = extraConfigType;
+ default = { };
+ example = { autorefresh = 20; };
+ description = ''
+ Extra configuration options to add to the account section.
+ '';
+ };
+
+ extraConfig.local = mkOption {
+ type = extraConfigType;
+ default = { };
+ example = { sync_deletes = true; };
+ description = ''
+ Extra configuration options to add to the local account
+ section.
+ '';
+ };
+
+ extraConfig.remote = mkOption {
+ type = extraConfigType;
+ default = { };
+ example = {
+ maxconnections = 2;
+ expunge = false;
+ };
+ description = ''
+ Extra configuration options to add to the remote account
+ section.
+ '';
+ };
+
+ postSyncHookCommand = mkOption {
+ type = types.lines;
+ default = "";
+ description = "Command to run after fetching new mails.";
+ };
+ };
+}
diff --git a/home-manager/modules/programs/offlineimap.nix b/home-manager/modules/programs/offlineimap.nix
new file mode 100644
index 00000000000..4ce12ec0a61
--- /dev/null
+++ b/home-manager/modules/programs/offlineimap.nix
@@ -0,0 +1,173 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.offlineimap;
+
+ accounts = filter (a: a.offlineimap.enable)
+ (attrValues config.accounts.email.accounts);
+
+ toIni = generators.toINI {
+ mkKeyValue = key: value:
+ let
+ value' = if isBool value then
+ (if value then "yes" else "no")
+ else
+ toString value;
+ in "${key} = ${value'}";
+ };
+
+ # Generates a script to fetch only a specific account.
+ #
+ # Note, these scripts are not actually created and installed at the
+ # moment. It will need some thinking on whether this is a good idea
+ # and whether other modules should have some similar functionality.
+ #
+ # Perhaps have a single tool `email` that wraps the command?
+ # Something like
+ #
+ # $ email <account name> <program name> <program args>
+ genOfflineImapScript = account:
+ with account;
+ pkgs.writeShellScriptBin "offlineimap-${name}" ''
+ exec ${pkgs.offlineimap}/bin/offlineimap -a${account.name} "$@"
+ '';
+
+ accountStr = account:
+ with account;
+ let
+ postSyncHook = optionalAttrs (offlineimap.postSyncHookCommand != "") {
+ postsynchook = pkgs.writeShellScriptBin "postsynchook"
+ offlineimap.postSyncHookCommand + "/bin/postsynchook";
+ };
+
+ localType =
+ if account.flavor == "gmail.com" then "GmailMaildir" else "Maildir";
+
+ remoteType = if account.flavor == "gmail.com" then "Gmail" else "IMAP";
+
+ remoteHost =
+ optionalAttrs (imap.host != null) { remotehost = imap.host; };
+
+ remotePort =
+ optionalAttrs ((imap.port or null) != null) { remoteport = imap.port; };
+
+ ssl = if imap.tls.enable then {
+ ssl = true;
+ sslcacertfile = imap.tls.certificatesFile;
+ starttls = imap.tls.useStartTls;
+ } else {
+ ssl = false;
+ };
+
+ remotePassEval =
+ let arglist = concatMapStringsSep "," (x: "'${x}'") passwordCommand;
+ in optionalAttrs (passwordCommand != null) {
+ remotepasseval = ''get_pass("${name}", [${arglist}])'';
+ };
+ in toIni {
+ "Account ${name}" = {
+ localrepository = "${name}-local";
+ remoterepository = "${name}-remote";
+ } // postSyncHook // offlineimap.extraConfig.account;
+
+ "Repository ${name}-local" = {
+ type = localType;
+ localfolders = maildir.absPath;
+ } // offlineimap.extraConfig.local;
+
+ "Repository ${name}-remote" = {
+ type = remoteType;
+ remoteuser = userName;
+ } // remoteHost // remotePort // remotePassEval // ssl
+ // offlineimap.extraConfig.remote;
+ };
+
+ extraConfigType = with types; attrsOf (either (either str int) bool);
+
+in {
+ options = {
+ programs.offlineimap = {
+ enable = mkEnableOption "OfflineIMAP";
+
+ pythonFile = mkOption {
+ type = types.lines;
+ default = ''
+ import subprocess
+
+ def get_pass(service, cmd):
+ return subprocess.check_output(cmd, )
+ '';
+ description = ''
+ Python code that can then be used in other parts of the
+ configuration.
+ '';
+ };
+
+ extraConfig.general = mkOption {
+ type = extraConfigType;
+ default = { };
+ example = {
+ maxage = 30;
+ ui = "blinkenlights";
+ };
+ description = ''
+ Extra configuration options added to the
+ <option>general</option> section.
+ '';
+ };
+
+ extraConfig.default = mkOption {
+ type = extraConfigType;
+ default = { };
+ example = { gmailtrashfolder = "[Gmail]/Papierkorb"; };
+ description = ''
+ Extra configuration options added to the
+ <option>DEFAULT</option> section.
+ '';
+ };
+
+ extraConfig.mbnames = mkOption {
+ type = extraConfigType;
+ default = { };
+ example = literalExample ''
+ {
+ filename = "~/.config/mutt/mailboxes";
+ header = "'mailboxes '";
+ peritem = "'+%(accountname)s/%(foldername)s'";
+ sep = "' '";
+ footer = "'\\n'";
+ }
+ '';
+ description = ''
+ Extra configuration options added to the
+ <code>mbnames</code> section.
+ '';
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ pkgs.offlineimap ];
+
+ xdg.configFile."offlineimap/get_settings.py".text = cfg.pythonFile;
+
+ xdg.configFile."offlineimap/config".text = ''
+ # Generated by Home Manager.
+ # See https://github.com/OfflineIMAP/offlineimap/blob/master/offlineimap.conf
+ # for an exhaustive list of options.
+ '' + toIni ({
+ general = {
+ accounts = concatMapStringsSep "," (a: a.name) accounts;
+ pythonfile = "${config.xdg.configHome}/offlineimap/get_settings.py";
+ metadata = "${config.xdg.dataHome}/offlineimap";
+ } // cfg.extraConfig.general;
+ } // optionalAttrs (cfg.extraConfig.mbnames != { }) {
+ mbnames = { enabled = true; } // cfg.extraConfig.mbnames;
+ } // optionalAttrs (cfg.extraConfig.default != { }) {
+ DEFAULT = cfg.extraConfig.default;
+ }) + "\n" + concatStringsSep "\n" (map accountStr accounts);
+ };
+}
diff --git a/home-manager/modules/programs/opam.nix b/home-manager/modules/programs/opam.nix
new file mode 100644
index 00000000000..a61ff7878df
--- /dev/null
+++ b/home-manager/modules/programs/opam.nix
@@ -0,0 +1,50 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.opam;
+
+in {
+ meta.maintainers = [ maintainers.marsam ];
+
+ options.programs.opam = {
+ enable = mkEnableOption "Opam";
+
+ package = mkOption {
+ type = types.package;
+ default = pkgs.opam;
+ defaultText = literalExample "pkgs.opam";
+ description = "Opam package to install.";
+ };
+
+ enableBashIntegration = mkOption {
+ default = true;
+ type = types.bool;
+ description = ''
+ Whether to enable Bash integration.
+ '';
+ };
+
+ enableZshIntegration = mkOption {
+ default = true;
+ type = types.bool;
+ description = ''
+ Whether to enable Zsh integration.
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ cfg.package ];
+
+ programs.bash.initExtra = mkIf cfg.enableBashIntegration ''
+ eval "$(${cfg.package}/bin/opam env --shell=bash)"
+ '';
+
+ programs.zsh.initExtra = mkIf cfg.enableZshIntegration ''
+ eval "$(${cfg.package}/bin/opam env --shell=zsh)"
+ '';
+ };
+}
diff --git a/home-manager/modules/programs/password-store.nix b/home-manager/modules/programs/password-store.nix
new file mode 100644
index 00000000000..db31146a1ba
--- /dev/null
+++ b/home-manager/modules/programs/password-store.nix
@@ -0,0 +1,62 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.password-store;
+
+in {
+ meta.maintainers = with maintainers; [ pacien ];
+
+ options.programs.password-store = {
+ enable = mkEnableOption "Password store";
+
+ package = mkOption {
+ type = types.package;
+ default = pkgs.pass;
+ defaultText = literalExample "pkgs.pass";
+ example = literalExample ''
+ pkgs.pass.withExtensions (exts: [ exts.pass-otp ])
+ '';
+ description = ''
+ The <literal>pass</literal> package to use.
+ Can be used to specify extensions.
+ '';
+ };
+
+ settings = mkOption rec {
+ type = with types; attrsOf str;
+ apply = mergeAttrs default;
+ default = {
+ PASSWORD_STORE_DIR = "${config.xdg.dataHome}/password-store";
+ };
+ defaultText = literalExample ''
+ { PASSWORD_STORE_DIR = "$XDG_DATA_HOME/password-store"; }
+ '';
+ example = literalExample ''
+ {
+ PASSWORD_STORE_DIR = "/some/directory";
+ PASSWORD_STORE_KEY = "12345678";
+ PASSWORD_STORE_CLIP_TIME = "60";
+ }
+ '';
+ description = ''
+ The <literal>pass</literal> environment variables dictionary.
+ </para><para>
+ See the "Environment variables" section of
+ <citerefentry>
+ <refentrytitle>pass</refentrytitle>
+ <manvolnum>1</manvolnum>
+ </citerefentry>
+ and the extension man pages for more information about the
+ available keys.
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ cfg.package ];
+ home.sessionVariables = cfg.settings;
+ };
+}
diff --git a/home-manager/modules/programs/pazi.nix b/home-manager/modules/programs/pazi.nix
new file mode 100644
index 00000000000..e1a08eb615a
--- /dev/null
+++ b/home-manager/modules/programs/pazi.nix
@@ -0,0 +1,55 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.pazi;
+
+in {
+ meta.maintainers = [ maintainers.marsam ];
+
+ options.programs.pazi = {
+ enable = mkEnableOption "pazi";
+
+ enableBashIntegration = mkOption {
+ default = true;
+ type = types.bool;
+ description = ''
+ Whether to enable Bash integration.
+ '';
+ };
+
+ enableZshIntegration = mkOption {
+ default = true;
+ type = types.bool;
+ description = ''
+ Whether to enable Zsh integration.
+ '';
+ };
+
+ enableFishIntegration = mkOption {
+ default = true;
+ type = types.bool;
+ description = ''
+ Whether to enable Fish integration.
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ pkgs.pazi ];
+
+ programs.bash.initExtra = mkIf cfg.enableBashIntegration ''
+ eval "$(${pkgs.pazi}/bin/pazi init bash)"
+ '';
+
+ programs.zsh.initExtra = mkIf cfg.enableZshIntegration ''
+ eval "$(${pkgs.pazi}/bin/pazi init zsh)"
+ '';
+
+ programs.fish.shellInit = mkIf cfg.enableFishIntegration ''
+ ${pkgs.pazi}/bin/pazi init fish | source
+ '';
+ };
+}
diff --git a/home-manager/modules/programs/pidgin.nix b/home-manager/modules/programs/pidgin.nix
new file mode 100644
index 00000000000..a375fd1b2bd
--- /dev/null
+++ b/home-manager/modules/programs/pidgin.nix
@@ -0,0 +1,34 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.pidgin;
+
+in {
+ meta.maintainers = [ maintainers.rycee ];
+
+ options = {
+ programs.pidgin = {
+ enable = mkEnableOption "Pidgin messaging client";
+
+ package = mkOption {
+ type = types.package;
+ default = pkgs.pidgin;
+ defaultText = literalExample "pkgs.pidgin";
+ description = "The Pidgin package to use.";
+ };
+
+ plugins = mkOption {
+ default = [ ];
+ example = literalExample "[ pkgs.pidgin-otr pkgs.pidgin-osd ]";
+ description = "Plugins that should be available to Pidgin.";
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ (cfg.package.override { inherit (cfg) plugins; }) ];
+ };
+}
diff --git a/home-manager/modules/programs/readline.nix b/home-manager/modules/programs/readline.nix
new file mode 100644
index 00000000000..2f79df6e103
--- /dev/null
+++ b/home-manager/modules/programs/readline.nix
@@ -0,0 +1,77 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.readline;
+
+ mkSetVariableStr = n: v:
+ let
+ mkValueStr = v:
+ if v == true then
+ "on"
+ else if v == false then
+ "off"
+ else if isInt v then
+ toString v
+ else if isString v then
+ v
+ else
+ abort ("values ${toPretty v} is of unsupported type");
+ in "set ${n} ${mkValueStr v}";
+
+ mkBindingStr = k: v: ''"${k}": ${v}'';
+
+in {
+ options.programs.readline = {
+ enable = mkEnableOption "readline";
+
+ bindings = mkOption {
+ default = { };
+ type = types.attrsOf types.str;
+ example = literalExample ''
+ { "\\C-h" = "backward-kill-word"; }
+ '';
+ description = "Readline bindings.";
+ };
+
+ variables = mkOption {
+ type = with types; attrsOf (either str (either int bool));
+ default = { };
+ example = { expand-tilde = true; };
+ description = ''
+ Readline customization variable assignments.
+ '';
+ };
+
+ includeSystemConfig = mkOption {
+ type = types.bool;
+ default = true;
+ description = "Whether to include the system-wide configuration.";
+ };
+
+ extraConfig = mkOption {
+ type = types.lines;
+ default = "";
+ description = ''
+ Configuration lines appended unchanged to the end of the
+ <filename>~/.inputrc</filename> file.
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.file.".inputrc".text = let
+ configStr = concatStringsSep "\n"
+ (optional cfg.includeSystemConfig "$include /etc/inputrc"
+ ++ mapAttrsToList mkSetVariableStr cfg.variables
+ ++ mapAttrsToList mkBindingStr cfg.bindings);
+ in ''
+ # Generated by Home Manager.
+
+ ${configStr}
+ ${cfg.extraConfig}
+ '';
+ };
+}
diff --git a/home-manager/modules/programs/rofi.nix b/home-manager/modules/programs/rofi.nix
new file mode 100644
index 00000000000..f344e88e2ff
--- /dev/null
+++ b/home-manager/modules/programs/rofi.nix
@@ -0,0 +1,327 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+with builtins;
+
+let
+
+ cfg = config.programs.rofi;
+
+ colorOption = description:
+ mkOption {
+ type = types.str;
+ description = description;
+ };
+
+ rowColorSubmodule = types.submodule {
+ options = {
+ background = colorOption "Background color";
+ foreground = colorOption "Foreground color";
+ backgroundAlt = colorOption "Alternative background color";
+ highlight = mkOption {
+ type = types.submodule {
+ options = {
+ background = colorOption "Highlight background color";
+ foreground = colorOption "Highlight foreground color";
+ };
+ };
+ description = "Color settings for highlighted row.";
+ };
+ };
+ };
+
+ windowColorSubmodule = types.submodule {
+ options = {
+ background = colorOption "Window background color";
+ border = colorOption "Window border color";
+ separator = colorOption "Separator color";
+ };
+ };
+
+ colorsSubmodule = types.submodule {
+ options = {
+ window = mkOption {
+ default = null;
+ type = windowColorSubmodule;
+ description = "Window color settings.";
+ };
+ rows = mkOption {
+ default = null;
+ type = types.submodule {
+ options = {
+ normal = mkOption {
+ default = null;
+ type = types.nullOr rowColorSubmodule;
+ description = "Normal row color settings.";
+ };
+ active = mkOption {
+ default = null;
+ type = types.nullOr rowColorSubmodule;
+ description = "Active row color settings.";
+ };
+ urgent = mkOption {
+ default = null;
+ type = types.nullOr rowColorSubmodule;
+ description = "Urgent row color settings.";
+ };
+ };
+ };
+ description = "Rows color settings.";
+ };
+ };
+ };
+
+ valueToString = value:
+ if isBool value then (if value then "true" else "else") else toString value;
+
+ windowColorsToString = window:
+ concatStringsSep ", " (with window; [ background border separator ]);
+
+ rowsColorsToString = rows: ''
+ ${optionalString (rows.normal != null)
+ (setOption "color-normal" (rowColorsToString rows.normal))}
+ ${optionalString (rows.active != null)
+ (setOption "color-active" (rowColorsToString rows.active))}
+ ${optionalString (rows.urgent != null)
+ (setOption "color-urgent" (rowColorsToString rows.urgent))}
+ '';
+
+ rowColorsToString = row:
+ concatStringsSep ", " (with row; [
+ background
+ foreground
+ backgroundAlt
+ highlight.background
+ highlight.foreground
+ ]);
+
+ setOption = name: value:
+ optionalString (value != null) "rofi.${name}: ${valueToString value}";
+
+ setColorScheme = colors:
+ optionalString (colors != null) ''
+ ${optionalString (colors.window != null) setOption "color-window"
+ (windowColorsToString colors.window)}
+ ${optionalString (colors.rows != null) (rowsColorsToString colors.rows)}
+ '';
+
+ locationsMap = {
+ center = 0;
+ top-left = 1;
+ top = 2;
+ top-right = 3;
+ right = 4;
+ bottom-right = 5;
+ bottom = 6;
+ bottom-left = 7;
+ left = 8;
+ };
+
+ themeName = if (cfg.theme == null) then
+ null
+ else if (lib.isString cfg.theme) then
+ cfg.theme
+ else
+ lib.removeSuffix ".rasi" (baseNameOf cfg.theme);
+
+ themePath = if (lib.isString cfg.theme) then null else cfg.theme;
+
+in {
+ options.programs.rofi = {
+ enable = mkEnableOption
+ "Rofi: A window switcher, application launcher and dmenu replacement";
+
+ width = mkOption {
+ default = null;
+ type = types.nullOr types.int;
+ description = "Window width";
+ example = 100;
+ };
+
+ lines = mkOption {
+ default = null;
+ type = types.nullOr types.int;
+ description = "Number of lines";
+ example = 10;
+ };
+
+ borderWidth = mkOption {
+ default = null;
+ type = types.nullOr types.int;
+ description = "Border width";
+ example = 1;
+ };
+
+ rowHeight = mkOption {
+ default = null;
+ type = types.nullOr types.int;
+ description = "Row height (in chars)";
+ example = 1;
+ };
+
+ padding = mkOption {
+ default = null;
+ type = types.nullOr types.int;
+ description = "Padding";
+ example = 400;
+ };
+
+ font = mkOption {
+ default = null;
+ type = types.nullOr types.str;
+ example = "Droid Sans Mono 14";
+ description = "Font to use.";
+ };
+
+ scrollbar = mkOption {
+ default = null;
+ type = types.nullOr types.bool;
+ description = "Whether to show a scrollbar.";
+ };
+
+ terminal = mkOption {
+ default = null;
+ type = types.nullOr types.str;
+ description = ''
+ Path to the terminal which will be used to run console applications
+ '';
+ example = "\${pkgs.gnome3.gnome_terminal}/bin/gnome-terminal";
+ };
+
+ separator = mkOption {
+ default = null;
+ type = types.nullOr (types.enum [ "none" "dash" "solid" ]);
+ description = "Separator style";
+ example = "solid";
+ };
+
+ cycle = mkOption {
+ default = null;
+ type = types.nullOr types.bool;
+ description = "Whether to cycle through the results list.";
+ };
+
+ fullscreen = mkOption {
+ default = null;
+ type = types.nullOr types.bool;
+ description = "Whether to run rofi fullscreen.";
+ };
+
+ location = mkOption {
+ default = "center";
+ type = types.enum (builtins.attrNames locationsMap);
+ description = "The location rofi appears on the screen.";
+ };
+
+ xoffset = mkOption {
+ default = 0;
+ type = types.int;
+ description = ''
+ Offset in the x-axis in pixels relative to the chosen location.
+ '';
+ };
+
+ yoffset = mkOption {
+ default = 0;
+ type = types.int;
+ description = ''
+ Offset in the y-axis in pixels relative to the chosen location.
+ '';
+ };
+
+ colors = mkOption {
+ default = null;
+ type = types.nullOr colorsSubmodule;
+ description = ''
+ Color scheme settings. Colors can be specified in CSS color
+ formats. This option may become deprecated in the future and
+ therefore the <varname>programs.rofi.theme</varname> option
+ should be used whenever possible.
+ '';
+ example = literalExample ''
+ colors = {
+ window = {
+ background = "argb:583a4c54";
+ border = "argb:582a373e";
+ separator = "#c3c6c8";
+ };
+
+ rows = {
+ normal = {
+ background = "argb:58455a64";
+ foreground = "#fafbfc";
+ backgroundAlt = "argb:58455a64";
+ highlight = {
+ background = "#00bcd4";
+ foreground = "#fafbfc";
+ };
+ };
+ };
+ };
+ '';
+ };
+
+ theme = mkOption {
+ default = null;
+ type = with types; nullOr (either str path);
+ example = "Arc";
+ description = ''
+ Name of theme or path to theme file in rasi format. Available
+ named themes can be viewed using the
+ <command>rofi-theme-selector</command> tool.
+ '';
+ };
+
+ configPath = mkOption {
+ default = "${config.xdg.configHome}/rofi/config";
+ defaultText = "$XDG_CONFIG_HOME/rofi/config";
+ type = types.str;
+ description = "Path where to put generated configuration file.";
+ };
+
+ extraConfig = mkOption {
+ default = "";
+ type = types.lines;
+ description = "Additional configuration to add.";
+ };
+
+ };
+
+ config = mkIf cfg.enable {
+ assertions = [{
+ assertion = cfg.theme == null || cfg.colors == null;
+ message = ''
+ Cannot use the rofi options 'theme' and 'colors' simultaneously.
+ '';
+ }];
+
+ home.packages = [ pkgs.rofi ];
+
+ home.file."${cfg.configPath}".text = ''
+ ${setOption "width" cfg.width}
+ ${setOption "lines" cfg.lines}
+ ${setOption "font" cfg.font}
+ ${setOption "bw" cfg.borderWidth}
+ ${setOption "eh" cfg.rowHeight}
+ ${setOption "padding" cfg.padding}
+ ${setOption "separator-style" cfg.separator}
+ ${setOption "hide-scrollbar"
+ (if (cfg.scrollbar != null) then (!cfg.scrollbar) else cfg.scrollbar)}
+ ${setOption "terminal" cfg.terminal}
+ ${setOption "cycle" cfg.cycle}
+ ${setOption "fullscreen" cfg.fullscreen}
+ ${setOption "location" (builtins.getAttr cfg.location locationsMap)}
+ ${setOption "xoffset" cfg.xoffset}
+ ${setOption "yoffset" cfg.yoffset}
+
+ ${setColorScheme cfg.colors}
+ ${setOption "theme" themeName}
+
+ ${cfg.extraConfig}
+ '';
+
+ xdg.dataFile = mkIf (themePath != null) {
+ "rofi/themes/${themeName}.rasi".source = themePath;
+ };
+ };
+}
diff --git a/home-manager/modules/programs/rtorrent.nix b/home-manager/modules/programs/rtorrent.nix
new file mode 100644
index 00000000000..7beeb2e4221
--- /dev/null
+++ b/home-manager/modules/programs/rtorrent.nix
@@ -0,0 +1,34 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.rtorrent;
+
+in {
+ meta.maintainers = [ maintainers.marsam ];
+
+ options.programs.rtorrent = {
+ enable = mkEnableOption "rTorrent";
+
+ settings = mkOption {
+ type = types.lines;
+ default = "";
+ description = ''
+ Configuration written to
+ <filename>~/.config/rtorrent/rtorrent.rc</filename>. See
+ <link xlink:href="https://github.com/rakshasa/rtorrent/wiki/Config-Guide" />
+ for explanation about possible values.
+ '';
+ };
+
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ pkgs.rtorrent ];
+
+ xdg.configFile."rtorrent/rtorrent.rc" =
+ mkIf (cfg.settings != "") { text = cfg.settings; };
+ };
+}
diff --git a/home-manager/modules/programs/skim.nix b/home-manager/modules/programs/skim.nix
new file mode 100644
index 00000000000..c90fe1b1a35
--- /dev/null
+++ b/home-manager/modules/programs/skim.nix
@@ -0,0 +1,124 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.skim;
+
+in {
+ options.programs.skim = {
+ enable = mkEnableOption "skim - a command-line fuzzy finder";
+
+ defaultCommand = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ example = "fd --type f";
+ description = ''
+ The command that gets executed as the default source for skim
+ when running.
+ '';
+ };
+
+ defaultOptions = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ example = [ "--height 40%" "--prompt ⟫" ];
+ description = ''
+ Extra command line options given to skim by default.
+ '';
+ };
+
+ fileWidgetCommand = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ example = "fd --type f";
+ description = ''
+ The command that gets executed as the source for skim for the
+ CTRL-T keybinding.
+ '';
+ };
+
+ fileWidgetOptions = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ example = [ "--preview 'head {}'" ];
+ description = ''
+ Command line options for the CTRL-T keybinding.
+ '';
+ };
+
+ changeDirWidgetCommand = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ example = "fd --type d";
+ description = ''
+ The command that gets executed as the source for skim for the
+ ALT-C keybinding.
+ '';
+ };
+
+ changeDirWidgetOptions = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ example = [ "--preview 'tree -C {} | head -200'" ];
+ description = ''
+ Command line options for the ALT-C keybinding.
+ '';
+ };
+
+ historyWidgetOptions = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ example = [ "--tac" "--exact" ];
+ description = ''
+ Command line options for the CTRL-R keybinding.
+ '';
+ };
+
+ enableBashIntegration = mkOption {
+ default = true;
+ type = types.bool;
+ description = ''
+ Whether to enable Bash integration.
+ '';
+ };
+
+ enableZshIntegration = mkOption {
+ default = true;
+ type = types.bool;
+ description = ''
+ Whether to enable Zsh integration.
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ pkgs.skim ];
+
+ home.sessionVariables = mapAttrs (n: v: toString v)
+ (filterAttrs (n: v: v != [ ] && v != null) {
+ SKIM_ALT_C_COMMAND = cfg.changeDirWidgetCommand;
+ SKIM_ALT_C_OPTS = cfg.changeDirWidgetOptions;
+ SKIM_CTRL_R_OPTS = cfg.historyWidgetOptions;
+ SKIM_CTRL_T_COMMAND = cfg.fileWidgetCommand;
+ SKIM_CTRL_T_OPTS = cfg.fileWidgetOptions;
+ SKIM_DEFAULT_COMMAND = cfg.defaultCommand;
+ SKIM_DEFAULT_OPTIONS = cfg.defaultOptions;
+ });
+
+ programs.bash.initExtra = mkIf cfg.enableBashIntegration ''
+ if [[ :$SHELLOPTS: =~ :(vi|emacs): ]]; then
+ . ${pkgs.skim}/share/skim/completion.bash
+ . ${pkgs.skim}/share/skim/key-bindings.bash
+ fi
+ '';
+
+ programs.zsh.initExtra = mkIf cfg.enableZshIntegration ''
+ if [[ $options[zle] = on ]]; then
+ . ${pkgs.skim}/share/skim/completion.zsh
+ . ${pkgs.skim}/share/skim/key-bindings.zsh
+ fi
+ '';
+ };
+}
diff --git a/home-manager/modules/programs/ssh.nix b/home-manager/modules/programs/ssh.nix
new file mode 100644
index 00000000000..6b0747dd9b1
--- /dev/null
+++ b/home-manager/modules/programs/ssh.nix
@@ -0,0 +1,461 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.ssh;
+
+ isPath = x: builtins.substring 0 1 (toString x) == "/";
+
+ addressPort = entry:
+ if isPath entry.address
+ then " ${entry.address}"
+ else " [${entry.address}]:${toString entry.port}";
+
+ yn = flag: if flag then "yes" else "no";
+
+ unwords = builtins.concatStringsSep " ";
+
+ bindOptions = {
+ address = mkOption {
+ type = types.str;
+ default = "localhost";
+ example = "example.org";
+ description = "The address where to bind the port.";
+ };
+
+ port = mkOption {
+ type = types.port;
+ example = 8080;
+ description = "Specifies port number to bind on bind address.";
+ };
+ };
+
+ dynamicForwardModule = types.submodule {
+ options = bindOptions;
+ };
+
+ forwardModule = types.submodule {
+ options = {
+ bind = bindOptions;
+
+ host = {
+ address = mkOption {
+ type = types.str;
+ example = "example.org";
+ description = "The address where to forward the traffic to.";
+ };
+
+ port = mkOption {
+ type = types.port;
+ example = 80;
+ description = "Specifies port number to forward the traffic to.";
+ };
+ };
+ };
+ };
+
+ matchBlockModule = types.submodule ({ name, ... }: {
+ options = {
+ host = mkOption {
+ type = types.str;
+ example = "*.example.org";
+ description = ''
+ The host pattern used by this conditional block.
+ '';
+ };
+
+ port = mkOption {
+ type = types.nullOr types.port;
+ default = null;
+ description = "Specifies port number to connect on remote host.";
+ };
+
+ forwardAgent = mkOption {
+ default = null;
+ type = types.nullOr types.bool;
+ description = ''
+ Whether the connection to the authentication agent (if any)
+ will be forwarded to the remote machine.
+ '';
+ };
+
+ forwardX11 = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Specifies whether X11 connections will be automatically redirected
+ over the secure channel and <envar>DISPLAY</envar> set.
+ '';
+ };
+
+ forwardX11Trusted = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Specifies whether remote X11 clients will have full access to the
+ original X11 display.
+ '';
+ };
+
+ identitiesOnly = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Specifies that ssh should only use the authentication
+ identity explicitly configured in the
+ <filename>~/.ssh/config</filename> files or passed on the
+ ssh command-line, even if <command>ssh-agent</command>
+ offers more identities.
+ '';
+ };
+
+ identityFile = mkOption {
+ type = with types; either (listOf str) (nullOr str);
+ default = [];
+ apply = p:
+ if p == null then []
+ else if isString p then [p]
+ else p;
+ description = ''
+ Specifies files from which the user identity is read.
+ Identities will be tried in the given order.
+ '';
+ };
+
+ user = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = "Specifies the user to log in as.";
+ };
+
+ hostname = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = "Specifies the real host name to log into.";
+ };
+
+ serverAliveInterval = mkOption {
+ type = types.int;
+ default = 0;
+ description =
+ "Set timeout in seconds after which response will be requested.";
+ };
+
+ sendEnv = mkOption {
+ type = types.listOf types.str;
+ default = [];
+ description = ''
+ Environment variables to send from the local host to the
+ server.
+ '';
+ };
+
+ compression = mkOption {
+ type = types.nullOr types.bool;
+ default = null;
+ description = ''
+ Specifies whether to use compression. Omitted from the host
+ block when <literal>null</literal>.
+ '';
+ };
+
+ checkHostIP = mkOption {
+ type = types.bool;
+ default = true;
+ description = ''
+ Check the host IP address in the
+ <filename>known_hosts</filename> file.
+ '';
+ };
+
+ proxyCommand = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = "The command to use to connect to the server.";
+ };
+
+ proxyJump = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = "The proxy host to use to connect to the server.";
+ };
+
+ certificateFile = mkOption {
+ type = with types; either (listOf str) (nullOr str);
+ default = [];
+ apply = p:
+ if p == null then []
+ else if isString p then [p]
+ else p;
+ description = ''
+ Specifies files from which the user certificate is read.
+ '';
+ };
+
+ addressFamily = mkOption {
+ default = null;
+ type = types.nullOr (types.enum ["any" "inet" "inet6"]);
+ description = ''
+ Specifies which address family to use when connecting.
+ '';
+ };
+
+ localForwards = mkOption {
+ type = types.listOf forwardModule;
+ default = [];
+ example = literalExample ''
+ [
+ {
+ bind.port = 8080;
+ host.address = "10.0.0.13";
+ host.port = 80;
+ }
+ ];
+ '';
+ description = ''
+ Specify local port forwardings. See
+ <citerefentry>
+ <refentrytitle>ssh_config</refentrytitle>
+ <manvolnum>5</manvolnum>
+ </citerefentry> for <literal>LocalForward</literal>.
+ '';
+ };
+
+ remoteForwards = mkOption {
+ type = types.listOf forwardModule;
+ default = [];
+ example = literalExample ''
+ [
+ {
+ bind.port = 8080;
+ host.address = "10.0.0.13";
+ host.port = 80;
+ }
+ ];
+ '';
+ description = ''
+ Specify remote port forwardings. See
+ <citerefentry>
+ <refentrytitle>ssh_config</refentrytitle>
+ <manvolnum>5</manvolnum>
+ </citerefentry> for <literal>RemoteForward</literal>.
+ '';
+ };
+
+ dynamicForwards = mkOption {
+ type = types.listOf dynamicForwardModule;
+ default = [];
+ example = literalExample ''
+ [ { port = 8080; } ];
+ '';
+ description = ''
+ Specify dynamic port forwardings. See
+ <citerefentry>
+ <refentrytitle>ssh_config</refentrytitle>
+ <manvolnum>5</manvolnum>
+ </citerefentry> for <literal>DynamicForward</literal>.
+ '';
+ };
+
+ extraOptions = mkOption {
+ type = types.attrsOf types.str;
+ default = {};
+ description = "Extra configuration options for the host.";
+ };
+ };
+
+ config.host = mkDefault name;
+ });
+
+ matchBlockStr = cf: concatStringsSep "\n" (
+ ["Host ${cf.host}"]
+ ++ optional (cf.port != null) " Port ${toString cf.port}"
+ ++ optional (cf.forwardAgent != null) " ForwardAgent ${yn cf.forwardAgent}"
+ ++ optional cf.forwardX11 " ForwardX11 yes"
+ ++ optional cf.forwardX11Trusted " ForwardX11Trusted yes"
+ ++ optional cf.identitiesOnly " IdentitiesOnly yes"
+ ++ optional (cf.user != null) " User ${cf.user}"
+ ++ optional (cf.hostname != null) " HostName ${cf.hostname}"
+ ++ optional (cf.addressFamily != null) " AddressFamily ${cf.addressFamily}"
+ ++ optional (cf.sendEnv != []) " SendEnv ${unwords cf.sendEnv}"
+ ++ optional (cf.serverAliveInterval != 0)
+ " ServerAliveInterval ${toString cf.serverAliveInterval}"
+ ++ optional (cf.compression != null) " Compression ${yn cf.compression}"
+ ++ optional (!cf.checkHostIP) " CheckHostIP no"
+ ++ optional (cf.proxyCommand != null) " ProxyCommand ${cf.proxyCommand}"
+ ++ optional (cf.proxyJump != null) " ProxyJump ${cf.proxyJump}"
+ ++ map (file: " IdentityFile ${file}") cf.identityFile
+ ++ map (file: " CertificateFile ${file}") cf.certificateFile
+ ++ map (f: " LocalForward" + addressPort f.bind + addressPort f.host) cf.localForwards
+ ++ map (f: " RemoteForward" + addressPort f.bind + addressPort f.host) cf.remoteForwards
+ ++ map (f: " DynamicForward" + addressPort f) cf.dynamicForwards
+ ++ mapAttrsToList (n: v: " ${n} ${v}") cf.extraOptions
+ );
+
+in
+
+{
+ meta.maintainers = [ maintainers.rycee ];
+
+ options.programs.ssh = {
+ enable = mkEnableOption "SSH client configuration";
+
+ forwardAgent = mkOption {
+ default = false;
+ type = types.bool;
+ description = ''
+ Whether the connection to the authentication agent (if any)
+ will be forwarded to the remote machine.
+ '';
+ };
+
+ compression = mkOption {
+ default = false;
+ type = types.bool;
+ description = "Specifies whether to use compression.";
+ };
+
+ serverAliveInterval = mkOption {
+ type = types.int;
+ default = 0;
+ description = ''
+ Set default timeout in seconds after which response will be requested.
+ '';
+ };
+
+ hashKnownHosts = mkOption {
+ default = false;
+ type = types.bool;
+ description = ''
+ Indicates that
+ <citerefentry>
+ <refentrytitle>ssh</refentrytitle>
+ <manvolnum>1</manvolnum>
+ </citerefentry>
+ should hash host names and addresses when they are added to
+ the known hosts file.
+ '';
+ };
+
+ userKnownHostsFile = mkOption {
+ type = types.str;
+ default = "~/.ssh/known_hosts";
+ description = ''
+ Specifies one or more files to use for the user host key
+ database, separated by whitespace. The default is
+ <filename>~/.ssh/known_hosts</filename>.
+ '';
+ };
+
+ controlMaster = mkOption {
+ default = "no";
+ type = types.enum ["yes" "no" "ask" "auto" "autoask"];
+ description = ''
+ Configure sharing of multiple sessions over a single network connection.
+ '';
+ };
+
+ controlPath = mkOption {
+ type = types.str;
+ default = "~/.ssh/master-%r@%n:%p";
+ description = ''
+ Specify path to the control socket used for connection sharing.
+ '';
+ };
+
+ controlPersist = mkOption {
+ type = types.str;
+ default = "no";
+ example = "10m";
+ description = ''
+ Whether control socket should remain open in the background.
+ '';
+ };
+
+ extraConfig = mkOption {
+ type = types.lines;
+ default = "";
+ description = ''
+ Extra configuration.
+ '';
+ };
+
+ extraOptionOverrides = mkOption {
+ type = types.attrsOf types.str;
+ default = {};
+ description = ''
+ Extra SSH configuration options that take precedence over any
+ host specific configuration.
+ '';
+ };
+
+ matchBlocks = mkOption {
+ type = types.loaOf matchBlockModule;
+ default = {};
+ example = literalExample ''
+ {
+ "john.example.com" = {
+ hostname = "example.com";
+ user = "john";
+ };
+ foo = {
+ hostname = "example.com";
+ identityFile = "/home/john/.ssh/foo_rsa";
+ };
+ };
+ '';
+ description = ''
+ Specify per-host settings. Note, if the order of rules matter
+ then this must be a list. See
+ <citerefentry>
+ <refentrytitle>ssh_config</refentrytitle>
+ <manvolnum>5</manvolnum>
+ </citerefentry>.
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+ assertions = [
+ {
+ assertion =
+ let
+ # `builtins.any`/`lib.lists.any` does not return `true` if there are no elements.
+ any' = pred: items: if items == [] then true else any pred items;
+ # Check that if `entry.address` is defined, and is a path, that `entry.port` has not
+ # been defined.
+ noPathWithPort = entry: entry ? address && isPath entry.address -> !(entry ? port);
+ checkDynamic = block: any' noPathWithPort block.dynamicForwards;
+ checkBindAndHost = fwd: noPathWithPort fwd.bind && noPathWithPort fwd.host;
+ checkLocal = block: any' checkBindAndHost block.localForwards;
+ checkRemote = block: any' checkBindAndHost block.remoteForwards;
+ checkMatchBlock = block: all (fn: fn block) [ checkLocal checkRemote checkDynamic ];
+ in any' checkMatchBlock (builtins.attrValues cfg.matchBlocks);
+ message = "Forwarded paths cannot have ports.";
+ }
+ ];
+
+ home.file.".ssh/config".text = ''
+ ${concatStringsSep "\n" (
+ mapAttrsToList (n: v: "${n} ${v}") cfg.extraOptionOverrides)}
+
+ ${concatStringsSep "\n\n" (
+ map matchBlockStr (
+ builtins.attrValues cfg.matchBlocks))}
+
+ Host *
+ ForwardAgent ${yn cfg.forwardAgent}
+ Compression ${yn cfg.compression}
+ ServerAliveInterval ${toString cfg.serverAliveInterval}
+ HashKnownHosts ${yn cfg.hashKnownHosts}
+ UserKnownHostsFile ${cfg.userKnownHostsFile}
+ ControlMaster ${cfg.controlMaster}
+ ControlPath ${cfg.controlPath}
+ ControlPersist ${cfg.controlPersist}
+
+ ${replaceStrings ["\n"] ["\n "] cfg.extraConfig}
+ '';
+ };
+}
diff --git a/home-manager/modules/programs/starship.nix b/home-manager/modules/programs/starship.nix
new file mode 100644
index 00000000000..7c7819865f7
--- /dev/null
+++ b/home-manager/modules/programs/starship.nix
@@ -0,0 +1,94 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.starship;
+
+ configFile = config:
+ pkgs.runCommand "config.toml" {
+ buildInputs = [ pkgs.remarshal ];
+ preferLocalBuild = true;
+ allowSubstitutes = false;
+ } ''
+ remarshal -if json -of toml \
+ < ${pkgs.writeText "config.json" (builtins.toJSON config)} \
+ > $out
+ '';
+
+in {
+ meta.maintainers = [ maintainers.marsam ];
+
+ options.programs.starship = {
+ enable = mkEnableOption "starship";
+
+ package = mkOption {
+ type = types.package;
+ default = pkgs.starship;
+ defaultText = literalExample "pkgs.starship";
+ description = "The package to use for the starship binary.";
+ };
+
+ settings = mkOption {
+ type = types.attrs;
+ default = { };
+ description = ''
+ Configuration written to
+ <filename>~/.config/starship.toml</filename>.
+ </para><para>
+ See <link xlink:href="https://starship.rs/config/" /> for the full list
+ of options.
+ '';
+ };
+
+ enableBashIntegration = mkOption {
+ default = true;
+ type = types.bool;
+ description = ''
+ Whether to enable Bash integration.
+ '';
+ };
+
+ enableZshIntegration = mkOption {
+ default = true;
+ type = types.bool;
+ description = ''
+ Whether to enable Zsh integration.
+ '';
+ };
+
+ enableFishIntegration = mkOption {
+ default = true;
+ type = types.bool;
+ description = ''
+ Whether to enable Fish integration.
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ cfg.package ];
+
+ xdg.configFile."starship.toml" =
+ mkIf (cfg.settings != { }) { source = configFile cfg.settings; };
+
+ programs.bash.initExtra = mkIf cfg.enableBashIntegration ''
+ if [[ -z $INSIDE_EMACS ]]; then
+ eval "$(${cfg.package}/bin/starship init bash)"
+ fi
+ '';
+
+ programs.zsh.initExtra = mkIf cfg.enableZshIntegration ''
+ if [ -z "$INSIDE_EMACS" ]; then
+ eval "$(${cfg.package}/bin/starship init zsh)"
+ fi
+ '';
+
+ programs.fish.shellInit = mkIf cfg.enableFishIntegration ''
+ if test -z "$INSIDE_EMACS"
+ eval (${cfg.package}/bin/starship init fish)
+ end
+ '';
+ };
+}
diff --git a/home-manager/modules/programs/taskwarrior.nix b/home-manager/modules/programs/taskwarrior.nix
new file mode 100644
index 00000000000..cf95511f8ef
--- /dev/null
+++ b/home-manager/modules/programs/taskwarrior.nix
@@ -0,0 +1,112 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.taskwarrior;
+
+ themePath = theme: "${pkgs.taskwarrior}/share/doc/task/rc/${theme}.theme";
+
+ includeTheme = location:
+ if location == null then
+ ""
+ else if isString location then
+ "include ${themePath location}"
+ else
+ "include ${location}";
+
+ formatValue = value:
+ if isBool value then
+ if value then "true" else "false"
+ else if isList value then
+ concatMapStringsSep "," formatValue value
+ else
+ toString value;
+
+ formatLine = key: value: "${key}=${formatValue value}";
+
+ formatSet = key: values:
+ (concatStringsSep "\n"
+ (mapAttrsToList (subKey: subValue: formatPair "${key}.${subKey}" subValue)
+ values));
+
+ formatPair = key: value:
+ if isAttrs value then formatSet key value else formatLine key value;
+
+in {
+ options = {
+ programs.taskwarrior = {
+ enable = mkEnableOption "Task Warrior";
+
+ config = mkOption {
+ type = types.attrs;
+ default = { };
+ example = literalExample ''
+ {
+ confirmation = false;
+ report.minimal.filter = "status:pending";
+ report.active.columns = [ "id" "start" "entry.age" "priority" "project" "due" "description" ];
+ report.active.labels = [ "ID" "Started" "Age" "Priority" "Project" "Due" "Description" ];
+ taskd = {
+ certificate = "/path/to/cert";
+ key = "/path/to/key";
+ ca = "/path/to/ca";
+ server = "host.domain:53589";
+ credentials = "Org/First Last/cf31f287-ee9e-43a8-843e-e8bbd5de4294";
+ };
+ }
+ '';
+ description = ''
+ Key-value configuration written to
+ <filename>~/.taskrc</filename>.
+ '';
+ };
+
+ dataLocation = mkOption {
+ type = types.str;
+ default = "${config.xdg.dataHome}/task";
+ defaultText = "$XDG_DATA_HOME/task";
+ description = ''
+ Location where Task Warrior will store its data.
+ </para><para>
+ Home Manager will attempt to create this directory.
+ '';
+ };
+
+ colorTheme = mkOption {
+ type = with types; nullOr (either str path);
+ default = null;
+ example = "dark-blue-256";
+ description = ''
+ Either one of the default provided theme as string, or a
+ path to a theme configuration file.
+ '';
+ };
+
+ extraConfig = mkOption {
+ type = types.lines;
+ default = "";
+ description = ''
+ Additional content written at the end of
+ <filename>~/.taskrc</filename>.
+ '';
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ pkgs.taskwarrior ];
+
+ home.file."${cfg.dataLocation}/.keep".text = "";
+
+ home.file.".taskrc".text = ''
+ data.location=${cfg.dataLocation}
+ ${includeTheme cfg.colorTheme}
+
+ ${concatStringsSep "\n" (mapAttrsToList formatPair cfg.config)}
+
+ ${cfg.extraConfig}
+ '';
+ };
+}
diff --git a/home-manager/modules/programs/termite.nix b/home-manager/modules/programs/termite.nix
new file mode 100644
index 00000000000..8a05db03558
--- /dev/null
+++ b/home-manager/modules/programs/termite.nix
@@ -0,0 +1,387 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.termite;
+
+ vteInitStr = ''
+ # See https://github.com/thestinger/termite#id1
+ if [[ $TERM == xterm-termite ]]; then
+ . ${pkgs.termite.vte-ng}/etc/profile.d/vte.sh
+ fi
+ '';
+
+in {
+ options = {
+ programs.termite = {
+ enable = mkEnableOption "Termite VTE-based terminal";
+
+ allowBold = mkOption {
+ default = null;
+ type = types.nullOr types.bool;
+ description = ''
+ Allow the output of bold characters when the bold escape sequence appears.
+ '';
+ };
+
+ audibleBell = mkOption {
+ default = null;
+ type = types.nullOr types.bool;
+ description = "Have the terminal beep on the terminal bell.";
+ };
+
+ clickableUrl = mkOption {
+ default = null;
+ type = types.nullOr types.bool;
+ description = ''
+ Auto-detected URLs can be clicked on to open them in your browser.
+ Only enabled if a browser is configured or detected.
+ '';
+ };
+
+ dynamicTitle = mkOption {
+ default = null;
+ type = types.nullOr types.bool;
+ description = ''
+ Settings dynamic title allows the terminal and the shell to
+ update the terminal's title.
+ '';
+ };
+
+ fullscreen = mkOption {
+ default = null;
+ type = types.nullOr types.bool;
+ description = "Enables entering fullscreen mode by pressing F11.";
+ };
+
+ mouseAutohide = mkOption {
+ default = null;
+ type = types.nullOr types.bool;
+ description = ''
+ Automatically hide the mouse pointer when you start typing.
+ '';
+ };
+
+ scrollOnOutput = mkOption {
+ default = null;
+ type = types.nullOr types.bool;
+ description = "Scroll to the bottom when the shell generates output.";
+ };
+
+ scrollOnKeystroke = mkOption {
+ default = null;
+ type = types.nullOr types.bool;
+ description = ''
+ Scroll to the bottom automatically when a key is pressed.
+ '';
+ };
+
+ searchWrap = mkOption {
+ default = null;
+ type = types.nullOr types.bool;
+ description = "Search from top again when you hit the bottom.";
+ };
+
+ urgentOnBell = mkOption {
+ default = null;
+ type = types.nullOr types.bool;
+ description = "Sets the window as urgent on the terminal bell.";
+ };
+
+ font = mkOption {
+ default = null;
+ example = "Monospace 12";
+ type = types.nullOr types.str;
+ description = "The font description for the terminal's font.";
+ };
+
+ geometry = mkOption {
+ default = null;
+ example = "640x480";
+ type = types.nullOr types.str;
+ description = "The default window geometry for new terminal windows.";
+ };
+
+ iconName = mkOption {
+ default = null;
+ example = "terminal";
+ type = types.nullOr types.str;
+ description =
+ "The name of the icon to be used for the terminal process.";
+ };
+
+ scrollbackLines = mkOption {
+ default = null;
+ example = 10000;
+ type = types.nullOr types.int;
+ description =
+ "Set the number of lines to limit the terminal's scrollback.";
+ };
+
+ browser = mkOption {
+ default = null;
+ type = types.nullOr types.str;
+ example = "${pkgs.xdg_utils}/xdg-open";
+ description = ''
+ Set the default browser for opening links. If its not set, $BROWSER is read.
+ If that's not set, url hints will be disabled.
+ '';
+ };
+
+ cursorBlink = mkOption {
+ default = null;
+ example = "system";
+ type = types.nullOr (types.enum [ "system" "on" "off" ]);
+ description = ''
+ Specify the how the terminal's cursor should behave.
+ Accepts system to respect the gtk global configuration,
+ on and off to explicitly enable or disable them.
+ '';
+ };
+
+ cursorShape = mkOption {
+ default = null;
+ example = "block";
+ type = types.nullOr (types.enum [ "block" "underline" "ibeam" ]);
+ description = ''
+ Specify how the cursor should look. Accepts block, ibeam and underline.
+ '';
+ };
+
+ filterUnmatchedUrls = mkOption {
+ default = null;
+ type = types.nullOr types.bool;
+ description =
+ "Whether to hide url hints not matching input in url hints mode.";
+ };
+
+ modifyOtherKeys = mkOption {
+ default = null;
+ type = types.nullOr types.bool;
+ description = ''
+ Emit escape sequences for extra keys,
+ like the modifyOtherKeys resource for
+ <citerefentry>
+ <refentrytitle>xterm</refentrytitle>
+ <manvolnum>1</manvolnum>
+ </citerefentry>.
+ '';
+ };
+
+ sizeHints = mkOption {
+ default = null;
+ type = types.nullOr types.bool;
+ description = ''
+ Enable size hints. Locks the terminal resizing
+ to increments of the terminal's cell size.
+ Requires a window manager that respects scroll hints.
+ '';
+ };
+
+ scrollbar = mkOption {
+ default = null;
+ type = types.nullOr (types.enum [ "off" "left" "right" ]);
+ description = "Scrollbar position.";
+ };
+
+ backgroundColor = mkOption {
+ default = null;
+ example = "rgba(63, 63, 63, 0.8)";
+ type = types.nullOr types.str;
+ description = "Background color value.";
+ };
+
+ cursorColor = mkOption {
+ default = null;
+ example = "#dcdccc";
+ type = types.nullOr types.str;
+ description = "Cursor color value.";
+ };
+
+ cursorForegroundColor = mkOption {
+ default = null;
+ example = "#dcdccc";
+ type = types.nullOr types.str;
+ description = "Cursor foreground color value.";
+ };
+
+ foregroundColor = mkOption {
+ default = null;
+ example = "#dcdccc";
+ type = types.nullOr types.str;
+ description = "Foreground color value.";
+ };
+
+ foregroundBoldColor = mkOption {
+ default = null;
+ example = "#ffffff";
+ type = types.nullOr types.str;
+ description = "Foreground bold color value.";
+ };
+
+ highlightColor = mkOption {
+ default = null;
+ example = "#2f2f2f";
+ type = types.nullOr types.str;
+ description = "highlight color value.";
+ };
+
+ hintsActiveBackgroundColor = mkOption {
+ default = null;
+ example = "#3f3f3f";
+ type = types.nullOr types.str;
+ description = "Hints active background color value.";
+ };
+
+ hintsActiveForegroundColor = mkOption {
+ default = null;
+ example = "#e68080";
+ type = types.nullOr types.str;
+ description = "Hints active foreground color value.";
+ };
+
+ hintsBackgroundColor = mkOption {
+ default = null;
+ example = "#3f3f3f";
+ type = types.nullOr types.str;
+ description = "Hints background color value.";
+ };
+
+ hintsForegroundColor = mkOption {
+ default = null;
+ example = "#dcdccc";
+ type = types.nullOr types.str;
+ description = "Hints foreground color value.";
+ };
+
+ hintsBorderColor = mkOption {
+ default = null;
+ example = "#3f3f3f";
+ type = types.nullOr types.str;
+ description = "Hints border color value.";
+ };
+
+ hintsBorderWidth = mkOption {
+ default = null;
+ example = "0.5";
+ type = types.nullOr types.str;
+ description = "Hints border width.";
+ };
+
+ hintsFont = mkOption {
+ default = null;
+ example = "Monospace 12";
+ type = types.nullOr types.str;
+ description = "The font description for the hints font.";
+ };
+
+ hintsPadding = mkOption {
+ default = null;
+ example = 2;
+ type = types.nullOr types.int;
+ description = "Hints padding.";
+ };
+
+ hintsRoundness = mkOption {
+ default = null;
+ example = "0.2";
+ type = types.nullOr types.str;
+ description = "Hints roundness.";
+ };
+
+ optionsExtra = mkOption {
+ default = "";
+ example = "fullscreen = true";
+ type = types.lines;
+ description =
+ "Extra options that should be added to [options] section.";
+ };
+
+ colorsExtra = mkOption {
+ default = "";
+ example = ''
+ color0 = #3f3f3f
+ color1 = #705050
+ color2 = #60b48a
+ '';
+ type = types.lines;
+ description =
+ "Extra colors options that should be added to [colors] section.";
+ };
+
+ hintsExtra = mkOption {
+ default = "";
+ example = "border = #3f3f3f";
+ type = types.lines;
+ description =
+ "Extra hints options that should be added to [hints] section.";
+ };
+ };
+ };
+
+ config = (let
+ boolToString = v: if v then "true" else "false";
+ optionalBoolean = name: val:
+ lib.optionalString (val != null) "${name} = ${boolToString val}";
+ optionalInteger = name: val:
+ lib.optionalString (val != null) "${name} = ${toString val}";
+ optionalString = name: val:
+ lib.optionalString (val != null) "${name} = ${val}";
+ in mkIf cfg.enable {
+ home.packages = [ pkgs.termite ];
+ xdg.configFile."termite/config".text = ''
+ [options]
+ ${optionalBoolean "allow_bold" cfg.allowBold}
+ ${optionalBoolean "audible_bell" cfg.audibleBell}
+ ${optionalString "browser" cfg.browser}
+ ${optionalBoolean "clickable_url" cfg.clickableUrl}
+ ${optionalString "cursor_blink" cfg.cursorBlink}
+ ${optionalString "cursor_shape" cfg.cursorShape}
+ ${optionalBoolean "dynamic_title" cfg.dynamicTitle}
+ ${optionalBoolean "filter_unmatched_urls" cfg.filterUnmatchedUrls}
+ ${optionalString "font" cfg.font}
+ ${optionalBoolean "fullscreen" cfg.fullscreen}
+ ${optionalString "geometry" cfg.geometry}
+ ${optionalString "icon_name" cfg.iconName}
+ ${optionalBoolean "modify_other_keys" cfg.modifyOtherKeys}
+ ${optionalBoolean "mouse_autohide" cfg.mouseAutohide}
+ ${optionalBoolean "scroll_on_keystroke" cfg.scrollOnKeystroke}
+ ${optionalBoolean "scroll_on_output" cfg.scrollOnOutput}
+ ${optionalInteger "scrollback_lines" cfg.scrollbackLines}
+ ${optionalString "scrollbar" cfg.scrollbar}
+ ${optionalBoolean "search_wrap" cfg.searchWrap}
+ ${optionalBoolean "size_hints" cfg.sizeHints}
+ ${optionalBoolean "urgent_on_bell" cfg.urgentOnBell}
+
+ ${cfg.optionsExtra}
+
+ [colors]
+ ${optionalString "background" cfg.backgroundColor}
+ ${optionalString "cursor" cfg.cursorColor}
+ ${optionalString "cursor_foreground" cfg.cursorForegroundColor}
+ ${optionalString "foreground" cfg.foregroundColor}
+ ${optionalString "foregroundBold" cfg.foregroundBoldColor}
+ ${optionalString "highlight" cfg.highlightColor}
+
+ ${cfg.colorsExtra}
+
+ [hints]
+ ${optionalString "active_background" cfg.hintsActiveBackgroundColor}
+ ${optionalString "active_foreground" cfg.hintsActiveForegroundColor}
+ ${optionalString "background" cfg.hintsBackgroundColor}
+ ${optionalString "border" cfg.hintsBorderColor}
+ ${optionalInteger "border_width" cfg.hintsBorderWidth}
+ ${optionalString "font" cfg.hintsFont}
+ ${optionalString "foreground" cfg.hintsForegroundColor}
+ ${optionalInteger "padding" cfg.hintsPadding}
+ ${optionalInteger "roundness" cfg.hintsRoundness}
+
+ ${cfg.hintsExtra}
+ '';
+
+ programs.bash.initExtra = vteInitStr;
+ programs.zsh.initExtra = vteInitStr;
+ });
+}
diff --git a/home-manager/modules/programs/texlive.nix b/home-manager/modules/programs/texlive.nix
new file mode 100644
index 00000000000..08a376d654a
--- /dev/null
+++ b/home-manager/modules/programs/texlive.nix
@@ -0,0 +1,46 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.texlive;
+
+ texlivePkgs = cfg.extraPackages pkgs.texlive;
+
+in {
+ meta.maintainers = [ maintainers.rycee ];
+
+ options = {
+ programs.texlive = {
+ enable = mkEnableOption "Texlive";
+
+ extraPackages = mkOption {
+ default = tpkgs: { inherit (tpkgs) collection-basic; };
+ defaultText = "tpkgs: { inherit (tpkgs) collection-basic; }";
+ example = literalExample ''
+ tpkgs: { inherit (tpkgs) collection-fontsrecommended algorithms; }
+ '';
+ description = "Extra packages available to Texlive.";
+ };
+
+ package = mkOption {
+ type = types.package;
+ description = "Resulting customized Texlive package.";
+ readOnly = true;
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ assertions = [{
+ assertion = texlivePkgs != { };
+ message = "Must provide at least one extra package in"
+ + " 'programs.texlive.extraPackages'.";
+ }];
+
+ home.packages = [ cfg.package ];
+
+ programs.texlive.package = pkgs.texlive.combine texlivePkgs;
+ };
+}
diff --git a/home-manager/modules/programs/tmux.nix b/home-manager/modules/programs/tmux.nix
new file mode 100644
index 00000000000..766bc6238ba
--- /dev/null
+++ b/home-manager/modules/programs/tmux.nix
@@ -0,0 +1,320 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.tmux;
+
+ pluginName = p: if types.package.check p then p.name else p.plugin.name;
+
+ pluginModule = types.submodule {
+ options = {
+ plugin = mkOption {
+ type = types.package;
+ description = "Path of the configuration file to include.";
+ };
+
+ extraConfig = mkOption {
+ type = types.lines;
+ description = "Additional configuration for the associated plugin.";
+ default = "";
+ };
+ };
+ };
+
+ defaultKeyMode = "emacs";
+ defaultResize = 5;
+ defaultShortcut = "b";
+ defaultTerminal = "screen";
+
+ boolToStr = value: if value then "on" else "off";
+
+ tmuxConf = ''
+ set -g default-terminal "${cfg.terminal}"
+ set -g base-index ${toString cfg.baseIndex}
+ setw -g pane-base-index ${toString cfg.baseIndex}
+
+ ${optionalString cfg.newSession "new-session"}
+
+ ${optionalString cfg.reverseSplit ''
+ bind v split-window -h
+ bind s split-window -v
+ ''}
+
+ set -g status-keys ${cfg.keyMode}
+ set -g mode-keys ${cfg.keyMode}
+
+ ${optionalString (cfg.keyMode == "vi" && cfg.customPaneNavigationAndResize) ''
+ bind h select-pane -L
+ bind j select-pane -D
+ bind k select-pane -U
+ bind l select-pane -R
+
+ bind -r H resize-pane -L ${toString cfg.resizeAmount}
+ bind -r J resize-pane -D ${toString cfg.resizeAmount}
+ bind -r K resize-pane -U ${toString cfg.resizeAmount}
+ bind -r L resize-pane -R ${toString cfg.resizeAmount}
+ ''}
+
+ ${optionalString (cfg.shortcut != defaultShortcut) ''
+ # rebind main key: C-${cfg.shortcut}
+ unbind C-${defaultShortcut}
+ set -g prefix C-${cfg.shortcut}
+ bind ${cfg.shortcut} send-prefix
+ bind C-${cfg.shortcut} last-window
+ ''}
+
+ ${optionalString cfg.disableConfirmationPrompt ''
+ bind-key & kill-window
+ bind-key x kill-pane
+ ''}
+
+ setw -g aggressive-resize ${boolToStr cfg.aggressiveResize}
+ setw -g clock-mode-style ${if cfg.clock24 then "24" else "12"}
+ set -s escape-time ${toString cfg.escapeTime}
+ set -g history-limit ${toString cfg.historyLimit}
+
+ ${cfg.extraConfig}
+ '';
+
+in
+
+{
+ options = {
+ programs.tmux = {
+ aggressiveResize = mkOption {
+ default = false;
+ type = types.bool;
+ description = ''
+ Resize the window to the size of the smallest session for
+ which it is the current window.
+ '';
+ };
+
+ baseIndex = mkOption {
+ default = 0;
+ example = 1;
+ type = types.ints.unsigned;
+ description = "Base index for windows and panes.";
+ };
+
+ clock24 = mkOption {
+ default = false;
+ type = types.bool;
+ description = "Use 24 hour clock.";
+ };
+
+ customPaneNavigationAndResize = mkOption {
+ default = false;
+ type = types.bool;
+ description = ''
+ Override the hjkl and HJKL bindings for pane navigation and
+ resizing in VI mode.
+ '';
+ };
+
+ disableConfirmationPrompt = mkOption {
+ default = false;
+ type = types.bool;
+ description = ''
+ Disable confirmation prompt before killing a pane or window
+ '';
+ };
+
+ enable = mkEnableOption "tmux";
+
+ escapeTime = mkOption {
+ default = 500;
+ example = 0;
+ type = types.ints.unsigned;
+ description = ''
+ Time in milliseconds for which tmux waits after an escape is
+ input.
+ '';
+ };
+
+ extraConfig = mkOption {
+ type = types.lines;
+ default = "";
+ description = ''
+ Additional configuration to add to
+ <filename>tmux.conf</filename>.
+ '';
+ };
+
+ historyLimit = mkOption {
+ default = 2000;
+ example = 5000;
+ type = types.ints.positive;
+ description = "Maximum number of lines held in window history.";
+ };
+
+ keyMode = mkOption {
+ default = defaultKeyMode;
+ example = "vi";
+ type = types.enum [ "emacs" "vi" ];
+ description = "VI or Emacs style shortcuts.";
+ };
+
+ newSession = mkOption {
+ default = false;
+ type = types.bool;
+ description = ''
+ Automatically spawn a session if trying to attach and none
+ are running.
+ '';
+ };
+
+ package = mkOption {
+ type = types.package;
+ default = pkgs.tmux;
+ defaultText = literalExample "pkgs.tmux";
+ example = literalExample "pkgs.tmux";
+ description = "The tmux package to install";
+ };
+
+ reverseSplit = mkOption {
+ default = false;
+ type = types.bool;
+ description = "Reverse the window split shortcuts.";
+ };
+
+ resizeAmount = mkOption {
+ default = defaultResize;
+ example = 10;
+ type = types.ints.positive;
+ description = "Number of lines/columns when resizing.";
+ };
+
+ sensibleOnTop = mkOption {
+ type = types.bool;
+ default = true;
+ description = ''
+ Run the sensible plugin at the top of the configuration. It
+ is possible to override the sensible settings using the
+ <option>programs.tmux.extraConfig</option> option.
+ '';
+ };
+
+ shortcut = mkOption {
+ default = defaultShortcut;
+ example = "a";
+ type = types.str;
+ description = ''
+ CTRL following by this key is used as the main shortcut.
+ '';
+ };
+
+ terminal = mkOption {
+ default = defaultTerminal;
+ example = "screen-256color";
+ type = types.str;
+ description = "Set the $TERM variable.";
+ };
+
+ secureSocket = mkOption {
+ default = true;
+ type = types.bool;
+ description = ''
+ Store tmux socket under <filename>/run</filename>, which is more
+ secure than <filename>/tmp</filename>, but as a downside it doesn't
+ survive user logout.
+ '';
+ };
+
+ tmuxp.enable = mkEnableOption "tmuxp";
+
+ tmuxinator.enable = mkEnableOption "tmuxinator";
+
+ plugins = mkOption {
+ type = with types;
+ listOf (either package pluginModule)
+ // { description = "list of plugin packages or submodules"; };
+ description = ''
+ List of tmux plugins to be included at the end of your tmux
+ configuration. The sensible plugin, however, is defaulted to
+ run at the top of your configuration.
+ '';
+ default = [ ];
+ example = literalExample ''
+ with pkgs; [
+ tmuxPlugins.cpu
+ {
+ plugin = tmuxPlugins.resurrect;
+ extraConfig = "set -g @resurrect-strategy-nvim 'session'";
+ }
+ {
+ plugin = tmuxPlugins.continuum;
+ extraConfig = '''
+ set -g @continuum-restore 'on'
+ set -g @continuum-save-interval '60' # minutes
+ ''';
+ }
+ ]
+ '';
+ };
+ };
+ };
+
+ config = mkIf cfg.enable (
+ mkMerge [
+ {
+ home.packages = [ cfg.package ]
+ ++ optional cfg.tmuxinator.enable pkgs.tmuxinator
+ ++ optional cfg.tmuxp.enable pkgs.tmuxp;
+
+ home.file.".tmux.conf".text = tmuxConf;
+ }
+
+ (mkIf cfg.sensibleOnTop {
+ home.file.".tmux.conf".text = mkBefore ''
+ # ============================================= #
+ # Start with defaults from the Sensible plugin #
+ # --------------------------------------------- #
+ run-shell ${pkgs.tmuxPlugins.sensible.rtp}
+ # ============================================= #
+ '';
+ })
+
+ (mkIf cfg.secureSocket {
+ home.sessionVariables = {
+ TMUX_TMPDIR = ''''${XDG_RUNTIME_DIR:-"/run/user/\$(id -u)"}'';
+ };
+ })
+
+ (mkIf (cfg.plugins != []) {
+ assertions = [(
+ let
+ hasBadPluginName = p: !(hasPrefix "tmuxplugin" (pluginName p));
+ badPlugins = filter hasBadPluginName cfg.plugins;
+ in
+ {
+ assertion = badPlugins == [];
+ message =
+ "Invalid tmux plugin (not prefixed with \"tmuxplugins\"): "
+ + concatMapStringsSep ", " pluginName badPlugins;
+ }
+ )];
+
+ home.file.".tmux.conf".text = mkAfter ''
+ # ============================================= #
+ # Load plugins with Home Manager #
+ # --------------------------------------------- #
+
+ ${(concatMapStringsSep "\n\n" (p: ''
+ # ${pluginName p}
+ # ---------------------
+ ${p.extraConfig or ""}
+ run-shell ${
+ if types.package.check p
+ then p.rtp
+ else p.plugin.rtp
+ }
+ '') cfg.plugins)}
+ # ============================================= #
+ '';
+ })
+ ]
+ );
+}
diff --git a/home-manager/modules/programs/urxvt.nix b/home-manager/modules/programs/urxvt.nix
new file mode 100644
index 00000000000..e4c72bfe272
--- /dev/null
+++ b/home-manager/modules/programs/urxvt.nix
@@ -0,0 +1,156 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.urxvt;
+
+in {
+ options.programs.urxvt = {
+ enable = mkEnableOption "rxvt-unicode terminal emulator";
+
+ package = mkOption {
+ type = types.package;
+ default = pkgs.rxvt_unicode;
+ defaultText = literalExample "pkgs.rxvt_unicode";
+ description = "rxvt-unicode package to install.";
+ };
+
+ fonts = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ description = "List of fonts to be used.";
+ example = [ "xft:Droid Sans Mono Nerd Font:size=9" ];
+ };
+
+ keybindings = mkOption {
+ type = types.attrsOf types.str;
+ default = { };
+ description = "Mapping of keybindings to actions";
+ example = literalExample ''
+ {
+ "Shift-Control-C" = "eval:selection_to_clipboard";
+ "Shift-Control-V" = "eval:paste_clipboard";
+ }
+ '';
+ };
+
+ iso14755 = mkOption {
+ type = types.bool;
+ default = true;
+ description =
+ "ISO14755 support for viewing and entering unicode characters.";
+ };
+
+ scroll = {
+ bar = mkOption {
+ type = types.submodule {
+ options = {
+ enable = mkOption {
+ type = types.bool;
+ default = true;
+ description = "Whether to enable the scrollbar";
+ };
+
+ style = mkOption {
+ type = types.enum [ "rxvt" "plain" "next" "xterm" ];
+ default = "plain";
+ description = "Scrollbar style.";
+ };
+
+ align = mkOption {
+ type = types.enum [ "top" "bottom" "center" ];
+ default = "center";
+ description = "Scrollbar alignment.";
+ };
+
+ position = mkOption {
+ type = types.enum [ "left" "right" ];
+ default = "right";
+ description = "Scrollbar position.";
+ };
+
+ floating = mkOption {
+ type = types.bool;
+ default = true;
+ description =
+ "Whether to display an rxvt scrollbar without a trough.";
+ };
+ };
+ };
+ default = { };
+ description = "Scrollbar settings.";
+ };
+
+ lines = mkOption {
+ type = types.ints.unsigned;
+ default = 10000;
+ description = "Number of lines to save in the scrollback buffer.";
+ };
+
+ keepPosition = mkOption {
+ type = types.bool;
+ default = true;
+ description =
+ "Whether to keep a scroll position when TTY receives new lines.";
+ };
+
+ scrollOnKeystroke = mkOption {
+ type = types.bool;
+ default = true;
+ description = "Whether to scroll to bottom on keyboard input.";
+ };
+
+ scrollOnOutput = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Whether to scroll to bottom on TTY output.";
+ };
+ };
+
+ transparent = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Whether to enable pseudo-transparency.";
+ };
+
+ shading = mkOption {
+ type = types.ints.between 0 200;
+ default = 100;
+ description =
+ "Darken (0 .. 99) or lighten (101 .. 200) the transparent background.";
+ };
+
+ extraConfig = mkOption {
+ default = { };
+ type = types.attrs;
+ description = "Additional configuration to add.";
+ example = { "shading" = 15; };
+ };
+
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ cfg.package ];
+
+ xresources.properties = {
+ "URxvt.scrollBar" = cfg.scroll.bar.enable;
+ "URxvt.scrollstyle" = cfg.scroll.bar.style;
+ "URxvt.scrollBar_align" = cfg.scroll.bar.align;
+ "URxvt.scrollBar_right" = cfg.scroll.bar.position == "right";
+ "URxvt.scrollBar_floating" = cfg.scroll.bar.floating;
+ "URxvt.saveLines" = cfg.scroll.lines;
+ "URxvt.scrollWithBuffer" = cfg.scroll.keepPosition;
+ "URxvt.scrollTtyKeypress" = cfg.scroll.scrollOnKeystroke;
+ "URxvt.scrollTtyOutput" = cfg.scroll.scrollOnOutput;
+ "URxvt.transparent" = cfg.transparent;
+ "URxvt.shading" = cfg.shading;
+ "URxvt.iso14755" = cfg.iso14755;
+ } // flip mapAttrs' cfg.keybindings
+ (kb: action: nameValuePair "URxvt.keysym.${kb}" action)
+ // optionalAttrs (cfg.fonts != [ ]) {
+ "URxvt.font" = concatStringsSep "," cfg.fonts;
+ } // flip mapAttrs' cfg.extraConfig (k: v: nameValuePair "URxvt.${k}" v);
+ };
+}
diff --git a/home-manager/modules/programs/vim.nix b/home-manager/modules/programs/vim.nix
new file mode 100644
index 00000000000..39826a9a5d6
--- /dev/null
+++ b/home-manager/modules/programs/vim.nix
@@ -0,0 +1,171 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.vim;
+ defaultPlugins = [ pkgs.vimPlugins.sensible ];
+
+ knownSettings = {
+ background = types.enum [ "dark" "light" ];
+ backupdir = types.listOf types.str;
+ copyindent = types.bool;
+ directory = types.listOf types.str;
+ expandtab = types.bool;
+ hidden = types.bool;
+ history = types.int;
+ ignorecase = types.bool;
+ modeline = types.bool;
+ mouse = types.enum [ "n" "v" "i" "c" "h" "a" "r" ];
+ mousefocus = types.bool;
+ mousehide = types.bool;
+ mousemodel = types.enum [ "extend" "popup" "popup_setpos" ];
+ number = types.bool;
+ relativenumber = types.bool;
+ shiftwidth = types.int;
+ smartcase = types.bool;
+ tabstop = types.int;
+ undodir = types.listOf types.str;
+ undofile = types.bool;
+ };
+
+ vimSettingsType = types.submodule {
+ options = let
+ opt = name: type:
+ mkOption {
+ type = types.nullOr type;
+ default = null;
+ visible = false;
+ };
+ in mapAttrs opt knownSettings;
+ };
+
+ setExpr = name: value:
+ let
+ v = if isBool value then
+ (if value then "" else "no") + name
+ else
+ "${name}=${
+ if isList value then concatStringsSep "," value else toString value
+ }";
+ in optionalString (value != null) ("set " + v);
+
+ plugins = let
+ vpkgs = pkgs.vimPlugins;
+ getPkg = p:
+ if isDerivation p then
+ [ p ]
+ else
+ optional (isString p && hasAttr p vpkgs) vpkgs.${p};
+ in concatMap getPkg cfg.plugins;
+
+in {
+ options = {
+ programs.vim = {
+ enable = mkEnableOption "Vim";
+
+ plugins = mkOption {
+ type = with types; listOf (either str package);
+ default = defaultPlugins;
+ example = literalExample "[ pkgs.vimPlugins.YankRing ]";
+ description = ''
+ List of vim plugins to install. To get a list of supported plugins run:
+ <command>nix-env -f '&lt;nixpkgs&gt;' -qaP -A vimPlugins</command>.
+
+ </para><para>
+
+ Note: String values are deprecated, please use actual packages.
+ '';
+ };
+
+ settings = mkOption {
+ type = vimSettingsType;
+ default = { };
+ example = literalExample ''
+ {
+ expandtab = true;
+ history = 1000;
+ background = "dark";
+ }
+ '';
+ description = ''
+ At attribute set of Vim settings. The attribute names and
+ corresponding values must be among the following supported
+ options.
+
+ <informaltable frame="none"><tgroup cols="1"><tbody>
+ ${concatStringsSep "\n" (mapAttrsToList (n: v: ''
+ <row>
+ <entry><varname>${n}</varname></entry>
+ <entry>${v.description}</entry>
+ </row>
+ '') knownSettings)}
+ </tbody></tgroup></informaltable>
+
+ See the Vim documentation for detailed descriptions of these
+ options. Note, use <varname>extraConfig</varname> to
+ manually set any options not listed above.
+ '';
+ };
+
+ extraConfig = mkOption {
+ type = types.lines;
+ default = "";
+ example = ''
+ set nocompatible
+ set nobackup
+ '';
+ description = "Custom .vimrc lines";
+ };
+
+ package = mkOption {
+ type = types.package;
+ description = "Resulting customized vim package";
+ readOnly = true;
+ };
+ };
+ };
+
+ config = (let
+ customRC = ''
+ ${concatStringsSep "\n" (filter (v: v != "") (mapAttrsToList setExpr
+ (builtins.intersectAttrs knownSettings cfg.settings)))}
+
+ ${cfg.extraConfig}
+ '';
+
+ vim = pkgs.vim_configurable.customize {
+ name = "vim";
+ vimrcConfig = {
+ inherit customRC;
+
+ packages.home-manager.start = plugins;
+ };
+ };
+ in mkIf cfg.enable {
+ assertions = let
+ packagesNotFound =
+ filter (p: isString p && (!hasAttr p pkgs.vimPlugins)) cfg.plugins;
+ in [{
+ assertion = packagesNotFound == [ ];
+ message = "Following VIM plugin not found in pkgs.vimPlugins: ${
+ concatMapStringsSep ", " (p: ''"${p}"'') packagesNotFound
+ }";
+ }];
+
+ warnings = let stringPlugins = filter isString cfg.plugins;
+ in optional (stringPlugins != [ ]) ''
+ Specifying VIM plugins using strings is deprecated, found ${
+ concatMapStringsSep ", " (p: ''"${p}"'') stringPlugins
+ } as strings.
+ '';
+
+ home.packages = [ cfg.package ];
+
+ programs.vim = {
+ package = vim;
+ plugins = defaultPlugins;
+ };
+ });
+}
diff --git a/home-manager/modules/programs/vscode.nix b/home-manager/modules/programs/vscode.nix
new file mode 100644
index 00000000000..cf7ac722210
--- /dev/null
+++ b/home-manager/modules/programs/vscode.nix
@@ -0,0 +1,101 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.vscode;
+
+ vscodePname = cfg.package.pname;
+
+ configDir = {
+ "vscode" = "Code";
+ "vscode-insiders" = "Code - Insiders";
+ "vscodium" = "VSCodium";
+ }.${vscodePname};
+
+ extensionDir = {
+ "vscode" = "vscode";
+ "vscode-insiders" = "vscode-insiders";
+ "vscodium" = "vscode-oss";
+ }.${vscodePname};
+
+ configFilePath =
+ if pkgs.stdenv.hostPlatform.isDarwin then
+ "Library/Application Support/${configDir}/User/settings.json"
+ else
+ "${config.xdg.configHome}/${configDir}/User/settings.json";
+
+ # TODO: On Darwin where are the extensions?
+ extensionPath = ".${extensionDir}/extensions";
+in
+
+{
+ options = {
+ programs.vscode = {
+ enable = mkEnableOption "Visual Studio Code";
+
+ package = mkOption {
+ type = types.package;
+ default = pkgs.vscode;
+ example = literalExample "pkgs.vscodium";
+ description = ''
+ Version of Visual Studio Code to install.
+ '';
+ };
+
+ userSettings = mkOption {
+ type = types.attrs;
+ default = {};
+ example = literalExample ''
+ {
+ "update.channel" = "none";
+ "[nix]"."editor.tabSize" = 2;
+ }
+ '';
+ description = ''
+ Configuration written to Visual Studio Code's
+ <filename>settings.json</filename>.
+ '';
+ };
+
+ extensions = mkOption {
+ type = types.listOf types.package;
+ default = [];
+ example = literalExample "[ pkgs.vscode-extensions.bbenoist.Nix ]";
+ description = ''
+ The extensions Visual Studio Code should be started with.
+ These will override but not delete manually installed ones.
+ '';
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ cfg.package ];
+
+ # Adapted from https://discourse.nixos.org/t/vscode-extensions-setup/1801/2
+ home.file =
+ let
+ toPaths = path:
+ let
+ p = "${path}/share/vscode/extensions";
+ in
+ # Links every dir in p to the extension path.
+ mapAttrsToList (k: v:
+ {
+ "${extensionPath}/${k}".source = "${p}/${k}";
+ }) (builtins.readDir p);
+ toSymlink = concatMap toPaths cfg.extensions;
+ in
+ foldr
+ (a: b: a // b)
+ {
+ "${configFilePath}" =
+ mkIf (cfg.userSettings != {}) {
+ text = builtins.toJSON cfg.userSettings;
+ };
+ }
+ toSymlink;
+ };
+}
diff --git a/home-manager/modules/programs/vscode/haskell.nix b/home-manager/modules/programs/vscode/haskell.nix
new file mode 100644
index 00000000000..ee84e707102
--- /dev/null
+++ b/home-manager/modules/programs/vscode/haskell.nix
@@ -0,0 +1,62 @@
+{ pkgs, config, lib, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.vscode.haskell;
+
+ defaultHieNixExe = hie-nix.hies + "/bin/hie-wrapper";
+ defaultHieNixExeText =
+ literalExample ''"''${pkgs.hie-nix.hies}/bin/hie-wrapper"'';
+
+ hie-nix = pkgs.hie-nix or (abort ''
+ vscode.haskell: pkgs.hie-nix missing. Please add an overlay such as:
+ ${exampleOverlay}
+ '');
+
+ exampleOverlay = ''
+ nixpkgs.overlays = [
+ (self: super: { hie-nix = import ~/src/hie-nix {}; })
+ ]
+ '';
+
+in {
+ options.programs.vscode.haskell = {
+ enable = mkEnableOption "Haskell integration for Visual Studio Code";
+
+ hie.enable = mkOption {
+ type = types.bool;
+ default = true;
+ description = "Whether to enable Haskell IDE engine integration.";
+ };
+
+ hie.executablePath = mkOption {
+ type = types.path;
+ default = defaultHieNixExe;
+ defaultText = defaultHieNixExeText;
+ description = ''
+ The path to the Haskell IDE Engine executable.
+ </para><para>
+ Because hie-nix is not packaged in Nixpkgs, you need to add it as an
+ overlay or set this option. Example overlay configuration:
+ <programlisting language="nix">${exampleOverlay}</programlisting>
+ '';
+ example = literalExample ''
+ (import ~/src/haskell-ide-engine {}).hies + "/bin/hie-wrapper";
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+ programs.vscode.userSettings = mkIf cfg.hie.enable {
+ "languageServerHaskell.enableHIE" = true;
+ "languageServerHaskell.hieExecutablePath" = cfg.hie.executablePath;
+ };
+
+ programs.vscode.extensions =
+ [ pkgs.vscode-extensions.justusadam.language-haskell ]
+ ++ lib.optional cfg.hie.enable
+ pkgs.vscode-extensions.alanz.vscode-hie-server;
+ };
+}
diff --git a/home-manager/modules/programs/z-lua.nix b/home-manager/modules/programs/z-lua.nix
new file mode 100644
index 00000000000..d722ac6a2f0
--- /dev/null
+++ b/home-manager/modules/programs/z-lua.nix
@@ -0,0 +1,90 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.z-lua;
+
+ aliases = {
+ zz = "z -c"; # restrict matches to subdirs of $PWD
+ zi = "z -i"; # cd with interactive selection
+ zf = "z -I"; # use fzf to select in multiple matches
+ zb = "z -b"; # quickly cd to the parent directory
+ zh = "z -I -t ."; # fzf
+ };
+
+in {
+ meta.maintainers = [ maintainers.marsam ];
+
+ options.programs.z-lua = {
+ enable = mkEnableOption "z.lua";
+
+ options = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ example = [ "enhanced" "once" "fzf" ];
+ description = ''
+ List of options to pass to z.lua.
+ '';
+ };
+
+ enableBashIntegration = mkOption {
+ default = true;
+ type = types.bool;
+ description = ''
+ Whether to enable Bash integration.
+ '';
+ };
+
+ enableZshIntegration = mkOption {
+ default = true;
+ type = types.bool;
+ description = ''
+ Whether to enable Zsh integration.
+ '';
+ };
+
+ enableFishIntegration = mkOption {
+ default = true;
+ type = types.bool;
+ description = ''
+ Whether to enable Fish integration.
+ '';
+ };
+
+ enableAliases = mkOption {
+ default = false;
+ type = types.bool;
+ description = ''
+ Whether to enable recommended z.lua aliases.
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ pkgs.z-lua ];
+
+ programs.bash.initExtra = mkIf cfg.enableBashIntegration ''
+ eval "$(${pkgs.z-lua}/bin/z --init bash ${
+ concatStringsSep " " cfg.options
+ })"
+ '';
+
+ programs.zsh.initExtra = mkIf cfg.enableZshIntegration ''
+ eval "$(${pkgs.z-lua}/bin/z --init zsh ${
+ concatStringsSep " " cfg.options
+ })"
+ '';
+
+ programs.fish.shellInit = mkIf cfg.enableFishIntegration ''
+ source (${pkgs.z-lua}/bin/z --init fish ${
+ concatStringsSep " " cfg.options
+ } | psub)
+ '';
+
+ programs.bash.shellAliases = mkIf cfg.enableAliases aliases;
+
+ programs.zsh.shellAliases = mkIf cfg.enableAliases aliases;
+ };
+}
diff --git a/home-manager/modules/programs/zathura.nix b/home-manager/modules/programs/zathura.nix
new file mode 100644
index 00000000000..d9f3c1af1fd
--- /dev/null
+++ b/home-manager/modules/programs/zathura.nix
@@ -0,0 +1,58 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.zathura;
+
+ formatLine = n: v:
+ let
+ formatValue = v:
+ if isBool v then (if v then "true" else "false") else toString v;
+ in ''set ${n} "${formatValue v}"'';
+
+in {
+ meta.maintainers = [ maintainers.rprospero ];
+
+ options.programs.zathura = {
+ enable = mkEnableOption ''
+ Zathura, a highly customizable and functional document viewer
+ focused on keyboard interaction'';
+
+ options = mkOption {
+ default = { };
+ type = with types; attrsOf (either str (either bool int));
+ description = ''
+ Add <option>:set</option> command options to zathura and make
+ them permanent. See
+ <citerefentry>
+ <refentrytitle>zathurarc</refentrytitle>
+ <manvolnum>5</manvolnum>
+ </citerefentry>
+ for the full list of options.
+ '';
+ example = {
+ default-bg = "#000000";
+ default-fg = "#FFFFFF";
+ };
+ };
+
+ extraConfig = mkOption {
+ type = types.lines;
+ default = "";
+ description = ''
+ Additional commands for zathura that will be added to the
+ <filename>zathurarc</filename> file.
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ pkgs.zathura ];
+
+ xdg.configFile."zathura/zathurarc".text = concatStringsSep "\n" ([ ]
+ ++ optional (cfg.extraConfig != "") cfg.extraConfig
+ ++ mapAttrsToList formatLine cfg.options) + "\n";
+ };
+}
diff --git a/home-manager/modules/programs/zsh.nix b/home-manager/modules/programs/zsh.nix
new file mode 100644
index 00000000000..c5694e1f704
--- /dev/null
+++ b/home-manager/modules/programs/zsh.nix
@@ -0,0 +1,456 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.programs.zsh;
+
+ relToDotDir = file: (optionalString (cfg.dotDir != null) (cfg.dotDir + "/")) + file;
+
+ pluginsDir = if cfg.dotDir != null then
+ relToDotDir "plugins" else ".zsh/plugins";
+
+ envVarsStr = config.lib.zsh.exportAll cfg.sessionVariables;
+ localVarsStr = config.lib.zsh.defineAll cfg.localVariables;
+
+ aliasesStr = concatStringsSep "\n" (
+ mapAttrsToList (k: v: "alias ${k}=${lib.escapeShellArg v}") cfg.shellAliases
+ );
+
+ zdotdir = "$HOME/" + cfg.dotDir;
+
+ bindkeyCommands = {
+ emacs = "bindkey -e";
+ viins = "bindkey -v";
+ vicmd = "bindkey -a";
+ };
+
+ stateVersion = config.home.stateVersion;
+
+ historyModule = types.submodule ({ config, ... }: {
+ options = {
+ size = mkOption {
+ type = types.int;
+ default = 10000;
+ description = "Number of history lines to keep.";
+ };
+
+ save = mkOption {
+ type = types.int;
+ defaultText = 10000;
+ default = config.size;
+ description = "Number of history lines to save.";
+ };
+
+ path = mkOption {
+ type = types.str;
+ default = if versionAtLeast stateVersion "20.03"
+ then "$HOME/.zsh_history"
+ else relToDotDir ".zsh_history";
+ example = literalExample ''"''${config.xdg.dataHome}/zsh/zsh_history"'';
+ description = "History file location";
+ };
+
+ ignoreDups = mkOption {
+ type = types.bool;
+ default = true;
+ description = ''
+ Do not enter command lines into the history list
+ if they are duplicates of the previous event.
+ '';
+ };
+
+ ignoreSpace = mkOption {
+ type = types.bool;
+ default = true;
+ description = ''
+ Do not enter command lines into the history list
+ if the first character is a space.
+ '';
+ };
+
+ expireDuplicatesFirst = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Expire duplicates first.";
+ };
+
+ extended = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Save timestamp into the history file.";
+ };
+
+ share = mkOption {
+ type = types.bool;
+ default = true;
+ description = "Share command history between zsh sessions.";
+ };
+ };
+ });
+
+ pluginModule = types.submodule ({ config, ... }: {
+ options = {
+ src = mkOption {
+ type = types.path;
+ description = ''
+ Path to the plugin folder.
+
+ Will be added to <envar>fpath</envar> and <envar>PATH</envar>.
+ '';
+ };
+
+ name = mkOption {
+ type = types.str;
+ description = ''
+ The name of the plugin.
+
+ Don't forget to add <option>file</option>
+ if the script name does not follow convention.
+ '';
+ };
+
+ file = mkOption {
+ type = types.str;
+ description = "The plugin script to source.";
+ };
+ };
+
+ config.file = mkDefault "${config.name}.plugin.zsh";
+ });
+
+ ohMyZshModule = types.submodule {
+ options = {
+ enable = mkEnableOption "oh-my-zsh";
+
+ plugins = mkOption {
+ default = [];
+ example = [ "git" "sudo" ];
+ type = types.listOf types.str;
+ description = ''
+ List of oh-my-zsh plugins
+ '';
+ };
+
+ custom = mkOption {
+ default = "";
+ type = types.str;
+ example = "$HOME/my_customizations";
+ description = ''
+ Path to a custom oh-my-zsh package to override config of
+ oh-my-zsh. See <link xlink:href="https://github.com/robbyrussell/oh-my-zsh/wiki/Customization"/>
+ for more information.
+ '';
+ };
+
+ theme = mkOption {
+ default = "";
+ example = "robbyrussell";
+ type = types.str;
+ description = ''
+ Name of the theme to be used by oh-my-zsh.
+ '';
+ };
+ };
+ };
+
+in
+
+{
+ options = {
+ programs.zsh = {
+ enable = mkEnableOption "Z shell (Zsh)";
+
+ autocd = mkOption {
+ default = null;
+ description = ''
+ Automatically enter into a directory if typed directly into shell.
+ '';
+ type = types.nullOr types.bool;
+ };
+
+ dotDir = mkOption {
+ default = null;
+ example = ".config/zsh";
+ description = ''
+ Directory where the zsh configuration and more should be located,
+ relative to the users home directory. The default is the home
+ directory.
+ '';
+ type = types.nullOr types.str;
+ };
+
+ shellAliases = mkOption {
+ default = {};
+ example = { ll = "ls -l"; ".." = "cd .."; };
+ description = ''
+ An attribute set that maps aliases (the top level attribute names in
+ this option) to command strings or directly to build outputs.
+ '';
+ type = types.attrsOf types.str;
+ };
+
+ enableCompletion = mkOption {
+ default = true;
+ description = ''
+ Enable zsh completion. Don't forget to add
+ <programlisting language="nix">
+ environment.pathsToLink = [ "/share/zsh" ];
+ </programlisting>
+ to your system configuration to get completion for system packages (e.g. systemd).
+ '';
+ type = types.bool;
+ };
+
+ enableAutosuggestions = mkOption {
+ default = false;
+ description = "Enable zsh autosuggestions";
+ };
+
+ history = mkOption {
+ type = historyModule;
+ default = {};
+ description = "Options related to commands history configuration.";
+ };
+
+ defaultKeymap = mkOption {
+ type = types.nullOr (types.enum (attrNames bindkeyCommands));
+ default = null;
+ example = "emacs";
+ description = "The default base keymap to use.";
+ };
+
+ sessionVariables = mkOption {
+ default = {};
+ type = types.attrs;
+ example = { MAILCHECK = 30; };
+ description = "Environment variables that will be set for zsh session.";
+ };
+
+ initExtraBeforeCompInit = mkOption {
+ default = "";
+ type = types.lines;
+ description = "Extra commands that should be added to <filename>.zshrc</filename> before compinit.";
+ };
+
+ initExtra = mkOption {
+ default = "";
+ type = types.lines;
+ description = "Extra commands that should be added to <filename>.zshrc</filename>.";
+ };
+
+ envExtra = mkOption {
+ default = "";
+ type = types.lines;
+ description = "Extra commands that should be added to <filename>.zshenv</filename>.";
+ };
+
+ profileExtra = mkOption {
+ default = "";
+ type = types.lines;
+ description = "Extra commands that should be added to <filename>.zprofile</filename>.";
+ };
+
+ loginExtra = mkOption {
+ default = "";
+ type = types.lines;
+ description = "Extra commands that should be added to <filename>.zlogin</filename>.";
+ };
+
+ logoutExtra = mkOption {
+ default = "";
+ type = types.lines;
+ description = "Extra commands that should be added to <filename>.zlogout</filename>.";
+ };
+
+ plugins = mkOption {
+ type = types.listOf pluginModule;
+ default = [];
+ example = literalExample ''
+ [
+ {
+ # will source zsh-autosuggestions.plugin.zsh
+ name = "zsh-autosuggestions";
+ src = pkgs.fetchFromGitHub {
+ owner = "zsh-users";
+ repo = "zsh-autosuggestions";
+ rev = "v0.4.0";
+ sha256 = "0z6i9wjjklb4lvr7zjhbphibsyx51psv50gm07mbb0kj9058j6kc";
+ };
+ }
+ {
+ name = "enhancd";
+ file = "init.sh";
+ src = pkgs.fetchFromGitHub {
+ owner = "b4b4r07";
+ repo = "enhancd";
+ rev = "v2.2.1";
+ sha256 = "0iqa9j09fwm6nj5rpip87x3hnvbbz9w9ajgm6wkrd5fls8fn8i5g";
+ };
+ }
+ ]
+ '';
+ description = "Plugins to source in <filename>.zshrc</filename>.";
+ };
+
+ oh-my-zsh = mkOption {
+ type = ohMyZshModule;
+ default = {};
+ description = "Options to configure oh-my-zsh.";
+ };
+
+ localVariables = mkOption {
+ type = types.attrs;
+ default = {};
+ example = { POWERLEVEL9K_LEFT_PROMPT_ELEMENTS=["dir" "vcs"]; };
+ description = ''
+ Extra local variables defined at the top of <filename>.zshrc</filename>.
+ '';
+ };
+ };
+ };
+
+ config = mkIf cfg.enable (mkMerge [
+ (mkIf (cfg.envExtra != "") {
+ home.file."${relToDotDir ".zshenv"}".text = cfg.envExtra;
+ })
+
+ (mkIf (cfg.profileExtra != "") {
+ home.file."${relToDotDir ".zprofile"}".text = cfg.profileExtra;
+ })
+
+ (mkIf (cfg.loginExtra != "") {
+ home.file."${relToDotDir ".zlogin"}".text = cfg.loginExtra;
+ })
+
+ (mkIf (cfg.logoutExtra != "") {
+ home.file."${relToDotDir ".zlogout"}".text = cfg.logoutExtra;
+ })
+
+ (mkIf cfg.oh-my-zsh.enable {
+ home.file."${relToDotDir ".zshenv"}".text = ''
+ ZSH="${pkgs.oh-my-zsh}/share/oh-my-zsh";
+ ZSH_CACHE_DIR="${config.xdg.cacheHome}/oh-my-zsh";
+ '';
+ })
+
+ (mkIf (cfg.dotDir != null) {
+ home.file."${relToDotDir ".zshenv"}".text = ''
+ ZDOTDIR=${zdotdir}
+ '';
+
+ # When dotDir is set, only use ~/.zshenv to source ZDOTDIR/.zshenv,
+ # This is so that if ZDOTDIR happens to be
+ # already set correctly (by e.g. spawning a zsh inside a zsh), all env
+ # vars still get exported
+ home.file.".zshenv".text = ''
+ source ${zdotdir}/.zshenv
+ '';
+ })
+
+ {
+ home.packages = with pkgs; [ zsh ]
+ ++ optional cfg.enableCompletion nix-zsh-completions
+ ++ optional cfg.oh-my-zsh.enable oh-my-zsh;
+
+ home.file."${relToDotDir ".zshrc"}".text = ''
+ typeset -U path cdpath fpath manpath
+
+ for profile in ''${(z)NIX_PROFILES}; do
+ fpath+=($profile/share/zsh/site-functions $profile/share/zsh/$ZSH_VERSION/functions $profile/share/zsh/vendor-completions)
+ done
+
+ HELPDIR="${pkgs.zsh}/share/zsh/$ZSH_VERSION/help"
+
+ ${optionalString (cfg.defaultKeymap != null) ''
+ # Use ${cfg.defaultKeymap} keymap as the default.
+ ${getAttr cfg.defaultKeymap bindkeyCommands}
+ ''}
+
+ ${localVarsStr}
+
+ ${cfg.initExtraBeforeCompInit}
+
+ ${concatStrings (map (plugin: ''
+ path+="$HOME/${pluginsDir}/${plugin.name}"
+ fpath+="$HOME/${pluginsDir}/${plugin.name}"
+ '') cfg.plugins)}
+
+ # Oh-My-Zsh calls compinit during initialization,
+ # calling it twice causes sight start up slowdown
+ # as all $fpath entries will be traversed again.
+ ${optionalString (cfg.enableCompletion && !cfg.oh-my-zsh.enable)
+ "autoload -U compinit && compinit"
+ }
+
+ ${optionalString cfg.enableAutosuggestions
+ "source ${pkgs.zsh-autosuggestions}/share/zsh-autosuggestions/zsh-autosuggestions.zsh"
+ }
+
+ # Environment variables
+ . "${config.home.profileDirectory}/etc/profile.d/hm-session-vars.sh"
+ ${envVarsStr}
+
+ ${optionalString cfg.oh-my-zsh.enable ''
+ # oh-my-zsh configuration generated by NixOS
+ ${optionalString (cfg.oh-my-zsh.plugins != [])
+ "plugins=(${concatStringsSep " " cfg.oh-my-zsh.plugins})"
+ }
+ ${optionalString (cfg.oh-my-zsh.custom != "")
+ "ZSH_CUSTOM=\"${cfg.oh-my-zsh.custom}\""
+ }
+ ${optionalString (cfg.oh-my-zsh.theme != "")
+ "ZSH_THEME=\"${cfg.oh-my-zsh.theme}\""
+ }
+ source $ZSH/oh-my-zsh.sh
+ ''}
+
+ ${concatStrings (map (plugin: ''
+ if [ -f "$HOME/${pluginsDir}/${plugin.name}/${plugin.file}" ]; then
+ source "$HOME/${pluginsDir}/${plugin.name}/${plugin.file}"
+ fi
+ '') cfg.plugins)}
+
+ # History options should be set in .zshrc and after oh-my-zsh sourcing.
+ # See https://github.com/rycee/home-manager/issues/177.
+ HISTSIZE="${toString cfg.history.size}"
+ SAVEHIST="${toString cfg.history.save}"
+ ${if versionAtLeast config.home.stateVersion "20.03"
+ then ''HISTFILE="${cfg.history.path}"''
+ else ''HISTFILE="$HOME/${cfg.history.path}"''}
+ mkdir -p "$(dirname "$HISTFILE")"
+
+ setopt HIST_FCNTL_LOCK
+ ${if cfg.history.ignoreDups then "setopt" else "unsetopt"} HIST_IGNORE_DUPS
+ ${if cfg.history.ignoreSpace then "setopt" else "unsetopt"} HIST_IGNORE_SPACE
+ ${if cfg.history.expireDuplicatesFirst then "setopt" else "unsetopt"} HIST_EXPIRE_DUPS_FIRST
+ ${if cfg.history.share then "setopt" else "unsetopt"} SHARE_HISTORY
+ ${if cfg.history.extended then "setopt" else "unsetopt"} EXTENDED_HISTORY
+ ${if cfg.autocd != null then "${if cfg.autocd then "setopt" else "unsetopt"} autocd" else ""}
+
+ ${cfg.initExtra}
+
+ # Aliases
+ ${aliasesStr}
+ '';
+ }
+
+ (mkIf cfg.oh-my-zsh.enable {
+ # Make sure we create a cache directory since some plugins expect it to exist
+ # See: https://github.com/rycee/home-manager/issues/761
+ home.file."${config.xdg.cacheHome}/oh-my-zsh/.keep".text = "";
+ })
+
+ (mkIf (cfg.plugins != []) {
+ # Many plugins require compinit to be called
+ # but allow the user to opt out.
+ programs.zsh.enableCompletion = mkDefault true;
+
+ home.file =
+ foldl' (a: b: a // b) {}
+ (map (plugin: { "${pluginsDir}/${plugin.name}".source = plugin.src; })
+ cfg.plugins);
+ })
+ ]);
+}
diff --git a/home-manager/modules/services/blueman-applet.nix b/home-manager/modules/services/blueman-applet.nix
new file mode 100644
index 00000000000..5a57acccc27
--- /dev/null
+++ b/home-manager/modules/services/blueman-applet.nix
@@ -0,0 +1,36 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ options = {
+ services.blueman-applet = {
+ enable = mkEnableOption "" // {
+ description = ''
+ Whether to enable the Blueman applet.
+ </para><para>
+ Note, for the applet to work, the 'blueman' service should
+ be enabled system-wide. You can enable it in the system
+ configuration using
+ <programlisting language="nix">
+ services.blueman.enable = true;
+ </programlisting>
+ '';
+ };
+ };
+ };
+
+ config = mkIf config.services.blueman-applet.enable {
+ systemd.user.services.blueman-applet = {
+ Unit = {
+ Description = "Blueman applet";
+ After = [ "graphical-session-pre.target" ];
+ PartOf = [ "graphical-session.target" ];
+ };
+
+ Install = { WantedBy = [ "graphical-session.target" ]; };
+
+ Service = { ExecStart = "${pkgs.blueman}/bin/blueman-applet"; };
+ };
+ };
+}
diff --git a/home-manager/modules/services/cbatticon.nix b/home-manager/modules/services/cbatticon.nix
new file mode 100644
index 00000000000..0de69c5f9ec
--- /dev/null
+++ b/home-manager/modules/services/cbatticon.nix
@@ -0,0 +1,118 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.services.cbatticon;
+
+ package = pkgs.cbatticon;
+
+ makeCommand = commandName: commandArg:
+ optional (commandArg != null)
+ (let cmd = pkgs.writeShellScript commandName commandArg;
+ in "--${commandName} ${cmd}");
+
+ commandLine = concatStringsSep " " ([ "${package}/bin/cbatticon" ]
+ ++ makeCommand "command-critical-level" cfg.commandCriticalLevel
+ ++ makeCommand "command-left-click" cfg.commandLeftClick
+ ++ optional (cfg.iconType != null) "--icon-type ${cfg.iconType}"
+ ++ optional (cfg.lowLevelPercent != null)
+ "--low-level ${toString cfg.lowLevelPercent}"
+ ++ optional (cfg.criticalLevelPercent != null)
+ "--critical-level ${toString cfg.criticalLevelPercent}"
+ ++ optional (cfg.updateIntervalSeconds != null)
+ "--update-interval ${toString cfg.updateIntervalSeconds}"
+ ++ optional (cfg.hideNotification != null && cfg.hideNotification)
+ "--hide-notification");
+
+in {
+ meta.maintainers = [ maintainers.pmiddend ];
+
+ options = {
+ services.cbatticon = {
+ enable = mkEnableOption "cbatticon";
+
+ commandCriticalLevel = mkOption {
+ type = types.nullOr types.lines;
+ default = null;
+ example = ''
+ notify-send "battery critical!"
+ '';
+ description = ''
+ Command to execute when the critical battery level is reached.
+ '';
+ };
+
+ commandLeftClick = mkOption {
+ type = types.nullOr types.lines;
+ default = null;
+ description = ''
+ Command to execute when left clicking on the tray icon.
+ '';
+ };
+
+ iconType = mkOption {
+ type =
+ types.nullOr (types.enum [ "standard" "notification" "symbolic" ]);
+ default = null;
+ example = "symbolic";
+ description = "Icon type to display in the system tray.";
+ };
+
+ lowLevelPercent = mkOption {
+ type = types.nullOr (types.ints.between 0 100);
+ default = null;
+ example = 20;
+ description = ''
+ Low level percentage of the battery in percent (without the
+ percent symbol).
+ '';
+ };
+
+ criticalLevelPercent = mkOption {
+ type = types.nullOr (types.ints.between 0 100);
+ default = null;
+ example = 5;
+ description = ''
+ Critical level percentage of the battery in percent (without
+ the percent symbol).
+ '';
+ };
+
+ updateIntervalSeconds = mkOption {
+ type = types.nullOr types.ints.positive;
+ default = null;
+ example = 5;
+ description = ''
+ Number of seconds between updates of the battery information.
+ '';
+ };
+
+ hideNotification = mkOption {
+ type = types.nullOr types.bool;
+ default = null;
+ description = "Hide the notification popups.";
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ package ];
+
+ systemd.user.services.cbatticon = {
+ Unit = {
+ Description = "cbatticon system tray battery icon";
+ After = [ "graphical-session-pre.target" ];
+ PartOf = [ "graphical-session.target" ];
+ };
+
+ Install = { WantedBy = [ "graphical-session.target" ]; };
+
+ Service = {
+ ExecStart = commandLine;
+ Restart = "on-abort";
+ };
+ };
+ };
+}
diff --git a/home-manager/modules/services/compton.nix b/home-manager/modules/services/compton.nix
new file mode 100644
index 00000000000..c5b96af34da
--- /dev/null
+++ b/home-manager/modules/services/compton.nix
@@ -0,0 +1,291 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+with builtins;
+
+let
+
+ cfg = config.services.compton;
+
+ configFile = pkgs.writeText "compton.conf" (optionalString cfg.fade ''
+ # fading
+ fading = true;
+ fade-delta = ${toString cfg.fadeDelta};
+ fade-in-step = ${elemAt cfg.fadeSteps 0};
+ fade-out-step = ${elemAt cfg.fadeSteps 1};
+ fade-exclude = ${toJSON cfg.fadeExclude};
+ '' + optionalString cfg.shadow ''
+
+ # shadows
+ shadow = true;
+ shadow-offset-x = ${toString (elemAt cfg.shadowOffsets 0)};
+ shadow-offset-y = ${toString (elemAt cfg.shadowOffsets 1)};
+ shadow-opacity = ${cfg.shadowOpacity};
+ shadow-exclude = ${toJSON cfg.shadowExclude};
+ no-dock-shadow = ${toJSON cfg.noDockShadow};
+ no-dnd-shadow = ${toJSON cfg.noDNDShadow};
+ '' + optionalString cfg.blur ''
+
+ # blur
+ blur-background = true;
+ blur-background-exclude = ${toJSON cfg.blurExclude};
+ '' + ''
+
+ # opacity
+ active-opacity = ${cfg.activeOpacity};
+ inactive-opacity = ${cfg.inactiveOpacity};
+ menu-opacity = ${cfg.menuOpacity};
+ opacity-rule = ${toJSON cfg.opacityRule};
+
+ # other options
+ backend = ${toJSON cfg.backend};
+ vsync = ${toJSON cfg.vSync};
+ refresh-rate = ${toString cfg.refreshRate};
+ '' + cfg.extraOptions);
+
+in {
+
+ options.services.compton = {
+ enable = mkEnableOption "Compton X11 compositor";
+
+ blur = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Enable background blur on transparent windows.
+ '';
+ };
+
+ blurExclude = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ example = [ "class_g = 'slop'" "class_i = 'polybar'" ];
+ description = ''
+ List of windows to exclude background blur.
+ See the
+ <citerefentry>
+ <refentrytitle>compton</refentrytitle>
+ <manvolnum>1</manvolnum>
+ </citerefentry>
+ man page for more examples.
+ '';
+ };
+
+ fade = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Fade windows in and out.
+ '';
+ };
+
+ fadeDelta = mkOption {
+ type = types.int;
+ default = 10;
+ example = 5;
+ description = ''
+ Time between fade animation step (in ms).
+ '';
+ };
+
+ fadeSteps = mkOption {
+ type = types.listOf types.str;
+ default = [ "0.028" "0.03" ];
+ example = [ "0.04" "0.04" ];
+ description = ''
+ Opacity change between fade steps (in and out).
+ '';
+ };
+
+ fadeExclude = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ example = [ "window_type *= 'menu'" "name ~= 'Firefox$'" "focused = 1" ];
+ description = ''
+ List of conditions of windows that should not be faded.
+ See the
+ <citerefentry>
+ <refentrytitle>compton</refentrytitle>
+ <manvolnum>1</manvolnum>
+ </citerefentry>
+ man page for more examples.
+ '';
+ };
+
+ shadow = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Draw window shadows.
+ '';
+ };
+
+ shadowOffsets = mkOption {
+ type = types.listOf types.int;
+ default = [ (-15) (-15) ];
+ example = [ (-10) (-15) ];
+ description = ''
+ Horizontal and vertical offsets for shadows (in pixels).
+ '';
+ };
+
+ shadowOpacity = mkOption {
+ type = types.str;
+ default = "0.75";
+ example = "0.8";
+ description = ''
+ Window shadows opacity (number in range 0 - 1).
+ '';
+ };
+
+ shadowExclude = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ example = [ "window_type *= 'menu'" "name ~= 'Firefox$'" "focused = 1" ];
+ description = ''
+ List of conditions of windows that should have no shadow.
+ See the
+ <citerefentry>
+ <refentrytitle>compton</refentrytitle>
+ <manvolnum>1</manvolnum>
+ </citerefentry>
+ man page for more examples.
+ '';
+ };
+
+ noDockShadow = mkOption {
+ type = types.bool;
+ default = true;
+ description = ''
+ Avoid shadow on docks.
+ '';
+ };
+
+ noDNDShadow = mkOption {
+ type = types.bool;
+ default = true;
+ description = ''
+ Avoid shadow on drag-and-drop windows.
+ '';
+ };
+
+ activeOpacity = mkOption {
+ type = types.str;
+ default = "1.0";
+ example = "0.8";
+ description = ''
+ Opacity of active windows.
+ '';
+ };
+
+ inactiveOpacity = mkOption {
+ type = types.str;
+ default = "1.0";
+ example = "0.8";
+ description = ''
+ Opacity of inactive windows.
+ '';
+ };
+
+ menuOpacity = mkOption {
+ type = types.str;
+ default = "1.0";
+ example = "0.8";
+ description = ''
+ Opacity of dropdown and popup menu.
+ '';
+ };
+
+ opacityRule = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ example = [ "87:class_i ?= 'scratchpad'" "91:class_i ?= 'xterm'" ];
+ description = ''
+ List of opacity rules.
+ See the
+ <citerefentry>
+ <refentrytitle>compton</refentrytitle>
+ <manvolnum>1</manvolnum>
+ </citerefentry>
+ man page for more examples.
+ '';
+ };
+
+ backend = mkOption {
+ type = types.str;
+ default = "glx";
+ description = ''
+ Backend to use: <literal>glx</literal> or <literal>xrender</literal>.
+ '';
+ };
+
+ vSync = mkOption {
+ type = types.str;
+ default = "none";
+ example = "opengl-swc";
+ description = ''
+ Enable vertical synchronization using the specified method.
+ See the
+ <citerefentry>
+ <refentrytitle>compton</refentrytitle>
+ <manvolnum>1</manvolnum>
+ </citerefentry>
+ man page for available methods.
+ '';
+ };
+
+ refreshRate = mkOption {
+ type = types.int;
+ default = 0;
+ example = 60;
+ description = ''
+ Screen refresh rate (0 = automatically detect).
+ '';
+ };
+
+ package = mkOption {
+ type = types.package;
+ default = pkgs.compton;
+ defaultText = literalExample "pkgs.compton";
+ example = literalExample "pkgs.compton";
+ description = ''
+ Compton derivation to use.
+ '';
+ };
+
+ extraOptions = mkOption {
+ type = types.str;
+ default = "";
+ example = ''
+ unredir-if-possible = true;
+ dbe = true;
+ '';
+ description = ''
+ Additional Compton configuration.
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ cfg.package ];
+
+ systemd.user.services.compton = {
+ Unit = {
+ Description = "Compton X11 compositor";
+ After = [ "graphical-session-pre.target" ];
+ PartOf = [ "graphical-session.target" ];
+ };
+
+ Install = { WantedBy = [ "graphical-session.target" ]; };
+
+ Service = {
+ ExecStart = "${cfg.package}/bin/compton --config ${configFile}";
+ Restart = "always";
+ RestartSec = 3;
+ } // optionalAttrs (cfg.backend == "glx") {
+ # Temporarily fixes corrupt colours with Mesa 18.
+ Environment = [ "allow_rgb10_configs=false" ];
+ };
+ };
+ };
+}
diff --git a/home-manager/modules/services/dunst.nix b/home-manager/modules/services/dunst.nix
new file mode 100644
index 00000000000..d32e875137b
--- /dev/null
+++ b/home-manager/modules/services/dunst.nix
@@ -0,0 +1,159 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.services.dunst;
+
+ eitherStrBoolIntList = with types;
+ either str (either bool (either int (listOf str)));
+
+ toDunstIni = generators.toINI {
+ mkKeyValue = key: value:
+ let
+ value' = if isBool value then
+ (if value then "yes" else "no")
+ else if isString value then
+ ''"${value}"''
+ else
+ toString value;
+ in "${key}=${value'}";
+ };
+
+ themeType = types.submodule {
+ options = {
+ package = mkOption {
+ type = types.package;
+ example = literalExample "pkgs.gnome3.adwaita-icon-theme";
+ description = "Package providing the theme.";
+ };
+
+ name = mkOption {
+ type = types.str;
+ example = "Adwaita";
+ description = "The name of the theme within the package.";
+ };
+
+ size = mkOption {
+ type = types.str;
+ default = "32x32";
+ example = "16x16";
+ description = "The desired icon size.";
+ };
+ };
+ };
+
+ hicolorTheme = {
+ package = pkgs.hicolor_icon_theme;
+ name = "hicolor";
+ size = "32x32";
+ };
+
+in {
+ meta.maintainers = [ maintainers.rycee ];
+
+ options = {
+ services.dunst = {
+ enable = mkEnableOption "the dunst notification daemon";
+
+ iconTheme = mkOption {
+ type = themeType;
+ default = hicolorTheme;
+ description = "Set the icon theme.";
+ };
+
+ settings = mkOption {
+ type = with types; attrsOf (attrsOf eitherStrBoolIntList);
+ default = { };
+ description = "Configuration written to ~/.config/dunstrc";
+ example = literalExample ''
+ {
+ global = {
+ geometry = "300x5-30+50";
+ transparency = 10;
+ frame_color = "#eceff1";
+ font = "Droid Sans 9";
+ };
+
+ urgency_normal = {
+ background = "#37474f";
+ foreground = "#eceff1";
+ timeout = 10;
+ };
+ };
+ '';
+ };
+ };
+ };
+
+ config = mkIf cfg.enable (mkMerge [
+ {
+ xdg.dataFile."dbus-1/services/org.knopwob.dunst.service".source =
+ "${pkgs.dunst}/share/dbus-1/services/org.knopwob.dunst.service";
+
+ services.dunst.settings.global.icon_path = let
+ useCustomTheme = cfg.iconTheme.package != hicolorTheme.package
+ || cfg.iconTheme.name != hicolorTheme.name || cfg.iconTheme.size
+ != hicolorTheme.size;
+
+ basePaths = [
+ "/run/current-system/sw"
+ config.home.profileDirectory
+ cfg.iconTheme.package
+ ] ++ optional useCustomTheme hicolorTheme.package;
+
+ themes = [ cfg.iconTheme ] ++ optional useCustomTheme
+ (hicolorTheme // { size = cfg.iconTheme.size; });
+
+ categories = [
+ "actions"
+ "animations"
+ "apps"
+ "categories"
+ "devices"
+ "emblems"
+ "emotes"
+ "filesystem"
+ "intl"
+ "mimetypes"
+ "places"
+ "status"
+ "stock"
+ ];
+ in concatStringsSep ":" (concatMap (theme:
+ concatMap (basePath:
+ map (category:
+ "${basePath}/share/icons/${theme.name}/${theme.size}/${category}")
+ categories) basePaths) themes);
+
+ systemd.user.services.dunst = {
+ Unit = {
+ Description = "Dunst notification daemon";
+ After = [ "graphical-session-pre.target" ];
+ PartOf = [ "graphical-session.target" ];
+ };
+
+ Service = {
+ Type = "dbus";
+ BusName = "org.freedesktop.Notifications";
+ ExecStart = "${pkgs.dunst}/bin/dunst";
+ };
+ };
+ }
+
+ (mkIf (cfg.settings != { }) {
+ xdg.configFile."dunst/dunstrc" = {
+ text = toDunstIni cfg.settings;
+ onChange = ''
+ pkillVerbose=""
+ if [[ -v VERBOSE ]]; then
+ pkillVerbose="-e"
+ fi
+ $DRY_RUN_CMD ${pkgs.procps}/bin/pkill -u $USER $pkillVerbose dunst || true
+ unset pkillVerbose
+ '';
+ };
+ })
+ ]);
+}
diff --git a/home-manager/modules/services/dwm-status.nix b/home-manager/modules/services/dwm-status.nix
new file mode 100644
index 00000000000..7a19e5e5fc9
--- /dev/null
+++ b/home-manager/modules/services/dwm-status.nix
@@ -0,0 +1,65 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+ cfg = config.services.dwm-status;
+
+ features = [ "audio" "backlight" "battery" "cpu_load" "network" "time" ];
+
+ configText = builtins.toJSON ({ inherit (cfg) order; } // cfg.extraConfig);
+
+ configFile = pkgs.writeText "dwm-status.json" configText;
+
+in {
+ options = {
+ services.dwm-status = {
+ enable = mkEnableOption "dwm-status user service";
+
+ package = mkOption {
+ type = types.package;
+ default = pkgs.dwm-status;
+ defaultText = literalExample "pkgs.dwm-status";
+ example = "pkgs.dwm-status.override { enableAlsaUtils = false; }";
+ description = "Which dwm-status package to use.";
+ };
+
+ order = mkOption {
+ type = types.listOf (types.enum features);
+ description = "List of enabled features in order.";
+ };
+
+ extraConfig = mkOption {
+ type = types.attrs;
+ default = { };
+ example = literalExample ''
+ {
+ separator = "#";
+
+ battery = {
+ notifier_levels = [ 2 5 10 15 20 ];
+ };
+
+ time = {
+ format = "%H:%M";
+ };
+ }
+ '';
+ description = "Extra config of dwm-status.";
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ systemd.user.services.dwm-status = {
+ Unit = {
+ Description = "DWM status service";
+ PartOf = [ "graphical-session.target" ];
+ };
+
+ Install = { WantedBy = [ "graphical-session.target" ]; };
+
+ Service = { ExecStart = "${cfg.package}/bin/dwm-status ${configFile}"; };
+ };
+ };
+}
diff --git a/home-manager/modules/services/emacs.nix b/home-manager/modules/services/emacs.nix
new file mode 100644
index 00000000000..5b0e88db72d
--- /dev/null
+++ b/home-manager/modules/services/emacs.nix
@@ -0,0 +1,42 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.services.emacs;
+ emacsCfg = config.programs.emacs;
+ emacsBinPath = "${emacsCfg.finalPackage}/bin";
+
+in {
+ options.services.emacs = { enable = mkEnableOption "the Emacs daemon"; };
+
+ config = mkIf cfg.enable {
+ assertions = [{
+ assertion = emacsCfg.enable;
+ message = "The Emacs service module requires"
+ + " 'programs.emacs.enable = true'.";
+ }];
+
+ systemd.user.services.emacs = {
+ Unit = {
+ Description = "Emacs: the extensible, self-documenting text editor";
+ Documentation =
+ "info:emacs man:emacs(1) https://gnu.org/software/emacs/";
+
+ # Avoid killing the Emacs session, which may be full of
+ # unsaved buffers.
+ X-RestartIfChanged = false;
+ };
+
+ Service = {
+ ExecStart =
+ "${pkgs.runtimeShell} -l -c 'exec ${emacsBinPath}/emacs --fg-daemon'";
+ ExecStop = "${emacsBinPath}/emacsclient --eval '(kill-emacs)'";
+ Restart = "on-failure";
+ };
+
+ Install = { WantedBy = [ "default.target" ]; };
+ };
+ };
+}
diff --git a/home-manager/modules/services/flameshot.nix b/home-manager/modules/services/flameshot.nix
new file mode 100644
index 00000000000..c8659d51d1e
--- /dev/null
+++ b/home-manager/modules/services/flameshot.nix
@@ -0,0 +1,39 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.services.flameshot;
+ package = pkgs.flameshot;
+
+in {
+ meta.maintainers = [ maintainers.hamhut1066 ];
+
+ options = { services.flameshot = { enable = mkEnableOption "Flameshot"; }; };
+
+ config = mkIf cfg.enable {
+ home.packages = [ package ];
+
+ systemd.user.services.flameshot = {
+ Unit = {
+ Description = "Flameshot screenshot tool";
+ After = [
+ "graphical-session-pre.target"
+ "polybar.service"
+ "stalonetray.service"
+ "taffybar.service"
+ ];
+ PartOf = [ "graphical-session.target" ];
+ };
+
+ Install = { WantedBy = [ "graphical-session.target" ]; };
+
+ Service = {
+ Environment = "PATH=${config.home.profileDirectory}/bin";
+ ExecStart = "${package}/bin/flameshot";
+ Restart = "on-abort";
+ };
+ };
+ };
+}
diff --git a/home-manager/modules/services/getmail.nix b/home-manager/modules/services/getmail.nix
new file mode 100644
index 00000000000..e7a1b1a4627
--- /dev/null
+++ b/home-manager/modules/services/getmail.nix
@@ -0,0 +1,55 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.services.getmail;
+
+ accounts =
+ filter (a: a.getmail.enable) (attrValues config.accounts.email.accounts);
+
+ # Note: The getmail service does not expect a path, but just the filename!
+ renderConfigFilepath = a:
+ if a.primary then "getmailrc" else "getmail${a.name}";
+ configFiles =
+ concatMapStringsSep " " (a: " --rcfile ${renderConfigFilepath a}") accounts;
+in {
+ options = {
+ services.getmail = {
+ enable = mkEnableOption
+ "the getmail systemd service to automatically retrieve mail";
+
+ frequency = mkOption {
+ type = types.str;
+ default = "*:0/5";
+ example = "hourly";
+ description = ''
+ The refresh frequency. Check <literal>man systemd.time</literal> for
+ more information on the syntax. If you use a gpg-agent in
+ combination with the passwordCommand, keep the poll
+ frequency below the cache-ttl value (as set by the
+ <literal>default</literal>) to avoid pinentry asking
+ permanently for a password.
+ '';
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ systemd.user.services.getmail = {
+ Unit = { Description = "getmail email fetcher"; };
+ Service = { ExecStart = "${pkgs.getmail}/bin/getmail ${configFiles}"; };
+ };
+
+ systemd.user.timers.getmail = {
+ Unit = { Description = "getmail email fetcher"; };
+ Timer = {
+ OnCalendar = "${cfg.frequency}";
+ Unit = "getmail.service";
+ };
+ Install = { WantedBy = [ "timers.target" ]; };
+ };
+
+ };
+}
diff --git a/home-manager/modules/services/gnome-keyring.nix b/home-manager/modules/services/gnome-keyring.nix
new file mode 100644
index 00000000000..6d8317dcffc
--- /dev/null
+++ b/home-manager/modules/services/gnome-keyring.nix
@@ -0,0 +1,46 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.services.gnome-keyring;
+
+in {
+ meta.maintainers = [ maintainers.rycee ];
+
+ options = {
+ services.gnome-keyring = {
+ enable = mkEnableOption "GNOME Keyring";
+
+ components = mkOption {
+ type = types.listOf (types.enum [ "pkcs11" "secrets" "ssh" ]);
+ default = [ ];
+ description = ''
+ The GNOME keyring components to start. If empty then the
+ default set of components will be started.
+ '';
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ systemd.user.services.gnome-keyring = {
+ Unit = {
+ Description = "GNOME Keyring";
+ PartOf = [ "graphical-session-pre.target" ];
+ };
+
+ Service = {
+ ExecStart = let
+ args = concatStringsSep " " ([ "--start" "--foreground" ]
+ ++ optional (cfg.components != [ ])
+ ("--components=" + concatStringsSep "," cfg.components));
+ in "${pkgs.gnome3.gnome_keyring}/bin/gnome-keyring-daemon ${args}";
+ Restart = "on-abort";
+ };
+
+ Install = { WantedBy = [ "graphical-session-pre.target" ]; };
+ };
+ };
+}
diff --git a/home-manager/modules/services/gpg-agent.nix b/home-manager/modules/services/gpg-agent.nix
new file mode 100644
index 00000000000..16a4723fea7
--- /dev/null
+++ b/home-manager/modules/services/gpg-agent.nix
@@ -0,0 +1,281 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.services.gpg-agent;
+
+ gpgInitStr = ''
+ GPG_TTY="$(tty)"
+ export GPG_TTY
+ ''
+ + optionalString cfg.enableSshSupport
+ "${pkgs.gnupg}/bin/gpg-connect-agent updatestartuptty /bye > /dev/null";
+
+in
+
+{
+ meta.maintainers = [ maintainers.rycee ];
+
+ options = {
+ services.gpg-agent = {
+ enable = mkEnableOption "GnuPG private key agent";
+
+ defaultCacheTtl = mkOption {
+ type = types.nullOr types.int;
+ default = null;
+ description = ''
+ Set the time a cache entry is valid to the given number of
+ seconds.
+ '';
+ };
+
+ defaultCacheTtlSsh = mkOption {
+ type = types.nullOr types.int;
+ default = null;
+ description = ''
+ Set the time a cache entry used for SSH keys is valid to the
+ given number of seconds.
+ '';
+ };
+
+ maxCacheTtl = mkOption {
+ type = types.nullOr types.int;
+ default = null;
+ description = ''
+ Set the maximum time a cache entry is valid to n seconds. After this
+ time a cache entry will be expired even if it has been accessed
+ recently or has been set using gpg-preset-passphrase. The default is
+ 2 hours (7200 seconds).
+ '';
+ };
+
+ maxCacheTtlSsh = mkOption {
+ type = types.nullOr types.int;
+ default = null;
+ description = ''
+ Set the maximum time a cache entry used for SSH keys is valid to n
+ seconds. After this time a cache entry will be expired even if it has
+ been accessed recently or has been set using gpg-preset-passphrase.
+ The default is 2 hours (7200 seconds).
+ '';
+ };
+
+ enableSshSupport = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Whether to use the GnuPG key agent for SSH keys.
+ '';
+ };
+
+ sshKeys = mkOption {
+ type = types.nullOr (types.listOf types.str);
+ default = null;
+ description = ''
+ Which GPG keys (by keygrip) to expose as SSH keys.
+ '';
+ };
+
+ enableExtraSocket = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Whether to enable extra socket of the GnuPG key agent (useful for GPG
+ Agent forwarding).
+ '';
+ };
+
+ verbose = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Whether to produce verbose output.
+ '';
+ };
+
+ grabKeyboardAndMouse = mkOption {
+ type = types.bool;
+ default = true;
+ description = ''
+ Tell the pinentry to grab the keyboard and mouse. This
+ option should in general be used to avoid X-sniffing
+ attacks. When disabled, this option passes
+ <option>no-grab</option> setting to gpg-agent.
+ '';
+ };
+
+ enableScDaemon = mkOption {
+ type = types.bool;
+ default = true;
+ description = ''
+ Make use of the scdaemon tool. This option has the effect of
+ enabling the ability to do smartcard operations. When
+ disabled, this option passes
+ <option>disable-scdaemon</option> setting to gpg-agent.
+ '';
+ };
+
+ extraConfig = mkOption {
+ type = types.lines;
+ default = "";
+ example = ''
+ allow-emacs-pinentry
+ allow-loopback-pinentry
+ '';
+ description = ''
+ Extra configuration lines to append to the gpg-agent
+ configuration file.
+ '';
+ };
+
+ pinentryFlavor = mkOption {
+ type = types.nullOr (types.enum pkgs.pinentry.flavors);
+ example = "gnome3";
+ default = "gtk2";
+ description = ''
+ Which pinentry interface to use. If not
+ <literal>null</literal>, it sets
+ <option>pinentry-program</option> in
+ <filename>gpg-agent.conf</filename>. Beware that
+ <literal>pinentry-gnome3</literal> may not work on non-Gnome
+ systems. You can fix it by adding the following to your
+ system configuration:
+ <programlisting language="nix">
+ services.dbus.packages = [ pkgs.gcr ];
+ </programlisting>
+ For this reason, the default is <literal>gtk2</literal> for
+ now.
+ '';
+ };
+ };
+ };
+
+ config = mkIf cfg.enable (mkMerge [
+ {
+ home.file.".gnupg/gpg-agent.conf".text = concatStringsSep "\n" (
+ optional (cfg.enableSshSupport) "enable-ssh-support"
+ ++
+ optional (!cfg.grabKeyboardAndMouse) "no-grab"
+ ++
+ optional (!cfg.enableScDaemon) "disable-scdaemon"
+ ++
+ optional (cfg.defaultCacheTtl != null)
+ "default-cache-ttl ${toString cfg.defaultCacheTtl}"
+ ++
+ optional (cfg.defaultCacheTtlSsh != null)
+ "default-cache-ttl-ssh ${toString cfg.defaultCacheTtlSsh}"
+ ++
+ optional (cfg.maxCacheTtl != null)
+ "max-cache-ttl ${toString cfg.maxCacheTtl}"
+ ++
+ optional (cfg.maxCacheTtlSsh != null)
+ "max-cache-ttl-ssh ${toString cfg.maxCacheTtlSsh}"
+ ++
+ optional (cfg.pinentryFlavor != null)
+ "pinentry-program ${pkgs.pinentry.${cfg.pinentryFlavor}}/bin/pinentry"
+ ++
+ [ cfg.extraConfig ]
+ );
+
+ home.sessionVariables =
+ optionalAttrs cfg.enableSshSupport {
+ SSH_AUTH_SOCK = "$(${pkgs.gnupg}/bin/gpgconf --list-dirs agent-ssh-socket)";
+ };
+
+ programs.bash.initExtra = gpgInitStr;
+ programs.zsh.initExtra = gpgInitStr;
+ }
+
+ (mkIf (cfg.sshKeys != null) {
+ # Trailing newlines are important
+ home.file.".gnupg/sshcontrol".text = concatMapStrings (s: "${s}\n") cfg.sshKeys;
+ })
+
+ # The systemd units below are direct translations of the
+ # descriptions in the
+ #
+ # ${pkgs.gnupg}/share/doc/gnupg/examples/systemd-user
+ #
+ # directory.
+ {
+ systemd.user.services.gpg-agent = {
+ Unit = {
+ Description = "GnuPG cryptographic agent and passphrase cache";
+ Documentation = "man:gpg-agent(1)";
+ Requires = "gpg-agent.socket";
+ After = "gpg-agent.socket";
+ # This is a socket-activated service:
+ RefuseManualStart = true;
+ };
+
+ Service = {
+ ExecStart = "${pkgs.gnupg}/bin/gpg-agent --supervised"
+ + optionalString cfg.verbose " --verbose";
+ ExecReload = "${pkgs.gnupg}/bin/gpgconf --reload gpg-agent";
+ };
+ };
+
+ systemd.user.sockets.gpg-agent = {
+ Unit = {
+ Description = "GnuPG cryptographic agent and passphrase cache";
+ Documentation = "man:gpg-agent(1)";
+ };
+
+ Socket = {
+ ListenStream = "%t/gnupg/S.gpg-agent";
+ FileDescriptorName = "std";
+ SocketMode = "0600";
+ DirectoryMode = "0700";
+ };
+
+ Install = {
+ WantedBy = [ "sockets.target" ];
+ };
+ };
+ }
+
+ (mkIf cfg.enableSshSupport {
+ systemd.user.sockets.gpg-agent-ssh = {
+ Unit = {
+ Description = "GnuPG cryptographic agent (ssh-agent emulation)";
+ Documentation = "man:gpg-agent(1) man:ssh-add(1) man:ssh-agent(1) man:ssh(1)";
+ };
+
+ Socket = {
+ ListenStream = "%t/gnupg/S.gpg-agent.ssh";
+ FileDescriptorName = "ssh";
+ Service = "gpg-agent.service";
+ SocketMode = "0600";
+ DirectoryMode = "0700";
+ };
+
+ Install = {
+ WantedBy = [ "sockets.target" ];
+ };
+ };
+ })
+
+ (mkIf cfg.enableExtraSocket {
+ systemd.user.sockets.gpg-agent-extra = {
+ Unit = {
+ Description = "GnuPG cryptographic agent and passphrase cache (restricted)";
+ Documentation = "man:gpg-agent(1) man:ssh(1)";
+ };
+
+ Socket = {
+ ListenStream = "%t/gnupg/S.gpg-agent.extra";
+ FileDescriptorName = "extra";
+ Service = "gpg-agent.service";
+ SocketMode = "0600";
+ DirectoryMode = "0700";
+ };
+
+ Install = {
+ WantedBy = [ "sockets.target" ];
+ };
+ };
+ })
+ ]);
+}
diff --git a/home-manager/modules/services/grobi.nix b/home-manager/modules/services/grobi.nix
new file mode 100644
index 00000000000..4dfc5d6331f
--- /dev/null
+++ b/home-manager/modules/services/grobi.nix
@@ -0,0 +1,97 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.services.grobi;
+
+ eitherStrBoolIntList = with types;
+ either str (either bool (either int (listOf str)));
+
+in {
+ meta.maintainers = [ maintainers.mbrgm ];
+
+ options = {
+ services.grobi = {
+ enable = mkEnableOption "the grobi display setup daemon";
+
+ executeAfter = mkOption {
+ type = with types; listOf str;
+ default = [ ];
+ example = [ "setxkbmap dvorak" ];
+ description = ''
+ Commands to be run after an output configuration was
+ changed. The Nix value declared here will be translated to
+ JSON and written to the <option>execute_after</option> key
+ in <filename>~/.config/grobi.conf</filename>.
+ '';
+ };
+
+ rules = mkOption {
+ type = with types; listOf (attrsOf eitherStrBoolIntList);
+ default = [ ];
+ example = literalExample ''
+ [
+ {
+ name = "Home";
+ outputs_connected = [ "DP-2" ];
+ configure_single = "DP-2";
+ primary = true;
+ atomic = true;
+ execute_after = [
+ "${pkgs.xorg.xrandr}/bin/xrandr --dpi 96"
+ "${pkgs.xmonad-with-packages}/bin/xmonad --restart";
+ ];
+ }
+ {
+ name = "Mobile";
+ outputs_disconnected = [ "DP-2" ];
+ configure_single = "eDP-1";
+ primary = true;
+ atomic = true;
+ execute_after = [
+ "${pkgs.xorg.xrandr}/bin/xrandr --dpi 120"
+ "${pkgs.xmonad-with-packages}/bin/xmonad --restart";
+ ];
+ }
+ ]
+ '';
+ description = ''
+ These are the rules grobi tries to match to the current
+ output configuration. The rules are evaluated top to bottom,
+ the first matching rule is applied and processing stops. See
+ <link xlink:href="https://github.com/fd0/grobi/blob/master/doc/grobi.conf"/>
+ for more information. The Nix value declared here will be
+ translated to JSON and written to the <option>rules</option>
+ key in <filename>~/.config/grobi.conf</filename>.
+ '';
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ systemd.user.services.grobi = {
+ Unit = {
+ Description = "grobi display auto config daemon";
+ After = [ "graphical-session-pre.target" ];
+ PartOf = [ "graphical-session.target" ];
+ };
+
+ Service = {
+ Type = "simple";
+ ExecStart = "${pkgs.grobi}/bin/grobi watch -v";
+ Restart = "always";
+ RestartSec = "2s";
+ Environment = "PATH=${pkgs.xorg.xrandr}/bin:${pkgs.bash}/bin";
+ };
+
+ Install = { WantedBy = [ "graphical-session.target" ]; };
+ };
+
+ xdg.configFile."grobi.conf".text = builtins.toJSON {
+ execute_after = cfg.executeAfter;
+ rules = cfg.rules;
+ };
+ };
+}
diff --git a/home-manager/modules/services/hound.nix b/home-manager/modules/services/hound.nix
new file mode 100644
index 00000000000..00589f3405f
--- /dev/null
+++ b/home-manager/modules/services/hound.nix
@@ -0,0 +1,74 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.services.hound;
+
+ configFile = pkgs.writeText "hound-config.json" (builtins.toJSON {
+ max-concurrent-indexers = cfg.maxConcurrentIndexers;
+ dbpath = cfg.databasePath;
+ repos = cfg.repositories;
+ health-check-url = "/healthz";
+ });
+
+ houndOptions = [ "--addr ${cfg.listenAddress}" "--conf ${configFile}" ];
+
+in {
+ meta.maintainers = [ maintainers.adisbladis ];
+
+ options.services.hound = {
+ enable = mkEnableOption "hound";
+
+ maxConcurrentIndexers = mkOption {
+ type = types.ints.positive;
+ default = 2;
+ description = "Limit the amount of concurrent indexers.";
+ };
+
+ databasePath = mkOption {
+ type = types.path;
+ default = "${config.xdg.dataHome}/hound";
+ defaultText = "$XDG_DATA_HOME/hound";
+ description = "The Hound database path.";
+ };
+
+ listenAddress = mkOption {
+ type = types.str;
+ default = "localhost:6080";
+ description = "Listen address of the Hound daemon.";
+ };
+
+ repositories = mkOption {
+ type = types.attrsOf (types.uniq types.attrs);
+ default = { };
+ example = literalExample ''
+ {
+ SomeGitRepo = {
+ url = "https://www.github.com/YourOrganization/RepoOne.git";
+ ms-between-poll = 10000;
+ exclude-dot-files = true;
+ };
+ }
+ '';
+ description = "The repository configuration.";
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ pkgs.hound ];
+
+ systemd.user.services.hound = {
+ Unit = { Description = "Hound source code search engine"; };
+
+ Install = { WantedBy = [ "default.target" ]; };
+
+ Service = {
+ Environment = "PATH=${makeBinPath [ pkgs.mercurial pkgs.git ]}";
+ ExecStart =
+ "${pkgs.hound}/bin/houndd ${concatStringsSep " " houndOptions}";
+ };
+ };
+ };
+}
diff --git a/home-manager/modules/services/imapnotify-accounts.nix b/home-manager/modules/services/imapnotify-accounts.nix
new file mode 100644
index 00000000000..94bdce5dfb4
--- /dev/null
+++ b/home-manager/modules/services/imapnotify-accounts.nix
@@ -0,0 +1,33 @@
+{ lib, ... }:
+
+with lib;
+
+{
+ options.imapnotify = {
+ enable = mkEnableOption "imapnotify";
+
+ onNotify = mkOption {
+ type = with types; either str (attrsOf str);
+ default = "";
+ example = "\${pkgs.isync}/bin/mbsync test-%s";
+ description = "Shell commands to run on any event.";
+ };
+
+ onNotifyPost = mkOption {
+ type = with types; either str (attrsOf str);
+ default = "";
+ example = {
+ mail =
+ "\${pkgs.notmuch}/bin/notmuch new && \${pkgs.libnotify}/bin/notify-send 'New mail arrived'";
+ };
+ description = "Shell commands to run after onNotify event.";
+ };
+
+ boxes = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ example = [ "Inbox" "[Gmail]/MyLabel" ];
+ description = "IMAP folders to watch.";
+ };
+ };
+}
diff --git a/home-manager/modules/services/imapnotify.nix b/home-manager/modules/services/imapnotify.nix
new file mode 100644
index 00000000000..b59b006e335
--- /dev/null
+++ b/home-manager/modules/services/imapnotify.nix
@@ -0,0 +1,90 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.services.imapnotify;
+
+ safeName = lib.replaceChars [ "@" ":" "\\" "[" "]" ] [ "-" "-" "-" "" "" ];
+
+ imapnotifyAccounts =
+ filter (a: a.imapnotify.enable) (attrValues config.accounts.email.accounts);
+
+ genAccountUnit = account:
+ let name = safeName account.name;
+ in {
+ name = "imapnotify-${name}";
+ value = {
+ Unit = { Description = "imapnotify for ${name}"; };
+
+ Service = {
+ ExecStart =
+ "${pkgs.imapnotify}/bin/imapnotify -c ${genAccountConfig account}";
+ } // optionalAttrs account.notmuch.enable {
+ Environment =
+ "NOTMUCH_CONFIG=${config.xdg.configHome}/notmuch/notmuchrc";
+ };
+
+ Install = { WantedBy = [ "default.target" ]; };
+ };
+ };
+
+ genAccountConfig = account:
+ pkgs.writeText "imapnotify-${safeName account.name}-config.js" (let
+ port = if account.imap.port != null then
+ account.imap.port
+ else if account.imap.tls.enable then
+ 993
+ else
+ 143;
+
+ toJSON = builtins.toJSON;
+ in ''
+ var child_process = require('child_process');
+
+ function getStdout(cmd) {
+ var stdout = child_process.execSync(cmd);
+ return stdout.toString().trim();
+ }
+
+ exports.host = ${toJSON account.imap.host}
+ exports.port = ${toJSON port};
+ exports.tls = ${toJSON account.imap.tls.enable};
+ exports.username = ${toJSON account.userName};
+ exports.password = getStdout("${toString account.passwordCommand}");
+ exports.onNotify = ${toJSON account.imapnotify.onNotify};
+ exports.onNotifyPost = ${toJSON account.imapnotify.onNotifyPost};
+ exports.boxes = ${toJSON account.imapnotify.boxes};
+ '');
+
+in {
+ meta.maintainers = [ maintainers.nickhu ];
+
+ options = {
+ services.imapnotify = { enable = mkEnableOption "imapnotify"; };
+
+ accounts.email.accounts = mkOption {
+ type = with types; attrsOf (submodule (import ./imapnotify-accounts.nix));
+ };
+ };
+
+ config = mkIf cfg.enable {
+ assertions = let
+ checkAccounts = pred: msg:
+ let badAccounts = filter pred imapnotifyAccounts;
+ in {
+ assertion = badAccounts == [ ];
+ message = "imapnotify: Missing ${msg} for accounts: "
+ + concatMapStringsSep ", " (a: a.name) badAccounts;
+ };
+ in [
+ (checkAccounts (a: a.maildir == null) "maildir configuration")
+ (checkAccounts (a: a.imap == null) "IMAP configuration")
+ (checkAccounts (a: a.passwordCommand == null) "password command")
+ (checkAccounts (a: a.userName == null) "username")
+ ];
+
+ systemd.user.services = listToAttrs (map genAccountUnit imapnotifyAccounts);
+ };
+}
diff --git a/home-manager/modules/services/kbfs.nix b/home-manager/modules/services/kbfs.nix
new file mode 100644
index 00000000000..863f4feea3b
--- /dev/null
+++ b/home-manager/modules/services/kbfs.nix
@@ -0,0 +1,67 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.services.kbfs;
+
+in
+
+{
+ options = {
+ services.kbfs = {
+ enable = mkEnableOption "Keybase File System";
+
+ mountPoint = mkOption {
+ type = types.str;
+ default = "keybase";
+ description = ''
+ Mount point for the Keybase filesystem, relative to
+ <envar>HOME</envar>.
+ '';
+ };
+
+ extraFlags = mkOption {
+ type = types.listOf types.str;
+ default = [];
+ example = [
+ "-label kbfs"
+ "-mount-type normal"
+ ];
+ description = ''
+ Additional flags to pass to the Keybase filesystem on launch.
+ '';
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ systemd.user.services.kbfs = {
+ Unit = {
+ Description = "Keybase File System";
+ Requires = [ "keybase.service" ];
+ After = [ "keybase.service" ];
+ };
+
+ Service =
+ let
+ mountPoint = "\"%h/${cfg.mountPoint}\"";
+ in {
+ Environment = "PATH=/run/wrappers/bin KEYBASE_SYSTEMD=1";
+ ExecStartPre = "${pkgs.coreutils}/bin/mkdir -p ${mountPoint}";
+ ExecStart ="${pkgs.kbfs}/bin/kbfsfuse ${toString cfg.extraFlags} ${mountPoint}";
+ ExecStopPost = "/run/wrappers/bin/fusermount -u ${mountPoint}";
+ Restart = "on-failure";
+ PrivateTmp = true;
+ };
+
+ Install = {
+ WantedBy = [ "default.target" ];
+ };
+ };
+
+ home.packages = [ pkgs.kbfs ];
+ services.keybase.enable = true;
+ };
+}
diff --git a/home-manager/modules/services/kdeconnect.nix b/home-manager/modules/services/kdeconnect.nix
new file mode 100644
index 00000000000..82de1f0eb7c
--- /dev/null
+++ b/home-manager/modules/services/kdeconnect.nix
@@ -0,0 +1,72 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.services.kdeconnect;
+ package = pkgs.kdeconnect;
+
+in {
+ meta.maintainers = [ maintainers.adisbladis ];
+
+ options = {
+ services.kdeconnect = {
+ enable = mkEnableOption "KDE connect";
+
+ indicator = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Whether to enable kdeconnect-indicator service.";
+ };
+
+ };
+ };
+
+ config = mkMerge [
+ (mkIf cfg.enable {
+ home.packages = [ package ];
+
+ systemd.user.services.kdeconnect = {
+ Unit = {
+ Description =
+ "Adds communication between your desktop and your smartphone";
+ After = [ "graphical-session-pre.target" ];
+ PartOf = [ "graphical-session.target" ];
+ };
+
+ Install = { WantedBy = [ "graphical-session.target" ]; };
+
+ Service = {
+ Environment = "PATH=${config.home.profileDirectory}/bin";
+ ExecStart = "${package}/libexec/kdeconnectd";
+ Restart = "on-abort";
+ };
+ };
+ })
+
+ (mkIf cfg.indicator {
+ systemd.user.services.kdeconnect-indicator = {
+ Unit = {
+ Description = "kdeconnect-indicator";
+ After = [
+ "graphical-session-pre.target"
+ "polybar.service"
+ "taffybar.service"
+ "stalonetray.service"
+ ];
+ PartOf = [ "graphical-session.target" ];
+ };
+
+ Install = { WantedBy = [ "graphical-session.target" ]; };
+
+ Service = {
+ Environment = "PATH=${config.home.profileDirectory}/bin";
+ ExecStart = "${package}/bin/kdeconnect-indicator";
+ Restart = "on-abort";
+ };
+ };
+ })
+
+ ];
+}
diff --git a/home-manager/modules/services/keepassx.nix b/home-manager/modules/services/keepassx.nix
new file mode 100644
index 00000000000..dc37066e20c
--- /dev/null
+++ b/home-manager/modules/services/keepassx.nix
@@ -0,0 +1,27 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ meta.maintainers = [ maintainers.rycee ];
+
+ options = {
+ services.keepassx = {
+ enable = mkEnableOption "the KeePassX password manager";
+ };
+ };
+
+ config = mkIf config.services.keepassx.enable {
+ systemd.user.services.keepassx = {
+ Unit = {
+ Description = "KeePassX password manager";
+ After = [ "graphical-session-pre.target" ];
+ PartOf = [ "graphical-session.target" ];
+ };
+
+ Install = { WantedBy = [ "graphical-session.target" ]; };
+
+ Service = { ExecStart = "${pkgs.keepassx}/bin/keepassx -min -lock"; };
+ };
+ };
+}
diff --git a/home-manager/modules/services/keybase.nix b/home-manager/modules/services/keybase.nix
new file mode 100644
index 00000000000..2d0a06b06a7
--- /dev/null
+++ b/home-manager/modules/services/keybase.nix
@@ -0,0 +1,37 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.services.keybase;
+
+in
+
+{
+ options = {
+ services.keybase = {
+ enable = mkEnableOption "Keybase";
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ pkgs.keybase ];
+
+ systemd.user.services.keybase = {
+ Unit = {
+ Description = "Keybase service";
+ };
+
+ Service = {
+ ExecStart = "${pkgs.keybase}/bin/keybase service --auto-forked";
+ Restart = "on-failure";
+ PrivateTmp = true;
+ };
+
+ Install = {
+ WantedBy = [ "default.target" ];
+ };
+ };
+ };
+}
diff --git a/home-manager/modules/services/lorri.nix b/home-manager/modules/services/lorri.nix
new file mode 100644
index 00000000000..3b2c244e3c0
--- /dev/null
+++ b/home-manager/modules/services/lorri.nix
@@ -0,0 +1,51 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.services.lorri;
+
+in {
+ meta.maintainers = [ maintainers.gerschtli ];
+
+ options = { services.lorri.enable = mkEnableOption "lorri build daemon"; };
+
+ config = mkIf cfg.enable {
+ home.packages = [ pkgs.lorri ];
+
+ systemd.user = {
+ services.lorri = {
+ Unit = {
+ Description = "lorri build daemon";
+ Requires = "lorri.socket";
+ After = "lorri.socket";
+ RefuseManualStart = true;
+ };
+
+ Service = {
+ ExecStart = "${pkgs.lorri}/bin/lorri daemon";
+ PrivateTmp = true;
+ ProtectSystem = "strict";
+ ProtectHome = "read-only";
+ Restart = "on-failure";
+ Environment = let
+ path = with pkgs;
+ makeSearchPath "bin" [ nix gitMinimal gnutar gzip ];
+ in "PATH=${path}";
+ };
+ };
+
+ sockets.lorri = {
+ Unit = { Description = "Socket for lorri build daemon"; };
+
+ Socket = {
+ ListenStream = "%t/lorri/daemon.socket";
+ RuntimeDirectory = "lorri";
+ };
+
+ Install = { WantedBy = [ "sockets.target" ]; };
+ };
+ };
+ };
+}
diff --git a/home-manager/modules/services/mbsync.nix b/home-manager/modules/services/mbsync.nix
new file mode 100644
index 00000000000..ac6ac1ef78a
--- /dev/null
+++ b/home-manager/modules/services/mbsync.nix
@@ -0,0 +1,104 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.services.mbsync;
+
+ mbsyncOptions = [ "--all" ] ++ optional (cfg.verbose) "--verbose"
+ ++ optional (cfg.configFile != null) "--config ${cfg.configFile}";
+
+in {
+ meta.maintainers = [ maintainers.pjones ];
+
+ options.services.mbsync = {
+ enable = mkEnableOption "mbsync";
+
+ package = mkOption {
+ type = types.package;
+ default = pkgs.isync;
+ defaultText = literalExample "pkgs.isync";
+ example = literalExample "pkgs.isync";
+ description = "The package to use for the mbsync binary.";
+ };
+
+ frequency = mkOption {
+ type = types.str;
+ default = "*:0/5";
+ description = ''
+ How often to run mbsync. This value is passed to the systemd
+ timer configuration as the onCalendar option. See
+ <citerefentry>
+ <refentrytitle>systemd.time</refentrytitle>
+ <manvolnum>7</manvolnum>
+ </citerefentry>
+ for more information about the format.
+ '';
+ };
+
+ verbose = mkOption {
+ type = types.bool;
+ default = true;
+ description = ''
+ Whether mbsync should produce verbose output.
+ '';
+ };
+
+ configFile = mkOption {
+ type = types.nullOr types.path;
+ default = null;
+ description = ''
+ Optional configuration file to link to use instead of
+ the default file (<filename>~/.mbsyncrc</filename>).
+ '';
+ };
+
+ preExec = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ example = "mkdir -p %h/mail";
+ description = ''
+ An optional command to run before mbsync executes. This is
+ useful for creating the directories mbsync is going to use.
+ '';
+ };
+
+ postExec = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ example = "\${pkgs.mu}/bin/mu index";
+ description = ''
+ An optional command to run after mbsync executes successfully.
+ This is useful for running mailbox indexing tools.
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+ systemd.user.services.mbsync = {
+ Unit = { Description = "mbsync mailbox synchronization"; };
+
+ Service = {
+ Type = "oneshot";
+ ExecStart =
+ "${cfg.package}/bin/mbsync ${concatStringsSep " " mbsyncOptions}";
+ } // (optionalAttrs (cfg.postExec != null) {
+ ExecStartPost = cfg.postExec;
+ }) // (optionalAttrs (cfg.preExec != null) {
+ ExecStartPre = cfg.preExec;
+ });
+ };
+
+ systemd.user.timers.mbsync = {
+ Unit = { Description = "mbsync mailbox synchronization"; };
+
+ Timer = {
+ OnCalendar = cfg.frequency;
+ Unit = "mbsync.service";
+ };
+
+ Install = { WantedBy = [ "timers.target" ]; };
+ };
+ };
+}
diff --git a/home-manager/modules/services/mpd.nix b/home-manager/modules/services/mpd.nix
new file mode 100644
index 00000000000..2aa1cd3a9fe
--- /dev/null
+++ b/home-manager/modules/services/mpd.nix
@@ -0,0 +1,150 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ name = "mpd";
+
+ cfg = config.services.mpd;
+
+ mpdConf = pkgs.writeText "mpd.conf" ''
+ music_directory "${cfg.musicDirectory}"
+ playlist_directory "${cfg.playlistDirectory}"
+ ${lib.optionalString (cfg.dbFile != null) ''
+ db_file "${cfg.dbFile}"
+ ''}
+ state_file "${cfg.dataDir}/state"
+ sticker_file "${cfg.dataDir}/sticker.sql"
+
+ ${optionalString (cfg.network.listenAddress != "any")
+ ''bind_to_address "${cfg.network.listenAddress}"''}
+ ${optionalString (cfg.network.port != 6600)
+ ''port "${toString cfg.network.port}"''}
+
+ ${cfg.extraConfig}
+ '';
+
+in {
+
+ ###### interface
+
+ options = {
+
+ services.mpd = {
+
+ enable = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Whether to enable MPD, the music player daemon.
+ '';
+ };
+
+ musicDirectory = mkOption {
+ type = types.path;
+ default = "${config.home.homeDirectory}/music";
+ defaultText = "$HOME/music";
+ apply = toString; # Prevent copies to Nix store.
+ description = ''
+ The directory where mpd reads music from.
+ '';
+ };
+
+ playlistDirectory = mkOption {
+ type = types.path;
+ default = "${cfg.dataDir}/playlists";
+ defaultText = ''''${dataDir}/playlists'';
+ apply = toString; # Prevent copies to Nix store.
+ description = ''
+ The directory where mpd stores playlists.
+ '';
+ };
+
+ extraConfig = mkOption {
+ type = types.lines;
+ default = "";
+ description = ''
+ Extra directives added to to the end of MPD's configuration
+ file, <filename>mpd.conf</filename>. Basic configuration
+ like file location and uid/gid is added automatically to the
+ beginning of the file. For available options see
+ <citerefentry>
+ <refentrytitle>mpd.conf</refentrytitle>
+ <manvolnum>5</manvolnum>
+ </citerefentry>.
+ '';
+ };
+
+ dataDir = mkOption {
+ type = types.path;
+ default = "${config.xdg.dataHome}/${name}";
+ defaultText = "$XDG_DATA_HOME/mpd";
+ apply = toString; # Prevent copies to Nix store.
+ description = ''
+ The directory where MPD stores its state, tag cache,
+ playlists etc.
+ '';
+ };
+
+ network = {
+
+ listenAddress = mkOption {
+ type = types.str;
+ default = "127.0.0.1";
+ example = "any";
+ description = ''
+ The address for the daemon to listen on.
+ Use <literal>any</literal> to listen on all addresses.
+ '';
+ };
+
+ port = mkOption {
+ type = types.port;
+ default = 6600;
+ description = ''
+ The TCP port on which the the daemon will listen.
+ '';
+ };
+
+ };
+
+ dbFile = mkOption {
+ type = types.nullOr types.str;
+ default = "${cfg.dataDir}/tag_cache";
+ defaultText = ''''${dataDir}/tag_cache'';
+ description = ''
+ The path to MPD's database. If set to
+ <literal>null</literal> the parameter is omitted from the
+ configuration.
+ '';
+ };
+ };
+
+ };
+
+
+ ###### implementation
+
+ config = mkIf cfg.enable {
+
+ systemd.user.services.mpd = {
+ Unit = {
+ After = [ "network.target" "sound.target" ];
+ Description = "Music Player Daemon";
+ };
+
+ Install = {
+ WantedBy = [ "default.target" ];
+ };
+
+ Service = {
+ Environment = "PATH=${config.home.profileDirectory}/bin";
+ ExecStart = "${pkgs.mpd}/bin/mpd --no-daemon ${mpdConf}";
+ Type = "notify";
+ ExecStartPre = ''${pkgs.bash}/bin/bash -c "${pkgs.coreutils}/bin/mkdir -p '${cfg.dataDir}' '${cfg.playlistDirectory}'"'';
+ };
+ };
+ };
+
+}
diff --git a/home-manager/modules/services/mpdris2.nix b/home-manager/modules/services/mpdris2.nix
new file mode 100644
index 00000000000..cb8cefba6bd
--- /dev/null
+++ b/home-manager/modules/services/mpdris2.nix
@@ -0,0 +1,101 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.services.mpdris2;
+
+ toIni = generators.toINI {
+ mkKeyValue = key: value:
+ let
+ value' = if isBool value then
+ (if value then "True" else "False")
+ else
+ toString value;
+ in "${key} = ${value'}";
+ };
+
+ mpdris2Conf = {
+ Connection = {
+ host = cfg.mpd.host;
+ port = cfg.mpd.port;
+ music_dir = cfg.mpd.musicDirectory;
+ };
+
+ Bling = {
+ notify = cfg.notifications;
+ mmkeys = cfg.multimediaKeys;
+ };
+ };
+
+in {
+ meta.maintainers = [ maintainers.pjones ];
+
+ options.services.mpdris2 = {
+ enable = mkEnableOption "mpDris2 the MPD to MPRIS2 bridge";
+ notifications = mkEnableOption "song change notifications";
+ multimediaKeys = mkEnableOption "multimedia key support";
+
+ package = mkOption {
+ type = types.package;
+ default = pkgs.mpdris2;
+ defaultText = literalExample "pkgs.mpdris2";
+ description = "The mpDris2 package to use.";
+ };
+
+ mpd = {
+ host = mkOption {
+ type = types.str;
+ default = config.services.mpd.network.listenAddress;
+ defaultText = "config.services.mpd.network.listenAddress";
+ example = "192.168.1.1";
+ description = "The address where MPD is listening for connections.";
+ };
+
+ port = mkOption {
+ type = types.port;
+ default = config.services.mpd.network.port;
+ defaultText = "config.services.mpd.network.port";
+ description = ''
+ The port number where MPD is listening for connections.
+ '';
+ };
+
+ musicDirectory = mkOption {
+ type = types.nullOr types.path;
+ default = config.services.mpd.musicDirectory;
+ defaultText = "config.services.mpd.musicDirectory";
+ description = ''
+ If set, mpDris2 will use this directory to access music artwork.
+ '';
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ assertions = [{
+ assertion = config.services.mpd.enable;
+ message = "The mpdris2 module requires 'services.mpd.enable = true'.";
+ }];
+
+ xdg.configFile."mpDris2/mpDris2.conf".text = toIni mpdris2Conf;
+
+ systemd.user.services.mpdris2 = {
+ Install = { WantedBy = [ "default.target" ]; };
+
+ Unit = {
+ Description = "MPRIS 2 support for MPD";
+ After = [ "mpd.service" ];
+ };
+
+ Service = {
+ Type = "simple";
+ Restart = "on-failure";
+ RestartSec = "5s";
+ ExecStart = "${cfg.package}/bin/mpDris2";
+ BusName = "org.mpris.MediaPlayer2.mpd";
+ };
+ };
+ };
+}
diff --git a/home-manager/modules/services/muchsync.nix b/home-manager/modules/services/muchsync.nix
new file mode 100644
index 00000000000..b7004418d35
--- /dev/null
+++ b/home-manager/modules/services/muchsync.nix
@@ -0,0 +1,203 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+# Documentation was partially copied from the muchsync manual.
+# See http://www.muchsync.org/muchsync.html
+
+let
+ cfg = config.services.muchsync;
+ syncOptions = {
+ options = {
+ frequency = mkOption {
+ type = types.str;
+ default = "*:0/5";
+ description = ''
+ How often to run <command>muchsync</command>. This
+ value is passed to the systemd timer configuration as the
+ <literal>OnCalendar</literal> option. See
+ <citerefentry>
+ <refentrytitle>systemd.time</refentrytitle>
+ <manvolnum>7</manvolnum>
+ </citerefentry>
+ for more information about the format.
+ '';
+ };
+
+ sshCommand = mkOption {
+ type = types.str;
+ default = "${pkgs.openssh}/bin/ssh -CTaxq";
+ defaultText = "ssh -CTaxq";
+ description = ''
+ Specifies a command line to pass to <command>/bin/sh</command>
+ to execute a command on another machine.
+ </para><para>
+ Note that because this string is passed to the shell,
+ special characters including spaces may need to be escaped.
+ '';
+ };
+
+ upload = mkOption {
+ type = types.bool;
+ default = true;
+ description = ''
+ Whether to propagate local changes to the remote.
+ '';
+ };
+
+ local = {
+ checkForModifiedFiles = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Check for locally modified files.
+ Without this option, muchsync assumes that files in a maildir are
+ never edited.
+ </para><para>
+ <option>checkForModifiedFiles</option> disables certain
+ optimizations so as to make muchsync at least check the timestamp on
+ every file, which will detect modified files at the cost of a longer
+ startup time.
+ </para><para>
+ This option is useful if your software regularly modifies the
+ contents of mail files (e.g., because you are running offlineimap
+ with "synclabels = yes").
+ '';
+ };
+
+ importNew = mkOption {
+ type = types.bool;
+ default = true;
+ description = ''
+ Whether to begin the synchronisation by running
+ <command>notmuch new</command> locally.
+ '';
+ };
+ };
+
+ remote = {
+ host = mkOption {
+ type = types.str;
+ description = ''
+ Remote SSH host to synchronize with.
+ '';
+ };
+
+ muchsyncPath = mkOption {
+ type = types.str;
+ default = "";
+ defaultText = "$PATH/muchsync";
+ description = ''
+ Specifies the path to muchsync on the server.
+ Ordinarily, muchsync should be in the default PATH on the server
+ so this option is not required.
+ However, this option is useful if you have to install muchsync in
+ a non-standard place or wish to test development versions of the
+ code.
+ '';
+ };
+
+ checkForModifiedFiles = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Check for modified files on the remote side.
+ Without this option, muchsync assumes that files in a maildir are
+ never edited.
+ </para><para>
+ <option>checkForModifiedFiles</option> disables certain
+ optimizations so as to make muchsync at least check the timestamp on
+ every file, which will detect modified files at the cost of a longer
+ startup time.
+ </para><para>
+ This option is useful if your software regularly modifies the
+ contents of mail files (e.g., because you are running offlineimap
+ with "synclabels = yes").
+ '';
+ };
+
+ importNew = mkOption {
+ type = types.bool;
+ default = true;
+ description = ''
+ Whether to begin the synchronisation by running
+ <command>notmuch new</command> on the remote side.
+ '';
+ };
+ };
+ };
+ };
+
+in {
+ meta.maintainers = with maintainers; [ pacien ];
+
+ options.services.muchsync = {
+ remotes = mkOption {
+ type = with types; attrsOf (submodule syncOptions);
+ default = { };
+ example = literalExample ''
+ {
+ server = {
+ frequency = "*:0/10";
+ remote.host = "server.tld";
+ };
+ }
+ '';
+ description = ''
+ Muchsync remotes to synchronise with.
+ '';
+ };
+ };
+
+ config = let
+ mapRemotes = gen:
+ with attrsets;
+ mapAttrs'
+ (name: remoteCfg: nameValuePair "muchsync-${name}" (gen name remoteCfg))
+ cfg.remotes;
+ in mkIf (cfg.remotes != { }) {
+ assertions = [{
+ assertion = config.programs.notmuch.enable;
+ message = ''
+ The muchsync module requires 'programs.notmuch.enable = true'.
+ '';
+ }];
+
+ systemd.user.services = mapRemotes (name: remoteCfg: {
+ Unit = { Description = "muchsync sync service (${name})"; };
+ Service = {
+ CPUSchedulingPolicy = "idle";
+ IOSchedulingClass = "idle";
+ Environment = [
+ ''"PATH=${pkgs.notmuch}/bin"''
+ ''"NOTMUCH_CONFIG=${config.home.sessionVariables.NOTMUCH_CONFIG}"''
+ ''"NMBGIT=${config.home.sessionVariables.NMBGIT}"''
+ ];
+ ExecStart = concatStringsSep " " ([ "${pkgs.muchsync}/bin/muchsync" ]
+ ++ [ "-s ${escapeShellArg remoteCfg.sshCommand}" ]
+ ++ optional (!remoteCfg.upload) "--noup"
+
+ # local configuration
+ ++ optional remoteCfg.local.checkForModifiedFiles "-F"
+ ++ optional (!remoteCfg.local.importNew) "--nonew"
+
+ # remote configuration
+ ++ [ (escapeShellArg remoteCfg.remote.host) ]
+ ++ optional (remoteCfg.remote.muchsyncPath != "")
+ "-r ${escapeShellArg remoteCfg.remote.muchsyncPath}"
+ ++ optional remoteCfg.remote.checkForModifiedFiles "-F"
+ ++ optional (!remoteCfg.remote.importNew) "--nonew");
+ };
+ });
+
+ systemd.user.timers = mapRemotes (name: remoteCfg: {
+ Unit = { Description = "muchsync periodic sync (${name})"; };
+ Timer = {
+ Unit = "muchsync-${name}.service";
+ OnCalendar = remoteCfg.frequency;
+ Persistent = true;
+ };
+ Install = { WantedBy = [ "timers.target" ]; };
+ });
+ };
+}
diff --git a/home-manager/modules/services/network-manager-applet.nix b/home-manager/modules/services/network-manager-applet.nix
new file mode 100644
index 00000000000..bf57ed65091
--- /dev/null
+++ b/home-manager/modules/services/network-manager-applet.nix
@@ -0,0 +1,36 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.services.network-manager-applet;
+
+in {
+ meta.maintainers = [ maintainers.rycee ];
+
+ options = {
+ services.network-manager-applet = {
+ enable = mkEnableOption "the Network Manager applet";
+ };
+ };
+
+ config = mkIf cfg.enable {
+ systemd.user.services.network-manager-applet = {
+ Unit = {
+ Description = "Network Manager applet";
+ After = [ "graphical-session-pre.target" ];
+ PartOf = [ "graphical-session.target" ];
+ };
+
+ Install = { WantedBy = [ "graphical-session.target" ]; };
+
+ Service = {
+ ExecStart = toString
+ ([ "${pkgs.networkmanagerapplet}/bin/nm-applet" "--sm-disable" ]
+ ++ optional config.xsession.preferStatusNotifierItems
+ "--indicator");
+ };
+ };
+ };
+}
diff --git a/home-manager/modules/services/nextcloud-client.nix b/home-manager/modules/services/nextcloud-client.nix
new file mode 100644
index 00000000000..555ca11ad64
--- /dev/null
+++ b/home-manager/modules/services/nextcloud-client.nix
@@ -0,0 +1,26 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ options = {
+ services.nextcloud-client = { enable = mkEnableOption "Nextcloud Client"; };
+ };
+
+ config = mkIf config.services.nextcloud-client.enable {
+ systemd.user.services.nextcloud-client = {
+ Unit = {
+ Description = "Nextcloud Client";
+ After = [ "graphical-session-pre.target" ];
+ PartOf = [ "graphical-session.target" ];
+ };
+
+ Service = {
+ Environment = "PATH=${config.home.profileDirectory}/bin";
+ ExecStart = "${pkgs.nextcloud-client}/bin/nextcloud";
+ };
+
+ Install = { WantedBy = [ "graphical-session.target" ]; };
+ };
+ };
+}
diff --git a/home-manager/modules/services/owncloud-client.nix b/home-manager/modules/services/owncloud-client.nix
new file mode 100644
index 00000000000..d55d8ffa2a4
--- /dev/null
+++ b/home-manager/modules/services/owncloud-client.nix
@@ -0,0 +1,26 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ options = {
+ services.owncloud-client = { enable = mkEnableOption "Owncloud Client"; };
+ };
+
+ config = mkIf config.services.owncloud-client.enable {
+ systemd.user.services.owncloud-client = {
+ Unit = {
+ Description = "Owncloud Client";
+ After = [ "graphical-session-pre.target" ];
+ PartOf = [ "graphical-session.target" ];
+ };
+
+ Service = {
+ Environment = "PATH=${config.home.profileDirectory}/bin";
+ ExecStart = "${pkgs.owncloud-client}/bin/owncloud";
+ };
+
+ Install = { WantedBy = [ "graphical-session.target" ]; };
+ };
+ };
+}
diff --git a/home-manager/modules/services/parcellite.nix b/home-manager/modules/services/parcellite.nix
new file mode 100644
index 00000000000..ce04238613b
--- /dev/null
+++ b/home-manager/modules/services/parcellite.nix
@@ -0,0 +1,35 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.services.parcellite;
+ package = pkgs.parcellite;
+
+in {
+ meta.maintainers = [ maintainers.gleber ];
+
+ options = {
+ services.parcellite = { enable = mkEnableOption "Parcellite"; };
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ package ];
+
+ systemd.user.services.parcellite = {
+ Unit = {
+ Description = "Lightweight GTK+ clipboard manager";
+ After = [ "graphical-session-pre.target" ];
+ PartOf = [ "graphical-session.target" ];
+ };
+
+ Install = { WantedBy = [ "graphical-session.target" ]; };
+
+ Service = {
+ ExecStart = "${package}/bin/parcellite";
+ Restart = "on-abort";
+ };
+ };
+ };
+}
diff --git a/home-manager/modules/services/password-store-sync.nix b/home-manager/modules/services/password-store-sync.nix
new file mode 100644
index 00000000000..81933914980
--- /dev/null
+++ b/home-manager/modules/services/password-store-sync.nix
@@ -0,0 +1,71 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ serviceCfg = config.services.password-store-sync;
+ programCfg = config.programs.password-store;
+
+in {
+ meta.maintainers = with maintainers; [ pacien ];
+
+ options.services.password-store-sync = {
+ enable = mkEnableOption "Password store periodic sync";
+
+ frequency = mkOption {
+ type = types.str;
+ default = "*:0/5";
+ description = ''
+ How often to synchronise the password store git repository with its
+ default upstream.
+ </para><para>
+ This value is passed to the systemd timer configuration as the
+ <literal>onCalendar</literal> option.
+ See
+ <citerefentry>
+ <refentrytitle>systemd.time</refentrytitle>
+ <manvolnum>7</manvolnum>
+ </citerefentry>
+ for more information about the format.
+ '';
+ };
+ };
+
+ config = mkIf serviceCfg.enable {
+ assertions = [{
+ assertion = programCfg.enable;
+ message = "The 'services.password-store-sync' module requires"
+ + " 'programs.password-store.enable = true'.";
+ }];
+
+ systemd.user.services.password-store-sync = {
+ Unit = { Description = "Password store sync"; };
+
+ Service = {
+ CPUSchedulingPolicy = "idle";
+ IOSchedulingClass = "idle";
+ Environment = let
+ makeEnvironmentPairs =
+ mapAttrsToList (key: value: "${key}=${builtins.toJSON value}");
+ in makeEnvironmentPairs programCfg.settings;
+ ExecStart = toString (pkgs.writeShellScript "password-store-sync" ''
+ ${pkgs.pass}/bin/pass git pull --rebase && \
+ ${pkgs.pass}/bin/pass git push
+ '');
+ };
+ };
+
+ systemd.user.timers.password-store-sync = {
+ Unit = { Description = "Password store periodic sync"; };
+
+ Timer = {
+ Unit = "password-store-sync.service";
+ OnCalendar = serviceCfg.frequency;
+ Persistent = true;
+ };
+
+ Install = { WantedBy = [ "timers.target" ]; };
+ };
+ };
+}
diff --git a/home-manager/modules/services/pasystray.nix b/home-manager/modules/services/pasystray.nix
new file mode 100644
index 00000000000..7c6651d9499
--- /dev/null
+++ b/home-manager/modules/services/pasystray.nix
@@ -0,0 +1,30 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ meta.maintainers = [ maintainers.pltanton ];
+
+ options = {
+ services.pasystray = { enable = mkEnableOption "PulseAudio system tray"; };
+ };
+
+ config = mkIf config.services.pasystray.enable {
+ systemd.user.services.pasystray = {
+ Unit = {
+ Description = "PulseAudio system tray";
+ After = [ "graphical-session-pre.target" ];
+ PartOf = [ "graphical-session.target" ];
+ };
+
+ Install = { WantedBy = [ "graphical-session.target" ]; };
+
+ Service = {
+ Environment =
+ let toolPaths = makeBinPath [ pkgs.paprefs pkgs.pavucontrol ];
+ in [ "PATH=${toolPaths}" ];
+ ExecStart = "${pkgs.pasystray}/bin/pasystray";
+ };
+ };
+ };
+}
diff --git a/home-manager/modules/services/polybar.nix b/home-manager/modules/services/polybar.nix
new file mode 100644
index 00000000000..934a990638f
--- /dev/null
+++ b/home-manager/modules/services/polybar.nix
@@ -0,0 +1,135 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.services.polybar;
+
+ eitherStrBoolIntList = with types;
+ either str (either bool (either int (listOf str)));
+
+ toPolybarIni = generators.toINI {
+ mkKeyValue = key: value:
+ let
+ quoted = v:
+ if hasPrefix " " v || hasSuffix " " v then ''"${v}"'' else v;
+
+ value' = if isBool value then
+ (if value then "true" else "false")
+ else if (isString value && key != "include-file") then
+ quoted value
+ else
+ toString value;
+ in "${key}=${value'}";
+ };
+
+ configFile = pkgs.writeText "polybar.conf"
+ (toPolybarIni cfg.config + "\n" + cfg.extraConfig);
+
+in {
+ options = {
+ services.polybar = {
+ enable = mkEnableOption "Polybar status bar";
+
+ package = mkOption {
+ type = types.package;
+ default = pkgs.polybar;
+ defaultText = literalExample "pkgs.polybar";
+ description = "Polybar package to install.";
+ example = literalExample ''
+ pkgs.polybar.override {
+ i3GapsSupport = true;
+ alsaSupport = true;
+ iwSupport = true;
+ githubSupport = true;
+ }
+ '';
+ };
+
+ config = mkOption {
+ type = types.coercedTo types.path
+ (p: { "section/base" = { include-file = "${p}"; }; })
+ (types.attrsOf (types.attrsOf eitherStrBoolIntList));
+ description = ''
+ Polybar configuration. Can be either path to a file, or set of attributes
+ that will be used to create the final configuration.
+ '';
+ default = { };
+ example = literalExample ''
+ {
+ "bar/top" = {
+ monitor = "\''${env:MONITOR:eDP1}";
+ width = "100%";
+ height = "3%";
+ radius = 0;
+ modules-center = "date";
+ };
+
+ "module/date" = {
+ type = "internal/date";
+ internal = 5;
+ date = "%d.%m.%y";
+ time = "%H:%M";
+ label = "%time% %date%";
+ };
+ }
+ '';
+ };
+
+ extraConfig = mkOption {
+ type = types.lines;
+ description = "Additional configuration to add.";
+ default = "";
+ example = ''
+ [module/date]
+ type = internal/date
+ interval = 5
+ date = "%d.%m.%y"
+ time = %H:%M
+ format-prefix-foreground = \''${colors.foreground-alt}
+ label = %time% %date%
+ '';
+ };
+
+ script = mkOption {
+ type = types.lines;
+ description = ''
+ This script will be used to start the polybars.
+ Set all necessary environment variables here and start all bars.
+ It can be assumed that <command>polybar</command> executable is in the <envar>PATH</envar>.
+
+ Note, this script must start all bars in the background and then terminate.
+ '';
+ example = "polybar bar &";
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ cfg.package ];
+ xdg.configFile."polybar/config".source = configFile;
+
+ systemd.user.services.polybar = {
+ Unit = {
+ Description = "Polybar status bar";
+ After = [ "graphical-session-pre.target" ];
+ PartOf = [ "graphical-session.target" ];
+ X-Restart-Triggers =
+ [ "${config.xdg.configFile."polybar/config".source}" ];
+ };
+
+ Service = {
+ Type = "forking";
+ Environment = "PATH=${cfg.package}/bin:/run/wrappers/bin";
+ ExecStart =
+ let scriptPkg = pkgs.writeShellScriptBin "polybar-start" cfg.script;
+ in "${scriptPkg}/bin/polybar-start";
+ Restart = "on-failure";
+ };
+
+ Install = { WantedBy = [ "graphical-session.target" ]; };
+ };
+ };
+
+}
diff --git a/home-manager/modules/services/random-background.nix b/home-manager/modules/services/random-background.nix
new file mode 100644
index 00000000000..9deee8deb5c
--- /dev/null
+++ b/home-manager/modules/services/random-background.nix
@@ -0,0 +1,97 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.services.random-background;
+
+ flags = lib.concatStringsSep " "
+ ([ "--bg-${cfg.display}" "--no-fehbg" "--randomize" ]
+ ++ lib.optional (!cfg.enableXinerama) "--no-xinerama");
+
+in {
+ meta.maintainers = [ maintainers.rycee ];
+
+ options = {
+ services.random-background = {
+ enable = mkEnableOption "" // {
+ description = ''
+ Whether to enable random desktop background.
+ </para><para>
+ Note, if you are using NixOS and have set up a custom
+ desktop manager session for Home Manager, then the session
+ configuration must have the <option>bgSupport</option>
+ option set to <literal>true</literal> or the background
+ image set by this module may be overwritten.
+ '';
+ };
+
+ imageDirectory = mkOption {
+ type = types.str;
+ example = "%h/backgrounds";
+ description = ''
+ The directory of images from which a background should be
+ chosen. Should be formatted in a way understood by systemd,
+ e.g., '%h' is the home directory.
+ '';
+ };
+
+ display = mkOption {
+ type = types.enum [ "center" "fill" "max" "scale" "tile" ];
+ default = "fill";
+ description = "Display background images according to this option.";
+ };
+
+ interval = mkOption {
+ default = null;
+ type = types.nullOr types.str;
+ example = "1h";
+ description = ''
+ The duration between changing background image, set to null
+ to only set background when logging in. Should be formatted
+ as a duration understood by systemd.
+ '';
+ };
+
+ enableXinerama = mkOption {
+ default = true;
+ type = types.bool;
+ description = ''
+ Will place a separate image per screen when enabled,
+ otherwise a single image will be stretched across all
+ screens.
+ '';
+ };
+ };
+ };
+
+ config = mkIf cfg.enable (mkMerge ([
+ {
+ systemd.user.services.random-background = {
+ Unit = {
+ Description = "Set random desktop background using feh";
+ After = [ "graphical-session-pre.target" ];
+ PartOf = [ "graphical-session.target" ];
+ };
+
+ Service = {
+ Type = "oneshot";
+ ExecStart = "${pkgs.feh}/bin/feh ${flags} ${cfg.imageDirectory}";
+ IOSchedulingClass = "idle";
+ };
+
+ Install = { WantedBy = [ "graphical-session.target" ]; };
+ };
+ }
+ (mkIf (cfg.interval != null) {
+ systemd.user.timers.random-background = {
+ Unit = { Description = "Set random desktop background using feh"; };
+
+ Timer = { OnUnitActiveSec = cfg.interval; };
+
+ Install = { WantedBy = [ "timers.target" ]; };
+ };
+ })
+ ]));
+}
diff --git a/home-manager/modules/services/redshift.nix b/home-manager/modules/services/redshift.nix
new file mode 100644
index 00000000000..86cbab205f6
--- /dev/null
+++ b/home-manager/modules/services/redshift.nix
@@ -0,0 +1,164 @@
+# Adapted from Nixpkgs.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.services.redshift;
+
+in {
+ meta.maintainers = [ maintainers.rycee ];
+
+ options.services.redshift = {
+ enable = mkOption {
+ type = types.bool;
+ default = false;
+ example = true;
+ description = ''
+ Enable Redshift to change your screen's colour temperature depending on
+ the time of day.
+ '';
+ };
+
+ latitude = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = ''
+ Your current latitude, between <literal>-90.0</literal> and
+ <literal>90.0</literal>. Must be provided along with
+ longitude.
+ '';
+ };
+
+ longitude = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = ''
+ Your current longitude, between <literal>-180.0</literal> and
+ <literal>180.0</literal>. Must be provided along with
+ latitude.
+ '';
+ };
+
+ provider = mkOption {
+ type = types.enum [ "manual" "geoclue2" ];
+ default = "manual";
+ description = ''
+ The location provider to use for determining your location. If set to
+ <literal>manual</literal> you must also provide latitude/longitude.
+ If set to <literal>geoclue2</literal>, you must also enable the global
+ geoclue2 service.
+ '';
+ };
+
+ temperature = {
+ day = mkOption {
+ type = types.int;
+ default = 5500;
+ description = ''
+ Colour temperature to use during the day, between
+ <literal>1000</literal> and <literal>25000</literal> K.
+ '';
+ };
+ night = mkOption {
+ type = types.int;
+ default = 3700;
+ description = ''
+ Colour temperature to use at night, between
+ <literal>1000</literal> and <literal>25000</literal> K.
+ '';
+ };
+ };
+
+ brightness = {
+ day = mkOption {
+ type = types.str;
+ default = "1";
+ description = ''
+ Screen brightness to apply during the day,
+ between <literal>0.1</literal> and <literal>1.0</literal>.
+ '';
+ };
+ night = mkOption {
+ type = types.str;
+ default = "1";
+ description = ''
+ Screen brightness to apply during the night,
+ between <literal>0.1</literal> and <literal>1.0</literal>.
+ '';
+ };
+ };
+
+ package = mkOption {
+ type = types.package;
+ default = pkgs.redshift;
+ defaultText = literalExample "pkgs.redshift";
+ description = ''
+ redshift derivation to use.
+ '';
+ };
+
+ tray = mkOption {
+ type = types.bool;
+ default = false;
+ example = true;
+ description = ''
+ Start the redshift-gtk tray applet.
+ '';
+ };
+
+ extraOptions = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ example = [ "-v" "-m randr" ];
+ description = ''
+ Additional command-line arguments to pass to
+ <command>redshift</command>.
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+ assertions = [{
+ assertion = cfg.provider == "manual" -> cfg.latitude != null
+ && cfg.longitude != null;
+ message = "Must provide services.redshift.latitude and"
+ + " services.redshift.latitude when"
+ + " services.redshift.provider is set to \"manual\".";
+ }];
+
+ systemd.user.services.redshift = {
+ Unit = {
+ Description = "Redshift colour temperature adjuster";
+ After = [ "graphical-session-pre.target" ];
+ PartOf = [ "graphical-session.target" ];
+ };
+
+ Install = { WantedBy = [ "graphical-session.target" ]; };
+
+ Service = {
+ ExecStart = let
+ providerString = if cfg.provider == "manual" then
+ "${cfg.latitude}:${cfg.longitude}"
+ else
+ cfg.provider;
+
+ args = [
+ "-l ${providerString}"
+ "-t ${toString cfg.temperature.day}:${
+ toString cfg.temperature.night
+ }"
+ "-b ${toString cfg.brightness.day}:${toString cfg.brightness.night}"
+ ] ++ cfg.extraOptions;
+
+ command = if cfg.tray then "redshift-gtk" else "redshift";
+ in "${cfg.package}/bin/${command} ${concatStringsSep " " args}";
+ RestartSec = 3;
+ Restart = "always";
+ };
+ };
+ };
+
+}
diff --git a/home-manager/modules/services/rsibreak.nix b/home-manager/modules/services/rsibreak.nix
new file mode 100644
index 00000000000..77eaa71f958
--- /dev/null
+++ b/home-manager/modules/services/rsibreak.nix
@@ -0,0 +1,32 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.services.rsibreak;
+
+in {
+ options.services.rsibreak = {
+
+ enable = mkEnableOption "rsibreak";
+
+ };
+
+ config = mkIf cfg.enable {
+ systemd.user.services.rsibreak = {
+ Unit = {
+ Description = "RSI break timer";
+ After = [ "graphical-session-pre.target" ];
+ PartOf = [ "graphical-session.target" ];
+ };
+
+ Install = { WantedBy = [ "graphical-session.target" ]; };
+
+ Service = {
+ Environment = "PATH=${config.home.profileDirectory}/bin";
+ ExecStart = "${pkgs.rsibreak}/bin/rsibreak";
+ };
+ };
+ };
+}
diff --git a/home-manager/modules/services/screen-locker.nix b/home-manager/modules/services/screen-locker.nix
new file mode 100644
index 00000000000..30591a7d1a5
--- /dev/null
+++ b/home-manager/modules/services/screen-locker.nix
@@ -0,0 +1,85 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.services.screen-locker;
+
+in {
+
+ options.services.screen-locker = {
+ enable = mkEnableOption "screen locker for X session";
+
+ lockCmd = mkOption {
+ type = types.str;
+ description = "Locker command to run.";
+ example = "\${pkgs.i3lock}/bin/i3lock -n -c 000000";
+ };
+
+ inactiveInterval = mkOption {
+ type = types.int;
+ default = 10;
+ description = ''
+ Inactive time interval in minutes after which session will be locked.
+ The minimum is 1 minute, and the maximum is 1 hour.
+ See <link xlink:href="https://linux.die.net/man/1/xautolock"/>.
+ '';
+ };
+
+ xautolockExtraOptions = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ description = ''
+ Extra command-line arguments to pass to <command>xautolock</command>.
+ '';
+ };
+
+ xssLockExtraOptions = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ description = ''
+ Extra command-line arguments to pass to <command>xss-lock</command>.
+ '';
+ };
+
+ };
+
+ config = mkIf cfg.enable {
+ systemd.user.services.xautolock-session = {
+ Unit = {
+ Description = "xautolock, session locker service";
+ After = [ "graphical-session-pre.target" ];
+ PartOf = [ "graphical-session.target" ];
+ };
+
+ Install = { WantedBy = [ "graphical-session.target" ]; };
+
+ Service = {
+ ExecStart = concatStringsSep " " ([
+ "${pkgs.xautolock}/bin/xautolock"
+ "-detectsleep"
+ "-time ${toString cfg.inactiveInterval}"
+ "-locker '${pkgs.systemd}/bin/loginctl lock-session $XDG_SESSION_ID'"
+ ] ++ cfg.xautolockExtraOptions);
+ };
+ };
+
+ systemd.user.services.xss-lock = {
+ Unit = {
+ Description = "xss-lock, session locker service";
+ After = [ "graphical-session-pre.target" ];
+ PartOf = [ "graphical-session.target" ];
+ };
+
+ Install = { WantedBy = [ "graphical-session.target" ]; };
+
+ Service = {
+ ExecStart = concatStringsSep " "
+ ([ "${pkgs.xss-lock}/bin/xss-lock" "-s \${XDG_SESSION_ID}" ]
+ ++ cfg.xssLockExtraOptions ++ [ "-- ${cfg.lockCmd}" ]);
+ };
+ };
+ };
+
+}
diff --git a/home-manager/modules/services/spotifyd.nix b/home-manager/modules/services/spotifyd.nix
new file mode 100644
index 00000000000..bc231814eba
--- /dev/null
+++ b/home-manager/modules/services/spotifyd.nix
@@ -0,0 +1,52 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.services.spotifyd;
+
+ configFile = pkgs.writeText "spotifyd.conf" ''
+ ${generators.toINI { } cfg.settings}
+ '';
+
+in {
+ options.services.spotifyd = {
+ enable = mkEnableOption "SpotifyD connect";
+
+ settings = mkOption {
+ type = types.attrsOf (types.attrsOf types.str);
+ default = { };
+ description = "Configuration for spotifyd";
+ example = literalExample ''
+ {
+ global = {
+ user = "Alex";
+ password = "foo";
+ device_name = "nix";
+ };
+ }
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ pkgs.spotifyd ];
+
+ systemd.user.services.spotifyd = {
+ Unit = {
+ Description = "spotify daemon";
+ Documentation = "https://github.com/Spotifyd/spotifyd";
+ };
+
+ Install.WantedBy = [ "default.target" ];
+
+ Service = {
+ ExecStart =
+ "${pkgs.spotifyd}/bin/spotifyd --no-daemon --config-path ${configFile}";
+ Restart = "always";
+ RestartSec = 12;
+ };
+ };
+ };
+}
diff --git a/home-manager/modules/services/stalonetray.nix b/home-manager/modules/services/stalonetray.nix
new file mode 100644
index 00000000000..cca60498963
--- /dev/null
+++ b/home-manager/modules/services/stalonetray.nix
@@ -0,0 +1,90 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.services.stalonetray;
+
+in {
+ options = {
+ services.stalonetray = {
+ enable = mkEnableOption "Stalonetray system tray";
+
+ package = mkOption {
+ default = pkgs.stalonetray;
+ defaultText = literalExample "pkgs.stalonetray";
+ type = types.package;
+ example = literalExample "pkgs.stalonetray";
+ description = "The package to use for the Stalonetray binary.";
+ };
+
+ config = mkOption {
+ type = with types; attrsOf (nullOr (either str (either bool int)));
+ description = ''
+ Stalonetray configuration as a set of attributes.
+ '';
+ default = { };
+ example = {
+ geometry = "3x1-600+0";
+ decorations = null;
+ icon_size = 30;
+ sticky = true;
+ background = "#cccccc";
+ };
+ };
+
+ extraConfig = mkOption {
+ type = types.lines;
+ description = "Additional configuration lines for stalonetrayrc.";
+ default = "";
+ example = ''
+ geometry 3x1-600+0
+ decorations none
+ icon_size 30
+ sticky true
+ background "#cccccc"
+ '';
+ };
+ };
+ };
+
+ config = mkIf cfg.enable (mkMerge [
+ {
+ home.packages = [ cfg.package ];
+
+ systemd.user.services.stalonetray = {
+ Unit = {
+ Description = "Stalonetray system tray";
+ After = [ "graphical-session-pre.target" ];
+ PartOf = [ "graphical-session.target" ];
+ };
+
+ Install = { WantedBy = [ "graphical-session.target" ]; };
+
+ Service = {
+ ExecStart = "${cfg.package}/bin/stalonetray";
+ Restart = "on-failure";
+ };
+ };
+ }
+
+ (mkIf (cfg.config != { }) {
+ home.file.".stalonetrayrc".text = let
+ valueToString = v:
+ if isBool v then
+ (if v then "true" else "false")
+ else if (v == null) then
+ "none"
+ else
+ ''"${toString v}"'';
+ in concatStrings (mapAttrsToList (k: v: ''
+ ${k} ${valueToString v}
+ '') cfg.config);
+ })
+
+ (mkIf (cfg.extraConfig != "") {
+ home.file.".stalonetrayrc".text = cfg.extraConfig;
+ })
+ ]);
+}
diff --git a/home-manager/modules/services/status-notifier-watcher.nix b/home-manager/modules/services/status-notifier-watcher.nix
new file mode 100644
index 00000000000..3c3e54877b4
--- /dev/null
+++ b/home-manager/modules/services/status-notifier-watcher.nix
@@ -0,0 +1,44 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.services.status-notifier-watcher;
+
+in {
+ meta.maintainers = [ maintainers.pltanton ];
+
+ options = {
+ services.status-notifier-watcher = {
+ enable = mkEnableOption "Status Notifier Watcher";
+
+ package = mkOption {
+ default = pkgs.haskellPackages.status-notifier-item;
+ defaultText =
+ literalExample "pkgs.haskellPackages.status-notifier-item";
+ type = types.package;
+ example = literalExample "pkgs.haskellPackages.status-notifier-item";
+ description =
+ "The package to use for the status notifier watcher binary.";
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ systemd.user.services.status-notifier-watcher = {
+ Unit = {
+ Description = "SNI watcher";
+ After = [ "graphical-session-pre.target" ];
+ PartOf = [ "graphical-session.target" ];
+ Before = [ "taffybar.service" ];
+ };
+
+ Service = { ExecStart = "${cfg.package}/bin/status-notifier-watcher"; };
+
+ Install = {
+ WantedBy = [ "graphical-session.target" "taffybar.service" ];
+ };
+ };
+ };
+}
diff --git a/home-manager/modules/services/sxhkd.nix b/home-manager/modules/services/sxhkd.nix
new file mode 100644
index 00000000000..d9f0a968515
--- /dev/null
+++ b/home-manager/modules/services/sxhkd.nix
@@ -0,0 +1,86 @@
+{config, lib, pkgs, ...}:
+
+with lib;
+
+let
+
+ cfg = config.services.sxhkd;
+
+ keybindingsStr = concatStringsSep "\n" (
+ mapAttrsToList (hotkey: command:
+ optionalString (command != null) ''
+ ${hotkey}
+ ${command}
+ ''
+ )
+ cfg.keybindings
+ );
+
+in
+
+{
+ options.services.sxhkd = {
+ enable = mkEnableOption "simple X hotkey daemon";
+
+ keybindings = mkOption {
+ type = types.attrsOf (types.nullOr types.str);
+ default = {};
+ description = "An attribute set that assigns hotkeys to commands.";
+ example = literalExample ''
+ {
+ "super + shift + {r,c}" = "i3-msg {restart,reload}";
+ "super + {s,w}" = "i3-msg {stacking,tabbed}";
+ }
+ '';
+ };
+
+ extraConfig = mkOption {
+ default = "";
+ type = types.lines;
+ description = "Additional configuration to add.";
+ example = literalExample ''
+ super + {_,shift +} {1-9,0}
+ i3-msg {workspace,move container to workspace} {1-10}
+ '';
+ };
+
+ extraPath = mkOption {
+ default = "";
+ type = types.envVar;
+ description = ''
+ Additional <envar>PATH</envar> entries to search for commands.
+ '';
+ example = "/home/some-user/bin:/extra/path/bin";
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ pkgs.sxhkd ];
+
+ xdg.configFile."sxhkd/sxhkdrc".text = concatStringsSep "\n" [
+ keybindingsStr
+ cfg.extraConfig
+ ];
+
+ systemd.user.services.sxhkd = {
+ Unit = {
+ Description = "simple X hotkey daemon";
+ After = [ "graphical-session-pre.target" ];
+ PartOf = [ "graphical-session.target" ];
+ };
+
+ Service = {
+ Environment =
+ "PATH="
+ + "${config.home.profileDirectory}/bin"
+ + optionalString (cfg.extraPath != "") ":"
+ + cfg.extraPath;
+ ExecStart = "${pkgs.sxhkd}/bin/sxhkd";
+ };
+
+ Install = {
+ WantedBy = [ "graphical-session.target" ];
+ };
+ };
+ };
+}
diff --git a/home-manager/modules/services/syncthing.nix b/home-manager/modules/services/syncthing.nix
new file mode 100644
index 00000000000..2ef10540164
--- /dev/null
+++ b/home-manager/modules/services/syncthing.nix
@@ -0,0 +1,68 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ meta.maintainers = [ maintainers.rycee ];
+
+ options = {
+ services.syncthing = {
+ enable = mkEnableOption "Syncthing continuous file synchronization";
+
+ tray = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Whether to enable QSyncthingTray service.";
+ };
+ };
+ };
+
+ config = mkMerge [
+ (mkIf config.services.syncthing.enable {
+ systemd.user.services = {
+ syncthing = {
+ Unit = {
+ Description =
+ "Syncthing - Open Source Continuous File Synchronization";
+ Documentation = "man:syncthing(1)";
+ After = [ "network.target" ];
+ };
+
+ Service = {
+ ExecStart =
+ "${pkgs.syncthing}/bin/syncthing -no-browser -no-restart -logflags=0";
+ Restart = "on-failure";
+ SuccessExitStatus = [ 3 4 ];
+ RestartForceExitStatus = [ 3 4 ];
+ };
+
+ Install = { WantedBy = [ "default.target" ]; };
+ };
+ };
+ })
+
+ (mkIf config.services.syncthing.tray {
+ systemd.user.services = {
+ qsyncthingtray = {
+ Unit = {
+ Description = "QSyncthingTray";
+ After = [
+ "graphical-session-pre.target"
+ "polybar.service"
+ "taffybar.service"
+ "stalonetray.service"
+ ];
+ PartOf = [ "graphical-session.target" ];
+ };
+
+ Service = {
+ Environment = "PATH=${config.home.profileDirectory}/bin";
+ ExecStart = "${pkgs.qsyncthingtray}/bin/QSyncthingTray";
+ };
+
+ Install = { WantedBy = [ "graphical-session.target" ]; };
+ };
+ };
+ })
+ ];
+}
diff --git a/home-manager/modules/services/taffybar.nix b/home-manager/modules/services/taffybar.nix
new file mode 100644
index 00000000000..5392755423d
--- /dev/null
+++ b/home-manager/modules/services/taffybar.nix
@@ -0,0 +1,44 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.services.taffybar;
+
+in {
+ meta.maintainers = [ maintainers.rycee ];
+
+ options = {
+ services.taffybar = {
+ enable = mkEnableOption "Taffybar";
+
+ package = mkOption {
+ default = pkgs.taffybar;
+ defaultText = literalExample "pkgs.taffybar";
+ type = types.package;
+ example = literalExample "pkgs.taffybar";
+ description = "The package to use for the Taffybar binary.";
+ };
+ };
+ };
+
+ config = mkIf config.services.taffybar.enable {
+ systemd.user.services.taffybar = {
+ Unit = {
+ Description = "Taffybar desktop bar";
+ After = [ "graphical-session-pre.target" ];
+ PartOf = [ "graphical-session.target" ];
+ };
+
+ Service = {
+ ExecStart = "${cfg.package}/bin/taffybar";
+ Restart = "on-failure";
+ };
+
+ Install = { WantedBy = [ "graphical-session.target" ]; };
+ };
+
+ xsession.importedVariables = [ "GDK_PIXBUF_MODULE_FILE" ];
+ };
+}
diff --git a/home-manager/modules/services/tahoe-lafs.nix b/home-manager/modules/services/tahoe-lafs.nix
new file mode 100644
index 00000000000..742b779b270
--- /dev/null
+++ b/home-manager/modules/services/tahoe-lafs.nix
@@ -0,0 +1,19 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ meta.maintainers = [ maintainers.rycee ];
+
+ options = {
+ services.tahoe-lafs = { enable = mkEnableOption "Tahoe-LAFS"; };
+ };
+
+ config = mkIf config.services.tahoe-lafs.enable {
+ systemd.user.services.tahoe-lafs = {
+ Unit = { Description = "Tahoe-LAFS"; };
+
+ Service = { ExecStart = "${pkgs.tahoelafs}/bin/tahoe run -C %h/.tahoe"; };
+ };
+ };
+}
diff --git a/home-manager/modules/services/taskwarrior-sync.nix b/home-manager/modules/services/taskwarrior-sync.nix
new file mode 100644
index 00000000000..d16c0681bee
--- /dev/null
+++ b/home-manager/modules/services/taskwarrior-sync.nix
@@ -0,0 +1,50 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.services.taskwarrior-sync;
+
+in {
+ meta.maintainers = with maintainers; [ minijackson pacien ];
+
+ options.services.taskwarrior-sync = {
+ enable = mkEnableOption "Taskwarrior periodic sync";
+
+ frequency = mkOption {
+ type = types.str;
+ default = "*:0/5";
+ description = ''
+ How often to run <literal>taskwarrior sync</literal>. This
+ value is passed to the systemd timer configuration as the
+ <literal>OnCalendar</literal> option. See
+ <citerefentry>
+ <refentrytitle>systemd.time</refentrytitle>
+ <manvolnum>7</manvolnum>
+ </citerefentry>
+ for more information about the format.
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+ systemd.user.services.taskwarrior-sync = {
+ Unit = { Description = "Taskwarrior sync"; };
+ Service = {
+ CPUSchedulingPolicy = "idle";
+ IOSchedulingClass = "idle";
+ ExecStart = "${pkgs.taskwarrior}/bin/task synchronize";
+ };
+ };
+
+ systemd.user.timers.taskwarrior-sync = {
+ Unit = { Description = "Taskwarrior periodic sync"; };
+ Timer = {
+ Unit = "taskwarrior-sync.service";
+ OnCalendar = cfg.frequency;
+ };
+ Install = { WantedBy = [ "timers.target" ]; };
+ };
+ };
+}
diff --git a/home-manager/modules/services/udiskie.nix b/home-manager/modules/services/udiskie.nix
new file mode 100644
index 00000000000..2444d68ff93
--- /dev/null
+++ b/home-manager/modules/services/udiskie.nix
@@ -0,0 +1,91 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.services.udiskie;
+
+ commandArgs = concatStringsSep " " (map (opt: "-" + opt) [
+ (if cfg.automount then "a" else "A")
+ (if cfg.notify then "n" else "N")
+ ({
+ always = "t";
+ auto = "s";
+ never = "T";
+ }.${cfg.tray})
+ ] ++ optional config.xsession.preferStatusNotifierItems "--appindicator");
+
+in {
+ meta.maintainers = [ maintainers.rycee ];
+
+ imports = [
+ (mkRemovedOptionModule [ "services" "udiskie" "sni" ] ''
+ Support for Status Notifier Items is now configured globally through the
+
+ xsession.preferStatusNotifierItems
+
+ option. Please change to use that instead.
+ '')
+ ];
+
+ options = {
+ services.udiskie = {
+ enable = mkEnableOption "udiskie mount daemon";
+
+ automount = mkOption {
+ type = types.bool;
+ default = true;
+ description = "Whether to automatically mount new devices.";
+ };
+
+ notify = mkOption {
+ type = types.bool;
+ default = true;
+ description = "Whether to show pop-up notifications.";
+ };
+
+ tray = mkOption {
+ type = types.enum [ "always" "auto" "never" ];
+ default = "auto";
+ description = ''
+ Whether to display tray icon.
+ </para><para>
+ The options are
+ <variablelist>
+ <varlistentry>
+ <term><literal>always</literal></term>
+ <listitem><para>Always show tray icon.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><literal>auto</literal></term>
+ <listitem><para>
+ Show tray icon only when there is a device available.
+ </para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><literal>never</literal></term>
+ <listitem><para>Never show tray icon.</para></listitem>
+ </varlistentry>
+ </variablelist>
+ '';
+ };
+ };
+ };
+
+ config = mkIf config.services.udiskie.enable {
+ systemd.user.services.udiskie = {
+ Unit = {
+ Description = "udiskie mount daemon";
+ After = [ "graphical-session-pre.target" ];
+ PartOf = [ "graphical-session.target" ];
+ };
+
+ Service = {
+ ExecStart = "${pkgs.udiskie}/bin/udiskie -2 ${commandArgs}";
+ };
+
+ Install = { WantedBy = [ "graphical-session.target" ]; };
+ };
+ };
+}
diff --git a/home-manager/modules/services/unclutter.nix b/home-manager/modules/services/unclutter.nix
new file mode 100644
index 00000000000..5e760639591
--- /dev/null
+++ b/home-manager/modules/services/unclutter.nix
@@ -0,0 +1,61 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let cfg = config.services.unclutter;
+
+in {
+ options.services.unclutter = {
+
+ enable = mkEnableOption "unclutter";
+
+ package = mkOption {
+ description = "unclutter derivation to use.";
+ type = types.package;
+ default = pkgs.unclutter-xfixes;
+ defaultText = literalExample "pkgs.unclutter-xfixes";
+ };
+
+ timeout = mkOption {
+ description = "Number of seconds before the cursor is marked inactive.";
+ type = types.int;
+ default = 1;
+ };
+
+ threshold = mkOption {
+ description = "Minimum number of pixels considered cursor movement.";
+ type = types.int;
+ default = 1;
+ };
+
+ extraOptions = mkOption {
+ description = "More arguments to pass to the unclutter command.";
+ type = types.listOf types.str;
+ default = [ ];
+ example = [ "exclude-root" "ignore-scrolling" ];
+ };
+ };
+
+ config = mkIf cfg.enable {
+ systemd.user.services.unclutter = {
+ Unit = {
+ Description = "unclutter";
+ After = [ "graphical-session-pre.target" ];
+ PartOf = [ "graphical-session.target" ];
+ };
+
+ Service = {
+ ExecStart = ''
+ ${cfg.package}/bin/unclutter \
+ --timeout ${toString cfg.timeout} \
+ --jitter ${toString (cfg.threshold - 1)} \
+ ${concatMapStrings (x: " --${x}") cfg.extraOptions}
+ '';
+ RestartSec = 3;
+ Restart = "always";
+ };
+
+ Install = { WantedBy = [ "graphical-session.target" ]; };
+ };
+ };
+}
diff --git a/home-manager/modules/services/unison.nix b/home-manager/modules/services/unison.nix
new file mode 100644
index 00000000000..93c59e8fd62
--- /dev/null
+++ b/home-manager/modules/services/unison.nix
@@ -0,0 +1,121 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.services.unison;
+
+ pairOf = t:
+ let list = types.addCheck (types.listOf t) (l: length l == 2);
+ in list // { description = list.description + " of length 2"; };
+
+ pairOptions = {
+ options = {
+ stateDirectory = mkOption {
+ type = types.path;
+ default = "${config.xdg.dataHome}/unison";
+ defaultText = "$XDG_DATA_HOME/unison";
+ description = ''
+ Unison state directory to use.
+ '';
+ };
+
+ commandOptions = mkOption rec {
+ type = with types; attrsOf str;
+ apply = mergeAttrs default;
+ default = {
+ repeat = "watch";
+ sshcmd = "${pkgs.openssh}/bin/ssh";
+ ui = "text";
+ auto = "true";
+ batch = "true";
+ log = "false"; # don't log to file, handled by systemd
+ };
+ description = ''
+ Additional command line options as a dictionary to pass to the
+ <literal>unison</literal> program.
+ </para><para>
+ See
+ <citerefentry>
+ <refentrytitle>unison</refentrytitle>
+ <manvolnum>1</manvolnum>
+ </citerefentry>
+ for a list of available options.
+ '';
+ };
+
+ roots = mkOption {
+ type = pairOf types.str;
+ example = literalExample ''
+ [
+ "/home/user/documents"
+ "ssh://remote/documents"
+ ]
+ '';
+ description = ''
+ Pair of roots to synchronise.
+ '';
+ };
+ };
+ };
+
+ serialiseArg = key: val: "-${key}=${escapeShellArg val}";
+
+ serialiseArgs = args: concatStringsSep " " (mapAttrsToList serialiseArg args);
+
+ makeDefs = gen:
+ mapAttrs'
+ (name: pairCfg: nameValuePair "unison-pair-${name}" (gen name pairCfg))
+ cfg.pairs;
+
+in {
+ meta.maintainers = with maintainers; [ pacien ];
+
+ options.services.unison = {
+ enable = mkEnableOption "Unison synchronisation";
+
+ pairs = mkOption {
+ type = with types; attrsOf (submodule pairOptions);
+ default = { };
+ example = literalExample ''
+ {
+ roots = [
+ "/home/user/documents"
+ "ssh://remote/documents"
+ ];
+ }
+ '';
+ description = ''
+ Unison root pairs to keep synchronised.
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+ systemd.user.services = makeDefs (name: pairCfg: {
+ Unit = {
+ Description = "Unison pair sync (${name})";
+ # Retry forever, useful in case of network disruption.
+ StartLimitIntervalSec = 0;
+ };
+
+ Service = {
+ Restart = "always";
+ RestartSec = 60;
+
+ CPUSchedulingPolicy = "idle";
+ IOSchedulingClass = "idle";
+
+ Environment = [ "UNISON='${toString pairCfg.stateDirectory}'" ];
+ ExecStart = ''
+ ${pkgs.unison}/bin/unison \
+ ${serialiseArgs pairCfg.commandOptions} \
+ ${strings.concatMapStringsSep " " escapeShellArg pairCfg.roots}
+ '';
+ };
+
+ Install = { WantedBy = [ "default.target" ]; };
+ });
+ };
+}
diff --git a/home-manager/modules/services/window-managers/awesome.nix b/home-manager/modules/services/window-managers/awesome.nix
new file mode 100644
index 00000000000..d2e2903f83b
--- /dev/null
+++ b/home-manager/modules/services/window-managers/awesome.nix
@@ -0,0 +1,52 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.xsession.windowManager.awesome;
+ awesome = cfg.package;
+ getLuaPath = lib: dir: "${lib}/${dir}/lua/${pkgs.luaPackages.lua.luaversion}";
+ makeSearchPath = lib.concatMapStrings (path:
+ " --search ${getLuaPath path "share"}"
+ + " --search ${getLuaPath path "lib"}");
+
+in {
+ options = {
+ xsession.windowManager.awesome = {
+ enable = mkEnableOption "Awesome window manager.";
+
+ package = mkOption {
+ type = types.package;
+ default = pkgs.awesome;
+ defaultText = literalExample "pkgs.awesome";
+ description = "Package to use for running the Awesome WM.";
+ };
+
+ luaModules = mkOption {
+ default = [ ];
+ type = types.listOf types.package;
+ description = ''
+ List of lua packages available for being
+ used in the Awesome configuration.
+ '';
+ example = literalExample "[ luaPackages.oocairo ]";
+ };
+
+ noArgb = mkOption {
+ default = false;
+ type = types.bool;
+ description = ''
+ Disable client transparency support, which can be greatly
+ detrimental to performance in some setups
+ '';
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ awesome ];
+ xsession.windowManager.command = "${awesome}/bin/awesome "
+ + optionalString cfg.noArgb "--no-argb " + makeSearchPath cfg.luaModules;
+ };
+}
diff --git a/home-manager/modules/services/window-managers/bspwm/default.nix b/home-manager/modules/services/window-managers/bspwm/default.nix
new file mode 100644
index 00000000000..9ea5adbc880
--- /dev/null
+++ b/home-manager/modules/services/window-managers/bspwm/default.nix
@@ -0,0 +1,74 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.xsession.windowManager.bspwm;
+ bspwm = cfg.package;
+
+ camelToSnake = s:
+ builtins.replaceStrings lib.upperChars (map (c: "_${c}") lib.lowerChars) s;
+
+ formatConfig = n: v:
+ let
+ formatList = x:
+ if isList x then
+ throw "can not convert 2-dimensional lists to bspwm format"
+ else
+ formatValue x;
+
+ formatValue = v:
+ if isBool v then
+ (if v then "true" else "false")
+ else if isList v then
+ concatMapStringsSep ", " formatList v
+ else if isString v then
+ "${lib.strings.escapeShellArg v}"
+ else
+ toString v;
+ in "bspc config ${n} ${formatValue v}";
+
+ formatMonitors = n: v: "bspc monitor ${n} -d ${concatStringsSep " " v}";
+
+ formatRules = target: directiveOptions:
+ let
+ formatDirective = n: v:
+ if isBool v then
+ (if v then "${camelToSnake n}=on" else "${camelToSnake n}=off")
+ else if (n == "desktop" || n == "node") then
+ "${camelToSnake n}='${v}'"
+ else
+ "${camelToSnake n}=${lib.strings.escapeShellArg v}";
+
+ directives =
+ filterAttrs (n: v: v != null && !(lib.strings.hasPrefix "_" n))
+ directiveOptions;
+ directivesStr = builtins.concatStringsSep " "
+ (mapAttrsToList formatDirective directives);
+ in "bspc rule -a ${target} ${directivesStr}";
+
+ formatStartupPrograms = map (s: "${s} &");
+
+in {
+ options = import ./options.nix {
+ inherit pkgs;
+ inherit lib;
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = [ bspwm ];
+ xsession.windowManager.command = let
+ configFile = pkgs.writeShellScript "bspwmrc" (concatStringsSep "\n"
+ ((mapAttrsToList formatMonitors cfg.monitors)
+ ++ (mapAttrsToList formatConfig cfg.settings)
+ ++ (mapAttrsToList formatRules cfg.rules) ++ [''
+ # java gui fixes
+ export _JAVA_AWT_WM_NONREPARENTING=1
+ bspc rule -a sun-awt-X11-XDialogPeer state=floating
+ ''] ++ [ cfg.extraConfig ]
+ ++ (formatStartupPrograms cfg.startupPrograms)));
+ configCmdOpt = optionalString (cfg.settings != null) "-c ${configFile}";
+ in "${cfg.package}/bin/bspwm ${configCmdOpt}";
+ };
+}
diff --git a/home-manager/modules/services/window-managers/bspwm/options.nix b/home-manager/modules/services/window-managers/bspwm/options.nix
new file mode 100644
index 00000000000..58a58a1a782
--- /dev/null
+++ b/home-manager/modules/services/window-managers/bspwm/options.nix
@@ -0,0 +1,214 @@
+{ pkgs, lib }:
+
+with lib;
+
+let
+
+ rule = types.submodule {
+ options = {
+ monitor = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = "The monitor where the rule should be applied.";
+ example = "HDMI-0";
+ };
+
+ desktop = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = "The desktop where the rule should be applied.";
+ example = "^8";
+ };
+
+ node = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = "The node where the rule should be applied.";
+ example = "1";
+ };
+
+ state = mkOption {
+ type = types.nullOr
+ (types.enum [ "tiled" "pseudo_tiled" "floating" "fullscreen" ]);
+ default = null;
+ description = "The state in which a new window should spawn.";
+ example = "floating";
+ };
+
+ layer = mkOption {
+ type = types.nullOr (types.enum [ "below" "normal" "above" ]);
+ default = null;
+ description = "The layer where a new window should spawn.";
+ example = "above";
+ };
+
+ splitDir = mkOption {
+ type = types.nullOr (types.enum [ "north" "west" "south" "east" ]);
+ default = null;
+ description = "The direction where the container is going to be split.";
+ example = "south";
+ };
+
+ splitRatio = mkOption {
+ type = types.nullOr types.float;
+ default = null;
+ description = ''
+ The ratio between the new window and the previous existing window in
+ the desktop.
+ '';
+ example = 0.65;
+ };
+
+ hidden = mkOption {
+ type = types.nullOr types.bool;
+ default = null;
+ description = "Whether the node should occupy any space.";
+ example = true;
+ };
+
+ sticky = mkOption {
+ type = types.nullOr types.bool;
+ default = null;
+ description = "Whether the node should stay on the focused desktop.";
+ example = true;
+ };
+
+ private = mkOption {
+ type = types.nullOr types.bool;
+ default = null;
+ description = ''
+ Whether the node should stay in the same tiling position and size.
+ '';
+ example = true;
+ };
+
+ locked = mkOption {
+ type = types.nullOr types.bool;
+ default = null;
+ description = ''
+ Whether the node should ignore <command>node --close</command>
+ messages.
+ '';
+ example = true;
+ };
+
+ marked = mkOption {
+ type = types.nullOr types.bool;
+ default = null;
+ description = "Whether the node will be marked for deferred actions.";
+ example = true;
+ };
+
+ center = mkOption {
+ type = types.nullOr types.bool;
+ default = null;
+ description = ''
+ Whether the node will be put in the center, in floating mode.
+ '';
+ example = true;
+ };
+
+ follow = mkOption {
+ type = types.nullOr types.bool;
+ default = null;
+ description = "Whether focus should follow the node when it is moved.";
+ example = true;
+ };
+
+ manage = mkOption {
+ type = types.nullOr types.bool;
+ default = null;
+ description = ''
+ Whether the window should be managed by bspwm. If false, the window
+ will be ignored by bspwm entirely. This is useful for overlay apps,
+ e.g. screenshot tools.
+ '';
+ example = true;
+ };
+
+ focus = mkOption {
+ type = types.nullOr types.bool;
+ default = null;
+ description = "Whether the node should gain focus on creation.";
+ example = true;
+ };
+
+ border = mkOption {
+ type = types.nullOr types.bool;
+ default = null;
+ description = "Whether the node should have border.";
+ example = true;
+ };
+ };
+ };
+
+in {
+ xsession.windowManager.bspwm = {
+ enable = mkEnableOption "bspwm window manager.";
+
+ package = mkOption {
+ type = types.package;
+ default = pkgs.bspwm;
+ defaultText = literalExample "pkgs.bspwm";
+ description = "bspwm package to use.";
+ example = literalExample "pkgs.bspwm-unstable";
+ };
+
+ settings = mkOption {
+ type = with types;
+ let primitive = either bool (either int (either float str));
+ in attrsOf (either primitive (listOf primitive));
+ default = { };
+ description = "bspwm configuration";
+ example = {
+ "border_width" = 2;
+ "split_ratio" = 0.52;
+ "gapless_monocle" = true;
+ };
+ };
+
+ extraConfig = mkOption {
+ type = types.lines;
+ default = "";
+ description = "Additional configuration to add.";
+ example = ''
+ bspc subscribe all > ~/bspc-report.log &
+ '';
+ };
+
+ monitors = mkOption {
+ type = types.attrsOf (types.listOf types.str);
+ default = { };
+ description = "bspc monitor configurations";
+ example = { "HDMI-0" = [ "web" "terminal" "III" "IV" ]; };
+ };
+
+ rules = mkOption {
+ type = types.attrsOf rule;
+ default = { };
+ description = "bspc rules";
+ example = literalExample ''
+ {
+ "Gimp" = {
+ desktop = "^8";
+ state = "floating";
+ follow = true;
+ };
+ "Kupfer.py" = {
+ focus = true;
+ };
+ "Screenkey" = {
+ manage = false;
+ };
+ }
+ '';
+ };
+
+ startupPrograms = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ description = "Programs to be executed during startup.";
+ example = [ "numlockx on" "tilda" ];
+ };
+ };
+}
diff --git a/home-manager/modules/services/window-managers/i3.nix b/home-manager/modules/services/window-managers/i3.nix
new file mode 100644
index 00000000000..7a4ec90b1cd
--- /dev/null
+++ b/home-manager/modules/services/window-managers/i3.nix
@@ -0,0 +1,856 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.xsession.windowManager.i3;
+
+ commonOptions = {
+ fonts = mkOption {
+ type = types.listOf types.str;
+ default = ["monospace 8"];
+ description = ''
+ Font list used for window titles. Only FreeType fonts are supported.
+ The order here is improtant (e.g. icons font should go before the one used for text).
+ '';
+ example = [ "FontAwesome 10" "Terminus 10" ];
+ };
+ };
+
+ startupModule = types.submodule {
+ options = {
+ command = mkOption {
+ type = types.str;
+ description = "Command that will be executed on startup.";
+ };
+
+ always = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Whether to run command on each i3 restart.";
+ };
+
+ notification = mkOption {
+ type = types.bool;
+ default = true;
+ description = ''
+ Whether to enable startup-notification support for the command.
+ See <option>--no-startup-id</option> option description in the i3 user guide.
+ '';
+ };
+
+ workspace = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = ''
+ Launch application on a particular workspace. DEPRECATED:
+ Use <varname><link linkend="opt-xsession.windowManager.i3.config.assigns">xsession.windowManager.i3.config.assigns</link></varname>
+ instead. See <link xlink:href="https://github.com/rycee/home-manager/issues/265"/>.
+ '';
+ };
+ };
+ };
+
+ barColorSetModule = types.submodule {
+ options = {
+ border = mkOption {
+ type = types.str;
+ visible = false;
+ };
+
+ background = mkOption {
+ type = types.str;
+ visible = false;
+ };
+
+ text = mkOption {
+ type = types.str;
+ visible = false;
+ };
+ };
+ };
+
+ colorSetModule = types.submodule {
+ options = {
+ border = mkOption {
+ type = types.str;
+ visible = false;
+ };
+
+ childBorder = mkOption {
+ type = types.str;
+ visible = false;
+ };
+
+ background = mkOption {
+ type = types.str;
+ visible = false;
+ };
+
+ text = mkOption {
+ type = types.str;
+ visible = false;
+ };
+
+ indicator = mkOption {
+ type = types.str;
+ visible = false;
+ };
+ };
+ };
+
+ barModule = types.submodule {
+ options = {
+ inherit (commonOptions) fonts;
+
+ extraConfig = mkOption {
+ type = types.lines;
+ default = "";
+ description = "Extra configuration lines for this bar.";
+ };
+
+ id = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = ''
+ Specifies the bar ID for the configured bar instance.
+ If this option is missing, the ID is set to bar-x, where x corresponds
+ to the position of the embedding bar block in the config file.
+ '';
+ };
+
+ mode = mkOption {
+ type = types.enum [ "dock" "hide" "invisible" ];
+ default = "dock";
+ description = "Bar visibility mode.";
+ };
+
+ hiddenState = mkOption {
+ type = types.enum [ "hide" "show" ];
+ default = "hide";
+ description = "The default bar mode when 'bar.mode' == 'hide'.";
+ };
+
+ position = mkOption {
+ type = types.enum [ "top" "bottom" ];
+ default = "bottom";
+ description = "The edge of the screen i3bar should show up.";
+ };
+
+ workspaceButtons = mkOption {
+ type = types.bool;
+ default = true;
+ description = "Whether workspace buttons should be shown or not.";
+ };
+
+ workspaceNumbers = mkOption {
+ type = types.bool;
+ default = true;
+ description = "Whether workspace numbers should be displayed within the workspace buttons.";
+ };
+
+ command = mkOption {
+ type = types.str;
+ default = "${cfg.package}/bin/i3bar";
+ defaultText = "i3bar";
+ description = "Command that will be used to start a bar.";
+ example = "\${pkgs.i3-gaps}/bin/i3bar -t";
+ };
+
+ statusCommand = mkOption {
+ type = types.str;
+ default = "${pkgs.i3status}/bin/i3status";
+ description = "Command that will be used to get status lines.";
+ };
+
+ colors = mkOption {
+ type = types.submodule {
+ options = {
+ background = mkOption {
+ type = types.str;
+ default = "#000000";
+ description = "Background color of the bar.";
+ };
+
+ statusline = mkOption {
+ type = types.str;
+ default = "#ffffff";
+ description = "Text color to be used for the statusline.";
+ };
+
+ separator = mkOption {
+ type = types.str;
+ default = "#666666";
+ description = "Text color to be used for the separator.";
+ };
+
+ focusedWorkspace = mkOption {
+ type = barColorSetModule;
+ default = { border = "#4c7899"; background = "#285577"; text = "#ffffff"; };
+ description = ''
+ Border, background and text color for a workspace button when the workspace has focus.
+ '';
+ };
+
+ activeWorkspace = mkOption {
+ type = barColorSetModule;
+ default = { border = "#333333"; background = "#5f676a"; text = "#ffffff"; };
+ description = ''
+ Border, background and text color for a workspace button when the workspace is active.
+ '';
+ };
+
+ inactiveWorkspace = mkOption {
+ type = barColorSetModule;
+ default = { border = "#333333"; background = "#222222"; text = "#888888"; };
+ description = ''
+ Border, background and text color for a workspace button when the workspace does not
+ have focus and is not active.
+ '';
+ };
+
+ urgentWorkspace = mkOption {
+ type = barColorSetModule;
+ default = { border = "#2f343a"; background = "#900000"; text = "#ffffff"; };
+ description = ''
+ Border, background and text color for a workspace button when the workspace contains
+ a window with the urgency hint set.
+ '';
+ };
+
+ bindingMode = mkOption {
+ type = barColorSetModule;
+ default = { border = "#2f343a"; background = "#900000"; text = "#ffffff"; };
+ description = "Border, background and text color for the binding mode indicator";
+ };
+ };
+ };
+ default = {};
+ description = ''
+ Bar color settings. All color classes can be specified using submodules
+ with 'border', 'background', 'text', fields and RGB color hex-codes as values.
+ See default values for the reference.
+ Note that 'background', 'status', and 'separator' parameters take a single RGB value.
+
+ See <link xlink:href="https://i3wm.org/docs/userguide.html#_colors"/>.
+ '';
+ };
+
+ trayOutput = mkOption {
+ type = types.str;
+ default = "primary";
+ description = "Where to output tray.";
+ };
+ };
+ };
+
+ windowCommandModule = types.submodule {
+ options = {
+ command = mkOption {
+ type = types.str;
+ description = "i3wm command to execute.";
+ example = "border pixel 1";
+ };
+
+ criteria = mkOption {
+ type = criteriaModule;
+ description = "Criteria of the windows on which command should be executed.";
+ example = { title = "x200: ~/work"; };
+ };
+ };
+ };
+
+ criteriaModule = types.attrsOf types.str;
+
+ configModule = types.submodule {
+ options = {
+ inherit (commonOptions) fonts;
+
+ window = mkOption {
+ type = types.submodule {
+ options = {
+ titlebar = mkOption {
+ type = types.bool;
+ default = cfg.package != pkgs.i3-gaps;
+ defaultText = "xsession.windowManager.i3.package != nixpkgs.i3-gaps (titlebar should be disabled for i3-gaps)";
+ description = "Whether to show window titlebars.";
+ };
+
+ border = mkOption {
+ type = types.int;
+ default = 2;
+ description = "Window border width.";
+ };
+
+ hideEdgeBorders = mkOption {
+ type = types.enum [ "none" "vertical" "horizontal" "both" "smart" ];
+ default = "none";
+ description = "Hide window borders adjacent to the screen edges.";
+ };
+
+ commands = mkOption {
+ type = types.listOf windowCommandModule;
+ default = [];
+ description = ''
+ List of commands that should be executed on specific windows.
+ See <option>for_window</option> i3wm option documentation.
+ '';
+ example = [ { command = "border pixel 1"; criteria = { class = "XTerm"; }; } ];
+ };
+ };
+ };
+ default = {};
+ description = "Window titlebar and border settings.";
+ };
+
+ floating = mkOption {
+ type = types.submodule {
+ options = {
+ titlebar = mkOption {
+ type = types.bool;
+ default = cfg.package != pkgs.i3-gaps;
+ defaultText = "xsession.windowManager.i3.package != nixpkgs.i3-gaps (titlebar should be disabled for i3-gaps)";
+ description = "Whether to show floating window titlebars.";
+ };
+
+ border = mkOption {
+ type = types.int;
+ default = 2;
+ description = "Floating windows border width.";
+ };
+
+ modifier = mkOption {
+ type = types.enum [ "Shift" "Control" "Mod1" "Mod2" "Mod3" "Mod4" "Mod5" ];
+ default = cfg.config.modifier;
+ defaultText = "i3.config.modifier";
+ description = "Modifier key that can be used to drag floating windows.";
+ example = "Mod4";
+ };
+
+ criteria = mkOption {
+ type = types.listOf criteriaModule;
+ default = [];
+ description = "List of criteria for windows that should be opened in a floating mode.";
+ example = [ {"title" = "Steam - Update News";} {"class" = "Pavucontrol";} ];
+ };
+ };
+ };
+ default = {};
+ description = "Floating window settings.";
+ };
+
+ focus = mkOption {
+ type = types.submodule {
+ options = {
+ newWindow = mkOption {
+ type = types.enum [ "smart" "urgent" "focus" "none" ];
+ default = "smart";
+ description = ''
+ This option modifies focus behavior on new window activation.
+
+ See <link xlink:href="https://i3wm.org/docs/userguide.html#focus_on_window_activation"/>
+ '';
+ example = "none";
+ };
+
+ followMouse = mkOption {
+ type = types.bool;
+ default = true;
+ description = "Whether focus should follow the mouse.";
+ };
+
+ forceWrapping = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Whether to force focus wrapping in tabbed or stacked container.
+
+ See <link xlink:href="https://i3wm.org/docs/userguide.html#_focus_wrapping"/>
+ '';
+ };
+
+ mouseWarping = mkOption {
+ type = types.bool;
+ default = true;
+ description = ''
+ Whether mouse cursor should be warped to the center of the window when switching focus
+ to a window on a different output.
+ '';
+ };
+ };
+ };
+ default = {};
+ description = "Focus related settings.";
+ };
+
+ assigns = mkOption {
+ type = types.attrsOf (types.listOf criteriaModule);
+ default = {};
+ description = ''
+ An attribute set that assigns applications to workspaces based
+ on criteria.
+ '';
+ example = literalExample ''
+ {
+ "1: web" = [{ class = "^Firefox$"; }];
+ "0: extra" = [{ class = "^Firefox$"; window_role = "About"; }];
+ }
+ '';
+ };
+
+ modifier = mkOption {
+ type = types.enum [ "Shift" "Control" "Mod1" "Mod2" "Mod3" "Mod4" "Mod5" ];
+ default = "Mod1";
+ description = "Modifier key that is used for all default keybindings.";
+ example = "Mod4";
+ };
+
+ workspaceLayout = mkOption {
+ type = types.enum [ "default" "stacked" "tabbed" ];
+ default = "default";
+ example = "tabbed";
+ description = ''
+ The mode in which new containers on workspace level will
+ start.
+ '';
+ };
+
+ workspaceAutoBackAndForth = mkOption {
+ type = types.bool;
+ default = false;
+ example = true;
+ description = ''
+ Assume you are on workspace "1: www" and switch to "2: IM" using
+ mod+2 because somebody sent you a message. You don’t need to remember
+ where you came from now, you can just press $mod+2 again to switch
+ back to "1: www".
+ '';
+ };
+
+ keybindings = mkOption {
+ type = types.attrsOf (types.nullOr types.str);
+ default = mapAttrs (n: mkOptionDefault) {
+ "${cfg.config.modifier}+Return" = "exec i3-sensible-terminal";
+ "${cfg.config.modifier}+Shift+q" = "kill";
+ "${cfg.config.modifier}+d" = "exec ${pkgs.dmenu}/bin/dmenu_run";
+
+ "${cfg.config.modifier}+Left" = "focus left";
+ "${cfg.config.modifier}+Down" = "focus down";
+ "${cfg.config.modifier}+Up" = "focus up";
+ "${cfg.config.modifier}+Right" = "focus right";
+
+ "${cfg.config.modifier}+Shift+Left" = "move left";
+ "${cfg.config.modifier}+Shift+Down" = "move down";
+ "${cfg.config.modifier}+Shift+Up" = "move up";
+ "${cfg.config.modifier}+Shift+Right" = "move right";
+
+ "${cfg.config.modifier}+h" = "split h";
+ "${cfg.config.modifier}+v" = "split v";
+ "${cfg.config.modifier}+f" = "fullscreen toggle";
+
+ "${cfg.config.modifier}+s" = "layout stacking";
+ "${cfg.config.modifier}+w" = "layout tabbed";
+ "${cfg.config.modifier}+e" = "layout toggle split";
+
+ "${cfg.config.modifier}+Shift+space" = "floating toggle";
+ "${cfg.config.modifier}+space" = "focus mode_toggle";
+
+ "${cfg.config.modifier}+a" = "focus parent";
+
+ "${cfg.config.modifier}+Shift+minus" = "move scratchpad";
+ "${cfg.config.modifier}+minus" = "scratchpad show";
+
+ "${cfg.config.modifier}+1" = "workspace number 1";
+ "${cfg.config.modifier}+2" = "workspace number 2";
+ "${cfg.config.modifier}+3" = "workspace number 3";
+ "${cfg.config.modifier}+4" = "workspace number 4";
+ "${cfg.config.modifier}+5" = "workspace number 5";
+ "${cfg.config.modifier}+6" = "workspace number 6";
+ "${cfg.config.modifier}+7" = "workspace number 7";
+ "${cfg.config.modifier}+8" = "workspace number 8";
+ "${cfg.config.modifier}+9" = "workspace number 9";
+ "${cfg.config.modifier}+0" = "workspace number 10";
+
+ "${cfg.config.modifier}+Shift+1" = "move container to workspace number 1";
+ "${cfg.config.modifier}+Shift+2" = "move container to workspace number 2";
+ "${cfg.config.modifier}+Shift+3" = "move container to workspace number 3";
+ "${cfg.config.modifier}+Shift+4" = "move container to workspace number 4";
+ "${cfg.config.modifier}+Shift+5" = "move container to workspace number 5";
+ "${cfg.config.modifier}+Shift+6" = "move container to workspace number 6";
+ "${cfg.config.modifier}+Shift+7" = "move container to workspace number 7";
+ "${cfg.config.modifier}+Shift+8" = "move container to workspace number 8";
+ "${cfg.config.modifier}+Shift+9" = "move container to workspace number 9";
+ "${cfg.config.modifier}+Shift+0" = "move container to workspace number 10";
+
+ "${cfg.config.modifier}+Shift+c" = "reload";
+ "${cfg.config.modifier}+Shift+r" = "restart";
+ "${cfg.config.modifier}+Shift+e" = "exec i3-nagbar -t warning -m 'Do you want to exit i3?' -b 'Yes' 'i3-msg exit'";
+
+ "${cfg.config.modifier}+r" = "mode resize";
+ };
+ defaultText = "Default i3 keybindings.";
+ description = ''
+ An attribute set that assigns a key press to an action using a key symbol.
+ See <link xlink:href="https://i3wm.org/docs/userguide.html#keybindings"/>.
+ </para><para>
+ Consider to use <code>lib.mkOptionDefault</code> function to extend or override
+ default keybindings instead of specifying all of them from scratch.
+ '';
+ example = literalExample ''
+ let
+ modifier = xsession.windowManager.i3.config.modifier;
+ in
+
+ lib.mkOptionDefault {
+ "''${modifier}+Return" = "exec i3-sensible-terminal";
+ "''${modifier}+Shift+q" = "kill";
+ "''${modifier}+d" = "exec \${pkgs.dmenu}/bin/dmenu_run";
+ }
+ '';
+ };
+
+ keycodebindings = mkOption {
+ type = types.attrsOf (types.nullOr types.str);
+ default = {};
+ description = ''
+ An attribute set that assigns keypress to an action using key code.
+ See <link xlink:href="https://i3wm.org/docs/userguide.html#keybindings"/>.
+ '';
+ example = { "214" = "exec --no-startup-id /bin/script.sh"; };
+ };
+
+ colors = mkOption {
+ type = types.submodule {
+ options = {
+ background = mkOption {
+ type = types.str;
+ default = "#ffffff";
+ description = ''
+ Background color of the window. Only applications which do not cover
+ the whole area expose the color.
+ '';
+ };
+
+ focused = mkOption {
+ type = colorSetModule;
+ default = {
+ border = "#4c7899"; background = "#285577"; text = "#ffffff";
+ indicator = "#2e9ef4"; childBorder = "#285577";
+ };
+ description = "A window which currently has the focus.";
+ };
+
+ focusedInactive = mkOption {
+ type = colorSetModule;
+ default = {
+ border = "#333333"; background = "#5f676a"; text = "#ffffff";
+ indicator = "#484e50"; childBorder = "#5f676a";
+ };
+ description = ''
+ A window which is the focused one of its container,
+ but it does not have the focus at the moment.
+ '';
+ };
+
+ unfocused = mkOption {
+ type = colorSetModule;
+ default = {
+ border = "#333333"; background = "#222222"; text = "#888888";
+ indicator = "#292d2e"; childBorder = "#222222";
+ };
+ description = "A window which is not focused.";
+ };
+
+ urgent = mkOption {
+ type = colorSetModule;
+ default = {
+ border = "#2f343a"; background = "#900000"; text = "#ffffff";
+ indicator = "#900000"; childBorder = "#900000";
+ };
+ description = "A window which has its urgency hint activated.";
+ };
+
+ placeholder = mkOption {
+ type = colorSetModule;
+ default = {
+ border = "#000000"; background = "#0c0c0c"; text = "#ffffff";
+ indicator = "#000000"; childBorder = "#0c0c0c";
+ };
+ description = ''
+ Background and text color are used to draw placeholder window
+ contents (when restoring layouts). Border and indicator are ignored.
+ '';
+ };
+ };
+ };
+ default = {};
+ description = ''
+ Color settings. All color classes can be specified using submodules
+ with 'border', 'background', 'text', 'indicator' and 'childBorder' fields
+ and RGB color hex-codes as values. See default values for the reference.
+ Note that 'i3.config.colors.background' parameter takes a single RGB value.
+
+ See <link xlink:href="https://i3wm.org/docs/userguide.html#_changing_colors"/>.
+ '';
+ };
+
+ modes = mkOption {
+ type = types.attrsOf (types.attrsOf types.str);
+ default = {
+ resize = {
+ "Left" = "resize shrink width 10 px or 10 ppt";
+ "Down" = "resize grow height 10 px or 10 ppt";
+ "Up" = "resize shrink height 10 px or 10 ppt";
+ "Right" = "resize grow width 10 px or 10 ppt";
+ "Escape" = "mode default";
+ "Return" = "mode default";
+ };
+ };
+ description = ''
+ An attribute set that defines binding modes and keybindings
+ inside them
+
+ Only basic keybinding is supported (bindsym keycomb action),
+ for more advanced setup use 'i3.extraConfig'.
+ '';
+ };
+
+ bars = mkOption {
+ type = types.listOf barModule;
+ default = [{}];
+ description = ''
+ i3 bars settings blocks. Set to empty list to remove bars completely.
+ '';
+ };
+
+ startup = mkOption {
+ type = types.listOf startupModule;
+ default = [];
+ description = ''
+ Commands that should be executed at startup.
+
+ See <link xlink:href="https://i3wm.org/docs/userguide.html#_automatically_starting_applications_on_i3_startup"/>.
+ '';
+ example = literalExample ''
+ [
+ { command = "systemctl --user restart polybar"; always = true; notification = false; }
+ { command = "dropbox start"; notification = false; }
+ { command = "firefox"; workspace = "1: web"; }
+ ];
+ '';
+ };
+
+ gaps = mkOption {
+ type = types.nullOr (types.submodule {
+ options = {
+ inner = mkOption {
+ type = types.nullOr types.int;
+ default = null;
+ description = "Inner gaps value.";
+ example = 12;
+ };
+
+ outer = mkOption {
+ type = types.nullOr types.int;
+ default = null;
+ description = "Outer gaps value.";
+ example = 5;
+ };
+
+ smartGaps = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ This option controls whether to disable all gaps (outer and inner)
+ on workspace with a single container.
+ '';
+ example = true;
+ };
+
+ smartBorders = mkOption {
+ type = types.enum [ "on" "off" "no_gaps" ];
+ default = "off";
+ description = ''
+ This option controls whether to disable container borders on
+ workspace with a single container.
+ '';
+ };
+ };
+ });
+ default = null;
+ description = ''
+ i3gaps related settings.
+ Note that i3-gaps package should be set for this options to take effect.
+ '';
+ };
+ };
+ };
+
+ keybindingsStr = keybindings: concatStringsSep "\n" (
+ mapAttrsToList (keycomb: action: optionalString (action != null) "bindsym ${keycomb} ${action}") keybindings
+ );
+
+ keycodebindingsStr = keycodebindings: concatStringsSep "\n" (
+ mapAttrsToList (keycomb: action: optionalString (action != null) "bindcode ${keycomb} ${action}") keycodebindings
+ );
+
+ colorSetStr = c: concatStringsSep " " [ c.border c.background c.text c.indicator c.childBorder ];
+ barColorSetStr = c: concatStringsSep " " [ c.border c.background c.text ];
+
+ criteriaStr = criteria: "[${concatStringsSep " " (mapAttrsToList (k: v: ''${k}="${v}"'') criteria)}]";
+
+ modeStr = name: keybindings: ''
+ mode "${name}" {
+ ${keybindingsStr keybindings}
+ }
+ '';
+
+ assignStr = workspace: criteria: concatStringsSep "\n" (
+ map (c: "assign ${criteriaStr c} ${workspace}") criteria
+ );
+
+ barStr = {
+ id, fonts, mode, hiddenState, position, workspaceButtons,
+ workspaceNumbers, command, statusCommand, colors, trayOutput, extraConfig, ...
+ }: ''
+ bar {
+ ${optionalString (id != null) "id ${id}"}
+ font pango:${concatStringsSep ", " fonts}
+ mode ${mode}
+ hidden_state ${hiddenState}
+ position ${position}
+ status_command ${statusCommand}
+ i3bar_command ${command}
+ workspace_buttons ${if workspaceButtons then "yes" else "no"}
+ strip_workspace_numbers ${if !workspaceNumbers then "yes" else "no"}
+ tray_output ${trayOutput}
+ colors {
+ background ${colors.background}
+ statusline ${colors.statusline}
+ separator ${colors.separator}
+ focused_workspace ${barColorSetStr colors.focusedWorkspace}
+ active_workspace ${barColorSetStr colors.activeWorkspace}
+ inactive_workspace ${barColorSetStr colors.inactiveWorkspace}
+ urgent_workspace ${barColorSetStr colors.urgentWorkspace}
+ binding_mode ${barColorSetStr colors.bindingMode}
+ }
+ ${extraConfig}
+ }
+ '';
+
+ gapsStr = with cfg.config.gaps; ''
+ ${optionalString (inner != null) "gaps inner ${toString inner}"}
+ ${optionalString (outer != null) "gaps outer ${toString outer}"}
+ ${optionalString smartGaps "smart_gaps on"}
+ ${optionalString (smartBorders != "off") "smart_borders ${smartBorders}"}
+ '';
+
+ floatingCriteriaStr = criteria: "for_window ${criteriaStr criteria} floating enable";
+ windowCommandsStr = { command, criteria, ... }: "for_window ${criteriaStr criteria} ${command}";
+
+ startupEntryStr = { command, always, notification, workspace, ... }: ''
+ ${if always then "exec_always" else "exec"} ${
+ if (notification && workspace == null) then "" else "--no-startup-id"
+ } ${
+ if (workspace == null) then
+ command
+ else
+ "i3-msg 'workspace ${workspace}; exec ${command}'"
+ }
+ '';
+
+ configFile = pkgs.writeText "i3.conf" ((if cfg.config != null then with cfg.config; ''
+ font pango:${concatStringsSep ", " fonts}
+ floating_modifier ${floating.modifier}
+ new_window ${if window.titlebar then "normal" else "pixel"} ${toString window.border}
+ new_float ${if floating.titlebar then "normal" else "pixel"} ${toString floating.border}
+ hide_edge_borders ${window.hideEdgeBorders}
+ force_focus_wrapping ${if focus.forceWrapping then "yes" else "no"}
+ focus_follows_mouse ${if focus.followMouse then "yes" else "no"}
+ focus_on_window_activation ${focus.newWindow}
+ mouse_warping ${if focus.mouseWarping then "output" else "none"}
+ workspace_layout ${workspaceLayout}
+ workspace_auto_back_and_forth ${if workspaceAutoBackAndForth then "yes" else "no"}
+
+ client.focused ${colorSetStr colors.focused}
+ client.focused_inactive ${colorSetStr colors.focusedInactive}
+ client.unfocused ${colorSetStr colors.unfocused}
+ client.urgent ${colorSetStr colors.urgent}
+ client.placeholder ${colorSetStr colors.placeholder}
+ client.background ${colors.background}
+
+ ${keybindingsStr keybindings}
+ ${keycodebindingsStr keycodebindings}
+ ${concatStringsSep "\n" (mapAttrsToList modeStr modes)}
+ ${concatStringsSep "\n" (mapAttrsToList assignStr assigns)}
+ ${concatStringsSep "\n" (map barStr bars)}
+ ${optionalString (gaps != null) gapsStr}
+ ${concatStringsSep "\n" (map floatingCriteriaStr floating.criteria)}
+ ${concatStringsSep "\n" (map windowCommandsStr window.commands)}
+ ${concatStringsSep "\n" (map startupEntryStr startup)}
+ '' else "") + "\n" + cfg.extraConfig);
+
+in
+
+{
+ options = {
+ xsession.windowManager.i3 = {
+ enable = mkEnableOption "i3 window manager.";
+
+ package = mkOption {
+ type = types.package;
+ default = pkgs.i3;
+ defaultText = literalExample "pkgs.i3";
+ example = literalExample "pkgs.i3-gaps";
+ description = ''
+ i3 package to use.
+ If 'i3.config.gaps' settings are specified, 'pkgs.i3-gaps' will be set as a default package.
+ '';
+ };
+
+ config = mkOption {
+ type = types.nullOr configModule;
+ default = {};
+ description = "i3 configuration options.";
+ };
+
+ extraConfig = mkOption {
+ type = types.lines;
+ default = "";
+ description = "Extra configuration lines to add to ~/.config/i3/config.";
+ };
+ };
+ };
+
+ config = mkIf cfg.enable (mkMerge [
+ {
+ home.packages = [ cfg.package ];
+ xsession.windowManager.command = "${cfg.package}/bin/i3";
+ xdg.configFile."i3/config" = {
+ source = configFile;
+ onChange = ''
+ i3Socket=''${XDG_RUNTIME_DIR:-/run/user/$UID}/i3/ipc-socket.*
+ if [ -S $i3Socket ]; then
+ echo "Reloading i3"
+ $DRY_RUN_CMD ${cfg.package}/bin/i3-msg -s $i3Socket reload 1>/dev/null
+ fi
+ '';
+ };
+ }
+
+ (mkIf (cfg.config != null) {
+ xsession.windowManager.i3.package = mkDefault (
+ if (cfg.config.gaps != null) then pkgs.i3-gaps else pkgs.i3
+ );
+ })
+
+ (mkIf (cfg.config != null && (any (s: s.workspace != null) cfg.config.startup)) {
+ warnings = [
+ ("'xsession.windowManager.i3.config.startup.*.workspace' is deprecated, "
+ + "use 'xsession.windowManager.i3.config.assigns' instead."
+ + "See https://github.com/rycee/home-manager/issues/265.")
+ ];
+ })
+ ]);
+}
diff --git a/home-manager/modules/services/window-managers/xmonad.nix b/home-manager/modules/services/window-managers/xmonad.nix
new file mode 100644
index 00000000000..7be03874a89
--- /dev/null
+++ b/home-manager/modules/services/window-managers/xmonad.nix
@@ -0,0 +1,101 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.xsession.windowManager.xmonad;
+
+ xmonad = pkgs.xmonad-with-packages.override {
+ ghcWithPackages = cfg.haskellPackages.ghcWithPackages;
+ packages = self:
+ cfg.extraPackages self ++ optionals cfg.enableContribAndExtras [
+ self.xmonad-contrib
+ self.xmonad-extras
+ ];
+ };
+
+in {
+ options = {
+ xsession.windowManager.xmonad = {
+ enable = mkEnableOption "xmonad window manager";
+
+ haskellPackages = mkOption {
+ default = pkgs.haskellPackages;
+ defaultText = literalExample "pkgs.haskellPackages";
+ example = literalExample "pkgs.haskell.packages.ghc784";
+ description = ''
+ The <varname>haskellPackages</varname> used to build xmonad
+ and other packages. This can be used to change the GHC
+ version used to build xmonad and the packages listed in
+ <varname>extraPackages</varname>.
+ '';
+ };
+
+ extraPackages = mkOption {
+ default = self: [ ];
+ defaultText = "self: []";
+ example = literalExample ''
+ haskellPackages: [
+ haskellPackages.xmonad-contrib
+ haskellPackages.monad-logger
+ ]
+ '';
+ description = ''
+ Extra packages available to GHC when rebuilding xmonad. The
+ value must be a function which receives the attribute set
+ defined in <varname>haskellPackages</varname> as the sole
+ argument.
+ '';
+ };
+
+ enableContribAndExtras = mkOption {
+ default = false;
+ type = types.bool;
+ description = "Enable xmonad-{contrib,extras} in xmonad.";
+ };
+
+ config = mkOption {
+ type = types.nullOr types.path;
+ default = null;
+ example = literalExample ''
+ pkgs.writeText "xmonad.hs" '''
+ import XMonad
+ main = xmonad defaultConfig
+ { terminal = "urxvt"
+ , modMask = mod4Mask
+ , borderWidth = 3
+ }
+ '''
+ '';
+ description = ''
+ The configuration file to be used for xmonad. This must be
+ an absolute path or <literal>null</literal> in which case
+ <filename>~/.xmonad/xmonad.hs</filename> will not be managed
+ by Home Manager.
+ '';
+ };
+ };
+ };
+
+ config = mkIf cfg.enable (mkMerge [
+ {
+ home.packages = [ (lowPrio xmonad) ];
+ xsession.windowManager.command = "${xmonad}/bin/xmonad";
+ }
+
+ (mkIf (cfg.config != null) {
+ home.file.".xmonad/xmonad.hs".source = cfg.config;
+ home.file.".xmonad/xmonad.hs".onChange = ''
+ echo "Recompiling xmonad"
+ $DRY_RUN_CMD ${config.xsession.windowManager.command} --recompile
+
+ # Attempt to restart xmonad if X is running.
+ if [[ -v DISPLAY ]] ; then
+ echo "Restarting xmonad"
+ $DRY_RUN_CMD ${config.xsession.windowManager.command} --restart
+ fi
+ '';
+ })
+ ]);
+}
diff --git a/home-manager/modules/services/xcape.nix b/home-manager/modules/services/xcape.nix
new file mode 100644
index 00000000000..f4f77caa331
--- /dev/null
+++ b/home-manager/modules/services/xcape.nix
@@ -0,0 +1,76 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.services.xcape;
+
+in {
+ meta.maintainers = [ maintainers.nickhu ];
+
+ options = {
+ services.xcape = {
+ enable = mkEnableOption "xcape";
+
+ timeout = mkOption {
+ type = types.nullOr types.int;
+ default = null;
+ example = 500;
+ description = ''
+ If you hold a key longer than this timeout, xcape will not
+ generate a key event. Default is 500 ms.
+ '';
+ };
+
+ mapExpression = mkOption {
+ type = types.attrsOf types.str;
+ default = { };
+ example = {
+ Shift_L = "Escape";
+ Control_L = "Control_L|O";
+ };
+ description = ''
+ The value has the grammar <literal>Key[|OtherKey]</literal>.
+ </para>
+ <para>
+ The list of key names is found in the header file
+ <filename>X11/keysymdef.h</filename> (remove the
+ <literal>XK_</literal> prefix). Note that due to limitations
+ of X11 shifted keys must be specified as a shift key
+ followed by the key to be pressed rather than the actual
+ name of the character. For example to generate "{" the
+ expression <literal>Shift_L|bracketleft</literal> could be
+ used (assuming that you have a key with "{" above "[").
+ </para>
+ <para>
+ You can also specify keys in decimal (prefix #), octal (#0),
+ or hexadecimal (#0x). They will be interpreted as keycodes
+ unless no corresponding key name is found.
+ '';
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ systemd.user.services.xcape = {
+ Unit = {
+ Description = "xcape";
+ After = [ "graphical-session-pre.target" ];
+ PartOf = [ "graphical-session.target" ];
+ };
+
+ Service = {
+ Type = "forking";
+ ExecStart = "${pkgs.xcape}/bin/xcape"
+ + optionalString (cfg.timeout != null) " -t ${toString cfg.timeout}"
+ + optionalString (cfg.mapExpression != { }) " -e '${
+ builtins.concatStringsSep ";"
+ (attrsets.mapAttrsToList (n: v: "${n}=${v}") cfg.mapExpression)
+ }'";
+ };
+
+ Install = { WantedBy = [ "graphical-session.target" ]; };
+ };
+ };
+}
diff --git a/home-manager/modules/services/xembed-sni-proxy.nix b/home-manager/modules/services/xembed-sni-proxy.nix
new file mode 100644
index 00000000000..ff63d108b77
--- /dev/null
+++ b/home-manager/modules/services/xembed-sni-proxy.nix
@@ -0,0 +1,45 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.services.xembed-sni-proxy;
+
+in {
+ meta.maintainers = [ maintainers.rycee ];
+
+ options = {
+ services.xembed-sni-proxy = {
+ enable = mkEnableOption "XEmbed SNI Proxy";
+
+ package = mkOption {
+ type = types.package;
+ default = pkgs.plasma-workspace;
+ defaultText = literalExample "pkgs.plasma-workspace";
+ description = ''
+ Package containing the <command>xembedsniproxy</command>
+ program.
+ '';
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ systemd.user.services.xembed-sni-proxy = {
+ Unit = {
+ Description = "XEmbed SNI Proxy";
+ After = [ "graphical-session-pre.target" ];
+ PartOf = [ "graphical-session.target" ];
+ };
+
+ Install = { WantedBy = [ "graphical-session.target" ]; };
+
+ Service = {
+ Environment = "PATH=${config.home.profileDirectory}/bin";
+ ExecStart = "${cfg.package}/bin/xembedsniproxy";
+ Restart = "on-abort";
+ };
+ };
+ };
+}
diff --git a/home-manager/modules/services/xscreensaver.nix b/home-manager/modules/services/xscreensaver.nix
new file mode 100644
index 00000000000..73a365aa730
--- /dev/null
+++ b/home-manager/modules/services/xscreensaver.nix
@@ -0,0 +1,52 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.services.xscreensaver;
+
+in {
+ meta.maintainers = [ maintainers.rycee ];
+
+ options = {
+ services.xscreensaver = {
+ enable = mkEnableOption "XScreenSaver";
+
+ settings = mkOption {
+ type = with types; attrsOf (either bool (either int str));
+ default = { };
+ example = {
+ mode = "blank";
+ lock = false;
+ fadeTicks = 20;
+ };
+ description = ''
+ The settings to use for XScreenSaver.
+ '';
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ # To make the xscreensaver-command tool available.
+ home.packages = [ pkgs.xscreensaver ];
+
+ xresources.properties =
+ mapAttrs' (n: nameValuePair "xscreensaver.${n}") cfg.settings;
+
+ systemd.user.services.xscreensaver = {
+ Unit = {
+ Description = "XScreenSaver";
+ After = [ "graphical-session-pre.target" ];
+ PartOf = [ "graphical-session.target" ];
+ };
+
+ Service = {
+ ExecStart = "${pkgs.xscreensaver}/bin/xscreensaver -no-splash";
+ };
+
+ Install = { WantedBy = [ "graphical-session.target" ]; };
+ };
+ };
+}
diff --git a/home-manager/modules/services/xsuspender.nix b/home-manager/modules/services/xsuspender.nix
new file mode 100644
index 00000000000..2eb40f5dd34
--- /dev/null
+++ b/home-manager/modules/services/xsuspender.nix
@@ -0,0 +1,192 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.services.xsuspender;
+
+ xsuspenderOptions = types.submodule {
+ options = {
+ matchWmClassContains = mkOption {
+ description = "Match windows that wm class contains string.";
+ type = types.nullOr types.str;
+ default = null;
+ };
+
+ matchWmClassGroupContains = mkOption {
+ description = "Match windows where wm class group contains string.";
+ type = types.nullOr types.str;
+ default = null;
+ };
+
+ matchWmNameContains = mkOption {
+ description = "Match windows where wm name contains string.";
+ type = types.nullOr types.str;
+ default = null;
+ };
+
+ suspendDelay = mkOption {
+ description = "Initial suspend delay in seconds.";
+ type = types.int;
+ default = 5;
+ };
+
+ resumeEvery = mkOption {
+ description = "Resume interval in seconds.";
+ type = types.int;
+ default = 50;
+ };
+
+ resumeFor = mkOption {
+ description = "Resume duration in seconds.";
+ type = types.int;
+ default = 5;
+ };
+
+ execSuspend = mkOption {
+ description = ''
+ Before suspending, execute this shell script. If it fails,
+ abort suspension.
+ '';
+ type = types.nullOr types.str;
+ default = null;
+ example = ''echo "suspending window $XID of process $PID"'';
+ };
+
+ execResume = mkOption {
+ description = ''
+ Before resuming, execute this shell script. Resume the
+ process regardless script failure.
+ '';
+ type = types.nullOr types.str;
+ default = null;
+ example = "echo resuming ...";
+ };
+
+ sendSignals = mkOption {
+ description = ''
+ Whether to send SIGSTOP / SIGCONT signals or not.
+ If false just the exec scripts are run.
+ '';
+ type = types.bool;
+ default = true;
+ };
+
+ suspendSubtreePattern = mkOption {
+ description =
+ "Also suspend descendant processes that match this regex.";
+ type = types.nullOr types.str;
+ default = null;
+ };
+
+ onlyOnBattery = mkOption {
+ description = "Whether to enable process suspend only on battery.";
+ type = types.bool;
+ default = false;
+ };
+
+ autoSuspendOnBattery = mkOption {
+ description = ''
+ Whether to auto-apply rules when switching to battery
+ power even if the window(s) didn't just lose focus.
+ '';
+ type = types.bool;
+ default = true;
+ };
+
+ downclockOnBattery = mkOption {
+ description = ''
+ Limit CPU consumption for this factor when on battery power.
+ Value 1 means 50% decrease, 2 means 66%, 3 means 75% etc.
+ '';
+ type = types.int;
+ default = 0;
+ };
+ };
+ };
+
+in {
+ meta.maintainers = [ maintainers.offline ];
+
+ options = {
+ services.xsuspender = {
+ enable = mkEnableOption "XSuspender";
+
+ defaults = mkOption {
+ description = "XSuspender defaults.";
+ type = xsuspenderOptions;
+ default = { };
+ };
+
+ rules = mkOption {
+ description = "Attribute set of XSuspender rules.";
+ type = types.attrsOf xsuspenderOptions;
+ default = { };
+ example = {
+ Chromium = {
+ suspendDelay = 10;
+ matchWmClassContains = "chromium-browser";
+ suspendSubtreePattern = "chromium";
+ };
+ };
+ };
+
+ debug = mkOption {
+ description = "Whether to enable debug output.";
+ type = types.bool;
+ default = false;
+ };
+
+ iniContent = mkOption {
+ type = types.attrsOf types.attrs;
+ internal = true;
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ services.xsuspender.iniContent = let
+ mkSection = values:
+ filterAttrs (_: v: v != null) {
+ match_wm_class_contains = values.matchWmClassContains;
+ match_wm_class_group_contains = values.matchWmClassGroupContains;
+ match_wm_name_contains = values.matchWmNameContains;
+ suspend_delay = values.suspendDelay;
+ resume_every = values.resumeEvery;
+ resume_for = values.resumeFor;
+ exec_suspend = values.execSuspend;
+ exec_resume = values.execResume;
+ send_signals = values.sendSignals;
+ suspend_subtree_pattern = values.suspendSubtreePattern;
+ only_on_battery = values.onlyOnBattery;
+ auto_suspend_on_battery = values.autoSuspendOnBattery;
+ downclock_on_battery = values.downclockOnBattery;
+ };
+ in {
+ Default = mkSection cfg.defaults;
+ } // mapAttrs (_: mkSection) cfg.rules;
+
+ # To make the xsuspender tool available.
+ home.packages = [ pkgs.xsuspender ];
+
+ xdg.configFile."xsuspender.conf".text = generators.toINI { } cfg.iniContent;
+
+ systemd.user.services.xsuspender = {
+ Unit = {
+ Description = "XSuspender";
+ After = [ "graphical-session-pre.target" ];
+ PartOf = [ "graphical-session.target" ];
+ X-Restart-Triggers =
+ [ "${config.xdg.configFile."xsuspender.conf".source}" ];
+ };
+
+ Service = {
+ ExecStart = "${pkgs.xsuspender}/bin/xsuspender";
+ Environment = mkIf cfg.debug [ "G_MESSAGE_DEBUG=all" ];
+ };
+
+ Install = { WantedBy = [ "graphical-session.target" ]; };
+ };
+ };
+}
diff --git a/home-manager/modules/systemd-activate.rb b/home-manager/modules/systemd-activate.rb
new file mode 100644
index 00000000000..8382c840e93
--- /dev/null
+++ b/home-manager/modules/systemd-activate.rb
@@ -0,0 +1,203 @@
+require 'set'
+require 'open3'
+require 'shellwords'
+
+@dry_run = ENV['DRY_RUN']
+@verbose = ENV['VERBOSE']
+
+UnitsDir = 'home-files/.config/systemd/user'
+
+# 1. Stop all services from the old generation that are not present in the new generation.
+# 2. Ensure all services from the new generation that are wanted by active targets are running:
+# - Start services that are not already running.
+# - Restart services whose unit config files have changed between generations.
+# 3. If any services were (re)started, wait 'start_timeout_ms' and report services
+# that failed to start. This helps debugging quickly failing services.
+#
+# Whenever service failures are detected, show the output of
+# 'systemd --user status' for the affected services.
+#
+def setup_services(old_gen_path, new_gen_path, start_timeout_ms_string)
+ start_timeout_ms = start_timeout_ms_string.to_i
+
+ old_units_path = File.join(old_gen_path, UnitsDir) unless old_gen_path.empty?
+ new_units_path = File.join(new_gen_path, UnitsDir)
+
+ old_services = get_services(old_units_path)
+ new_services = get_services(new_units_path)
+
+ exit if old_services.empty? && new_services.empty?
+
+ # These services should be running when this script is finished
+ services_to_run = get_services_to_run(new_units_path)
+ maybe_changed_services = services_to_run & old_services
+
+ # Only stop active services, otherwise we might get a 'service not loaded' error
+ # for inactive services that were removed in the current generation.
+ to_stop = get_active_units(old_services - new_services)
+ to_restart = get_changed_services(old_units_path, new_units_path, maybe_changed_services)
+ to_start = get_inactive_units(services_to_run - to_restart)
+
+ raise "daemon-reload failed" unless run_cmd('systemctl --user daemon-reload')
+
+ # Exclude services that aren't allowed to be manually started or stopped
+ no_manual_start, no_manual_stop, no_restart = get_restricted_units(to_stop + to_restart + to_start)
+ to_stop -= no_manual_stop + no_restart
+ to_restart -= no_manual_stop + no_manual_start + no_restart
+ to_start -= no_manual_start
+
+ puts "Not restarting: #{no_restart.join(' ')}" unless no_restart.empty?
+
+ if to_stop.empty? && to_start.empty? && to_restart.empty?
+ print_service_msg("All services are already running", services_to_run)
+ else
+ puts "Setting up services" if @verbose
+ systemctl('stop', to_stop)
+ systemctl('start', to_start)
+ systemctl('restart', to_restart)
+ started_services = to_start + to_restart
+ if start_timeout_ms > 0 && !started_services.empty? && !@dry_run
+ failed = wait_and_get_failed_services(started_services, start_timeout_ms)
+ if failed.empty?
+ print_service_msg("All services are running", services_to_run)
+ else
+ puts
+ puts "Error. These services failed to start:", failed
+ show_failed_services_status(failed)
+ exit 1
+ end
+ end
+ end
+end
+
+def get_services(dir)
+ services = get_service_files(dir) if dir && Dir.exists?(dir)
+ Set.new(services)
+end
+
+def get_service_files(dir)
+ Dir.chdir(dir) { Dir['*.{service,socket}'] }
+end
+
+def get_changed_services(dir_a, dir_b, services)
+ services.select do |service|
+ a = File.join(dir_a, service)
+ b = File.join(dir_b, service)
+ (File.size(a) != File.size(b)) || (File.read(a) != File.read(b))
+ end
+end
+
+TargetDirRegexp = /^(.*\.target)\.wants$/
+
+# @return all services wanted by active targets
+def get_services_to_run(units_dir)
+ return Set.new unless Dir.exists?(units_dir)
+ targets = Dir.entries(units_dir).map { |entry| entry[TargetDirRegexp, 1] }.compact
+ active_targets = get_active_units(targets)
+ services_to_run = active_targets.map do |target|
+ get_service_files(File.join(units_dir, "#{target}.wants"))
+ end.flatten
+ Set.new(services_to_run)
+end
+
+# @return true on success
+def run_cmd(cmd)
+ print_cmd cmd
+ @dry_run || system(cmd)
+end
+
+def systemctl(cmd, services)
+ return if services.empty?
+
+ verb = (cmd == 'stop') ? 'Stopping' : "#{cmd.capitalize}ing"
+ puts "#{verb}: #{services.join(' ')}"
+
+ cmd = ['systemctl', '--user', cmd, *services]
+ if @dry_run
+ puts cmd
+ return
+ end
+
+ output, status = Open3.capture2e(*cmd)
+ print output
+ # Show status for failed services
+ unless status.success?
+ # Due to a bug in systemd, the '--user' argument is not always provided
+ output.scan(/systemctl (?:--user )?(status .*?)['"]/).flatten.each do |status_cmd|
+ puts
+ run_cmd("systemctl --user #{status_cmd}")
+ end
+ exit 1
+ end
+end
+
+def print_cmd(cmd)
+ puts cmd if @verbose || @dry_run
+end
+
+def get_active_units(units)
+ get_units_by_activity(units, true)
+end
+
+def get_inactive_units(units)
+ get_units_by_activity(units, false)
+end
+
+def get_units_by_activity(units, active)
+ return [] if units.empty?
+ units = units.to_a
+ is_active = `systemctl --user is-active #{units.shelljoin}`.split
+ units.select.with_index do |_, i|
+ (is_active[i] == 'active') == active
+ end
+end
+
+def get_restricted_units(units)
+ units = units.to_a
+ infos = `systemctl --user show -p RefuseManualStart -p RefuseManualStop #{units.shelljoin}`
+ .split("\n\n")
+ no_restart = []
+ no_manual_start = []
+ no_manual_stop = []
+ infos.zip(units).each do |info, unit|
+ no_start, no_stop = info.split("\n")
+ no_manual_start << unit if no_start.end_with?('yes')
+ no_manual_stop << unit if no_stop.end_with?('yes')
+ end
+ # Regular expression that indicates that a service should not be
+ # restarted even if a change has been detected.
+ restartRe = /^[ \t]*X-RestartIfChanged[ \t]*=[ \t]*false[ \t]*(?:#.*)?$/
+ units.each do |unit|
+ if `systemctl --user cat #{unit.shellescape}` =~ restartRe
+ no_restart << unit
+ end
+ end
+ [no_manual_start, no_manual_stop, no_restart]
+end
+
+def wait_and_get_failed_services(services, start_timeout_ms)
+ puts "Waiting #{start_timeout_ms} ms for services to fail"
+ # Force the previous message to always be visible before sleeping
+ STDOUT.flush
+ sleep(start_timeout_ms / 1000.0)
+ get_inactive_units(services)
+end
+
+def show_failed_services_status(services)
+ puts
+ services.each do |service|
+ run_cmd("systemctl --user status #{service.shellescape}")
+ puts
+ end
+end
+
+def print_service_msg(msg, services)
+ return if services.empty?
+ if @verbose
+ puts "#{msg}:", services.to_a
+ else
+ puts msg
+ end
+end
+
+setup_services(*ARGV)
diff --git a/home-manager/modules/systemd-activate.sh b/home-manager/modules/systemd-activate.sh
new file mode 100644
index 00000000000..1c464693cfc
--- /dev/null
+++ b/home-manager/modules/systemd-activate.sh
@@ -0,0 +1,114 @@
+#!/usr/bin/env bash
+
+function isStartable() {
+ local service="$1"
+ [[ $(systemctl --user show -p RefuseManualStart "$service") == *=no ]]
+}
+
+function isStoppable() {
+ if [[ -v oldGenPath ]] ; then
+ local service="$1"
+ [[ $(systemctl --user show -p RefuseManualStop "$service") == *=no ]]
+ fi
+}
+
+function systemdPostReload() {
+ local workDir
+ workDir="$(mktemp -d)"
+
+ if [[ -v oldGenPath ]] ; then
+ local oldUserServicePath="$oldGenPath/home-files/.config/systemd/user"
+ fi
+
+ local newUserServicePath="$newGenPath/home-files/.config/systemd/user"
+ local oldServiceFiles="$workDir/old-files"
+ local newServiceFiles="$workDir/new-files"
+ local servicesDiffFile="$workDir/diff-files"
+
+ if [[ ! (-v oldUserServicePath && -d "$oldUserServicePath") \
+ && ! -d "$newUserServicePath" ]]; then
+ return
+ fi
+
+ if [[ ! (-v oldUserServicePath && -d "$oldUserServicePath") ]]; then
+ touch "$oldServiceFiles"
+ else
+ find "$oldUserServicePath" \
+ -maxdepth 1 -name '*.service' -exec basename '{}' ';' \
+ | sort \
+ > "$oldServiceFiles"
+ fi
+
+ if [[ ! -d "$newUserServicePath" ]]; then
+ touch "$newServiceFiles"
+ else
+ find "$newUserServicePath" \
+ -maxdepth 1 -name '*.service' -exec basename '{}' ';' \
+ | sort \
+ > "$newServiceFiles"
+ fi
+
+ diff \
+ --new-line-format='+%L' \
+ --old-line-format='-%L' \
+ --unchanged-line-format=' %L' \
+ "$oldServiceFiles" "$newServiceFiles" \
+ > "$servicesDiffFile" || true
+
+ local -a maybeRestart=( $(grep '^ ' "$servicesDiffFile" | cut -c2-) )
+ local -a maybeStop=( $(grep '^-' "$servicesDiffFile" | cut -c2-) )
+ local -a maybeStart=( $(grep '^+' "$servicesDiffFile" | cut -c2-) )
+ local -a toRestart=( )
+ local -a toStop=( )
+ local -a toStart=( )
+
+ for f in "${maybeRestart[@]}" ; do
+ if isStoppable "$f" \
+ && isStartable "$f" \
+ && systemctl --quiet --user is-active "$f" \
+ && ! cmp --quiet \
+ "$oldUserServicePath/$f" \
+ "$newUserServicePath/$f" ; then
+ toRestart+=("$f")
+ fi
+ done
+
+ for f in "${maybeStop[@]}" ; do
+ if isStoppable "$f" ; then
+ toStop+=("$f")
+ fi
+ done
+
+ for f in "${maybeStart[@]}" ; do
+ if isStartable "$f" ; then
+ toStart+=("$f")
+ fi
+ done
+
+ rm -r "$workDir"
+
+ local sugg=""
+
+ if [[ -n "${toRestart[@]}" ]] ; then
+ sugg="${sugg}systemctl --user restart ${toRestart[@]}\n"
+ fi
+
+ if [[ -n "${toStop[@]}" ]] ; then
+ sugg="${sugg}systemctl --user stop ${toStop[@]}\n"
+ fi
+
+ if [[ -n "${toStart[@]}" ]] ; then
+ sugg="${sugg}systemctl --user start ${toStart[@]}\n"
+ fi
+
+ if [[ -n "$sugg" ]] ; then
+ echo "Suggested commands:"
+ echo -n -e "$sugg"
+ fi
+}
+
+oldGenPath="$1"
+newGenPath="$2"
+
+$DRY_RUN_CMD systemctl --user daemon-reload
+systemdPostReload
diff --git a/home-manager/modules/systemd.nix b/home-manager/modules/systemd.nix
new file mode 100644
index 00000000000..56164020577
--- /dev/null
+++ b/home-manager/modules/systemd.nix
@@ -0,0 +1,268 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.systemd.user;
+
+ enabled = cfg.services != {}
+ || cfg.sockets != {}
+ || cfg.targets != {}
+ || cfg.timers != {}
+ || cfg.paths != {}
+ || cfg.sessionVariables != {};
+
+ toSystemdIni = generators.toINI {
+ mkKeyValue = key: value:
+ let
+ value' =
+ if isBool value then (if value then "true" else "false")
+ else toString value;
+ in
+ "${key}=${value'}";
+ };
+
+ buildService = style: name: serviceCfg:
+ let
+ filename = "${name}.${style}";
+ pathSafeName = lib.replaceChars ["@" ":" "\\" "[" "]"]
+ ["-" "-" "-" "" "" ]
+ filename;
+
+ # Needed because systemd derives unit names from the ultimate
+ # link target.
+ source = pkgs.writeTextFile {
+ name = pathSafeName;
+ text = toSystemdIni serviceCfg;
+ destination = "/${filename}";
+ } + "/${filename}";
+
+ wantedBy = target:
+ {
+ name = "systemd/user/${target}.wants/${filename}";
+ value = { inherit source; };
+ };
+ in
+ singleton {
+ name = "systemd/user/${filename}";
+ value = { inherit source; };
+ }
+ ++
+ map wantedBy (serviceCfg.Install.WantedBy or []);
+
+ buildServices = style: serviceCfgs:
+ concatLists (mapAttrsToList (buildService style) serviceCfgs);
+
+ servicesStartTimeoutMs = builtins.toString cfg.servicesStartTimeoutMs;
+
+ unitType = unitKind: with types;
+ let
+ primitive = either bool (either int str);
+ in
+ attrsOf (attrsOf (attrsOf (either primitive (listOf primitive))))
+ // {
+ description = "systemd ${unitKind} unit configuration";
+ };
+
+ unitDescription = type: ''
+ Definition of systemd per-user ${type} units. Attributes are
+ merged recursively.
+ </para><para>
+ Note that the attributes follow the capitalization and naming used
+ by systemd. More details can be found in
+ <citerefentry>
+ <refentrytitle>systemd.${type}</refentrytitle>
+ <manvolnum>5</manvolnum>
+ </citerefentry>.
+ '';
+
+ unitExample = type: literalExample ''
+ {
+ ${toLower type}-name = {
+ Unit = {
+ Description = "Example description";
+ Documentation = [ "man:example(1)" "man:example(5)" ];
+ };
+
+ ${type} = {
+ …
+ };
+ }
+ };
+ '';
+
+ sessionVariables = mkIf (cfg.sessionVariables != {}) {
+ "environment.d/10-home-manager.conf".text =
+ concatStringsSep "\n" (
+ mapAttrsToList (n: v: "${n}=${toString v}") cfg.sessionVariables
+ ) + "\n";
+ };
+
+in
+
+{
+ meta.maintainers = [ maintainers.rycee ];
+
+ options = {
+ systemd.user = {
+ systemctlPath = mkOption {
+ default = "${pkgs.systemd}/bin/systemctl";
+ defaultText = "\${pkgs.systemd}/bin/systemctl";
+ type = types.str;
+ description = ''
+ Absolute path to the <command>systemctl</command> tool. This
+ option may need to be set if running Home Manager on a
+ non-NixOS distribution.
+ '';
+ };
+
+ services = mkOption {
+ default = {};
+ type = unitType "service";
+ description = unitDescription "service";
+ example = unitExample "Service";
+ };
+
+ sockets = mkOption {
+ default = {};
+ type = unitType "socket";
+ description = unitDescription "socket";
+ example = unitExample "Socket";
+ };
+
+ targets = mkOption {
+ default = {};
+ type = unitType "target";
+ description = unitDescription "target";
+ example = unitExample "Target";
+ };
+
+ timers = mkOption {
+ default = {};
+ type = unitType "timer";
+ description = unitDescription "timer";
+ example = unitExample "Timer";
+ };
+
+ paths = mkOption {
+ default = {};
+ type = unitType "path";
+ description = unitDescription "path";
+ example = unitExample "Path";
+ };
+
+ startServices = mkOption {
+ default = false;
+ type = types.bool;
+ description = ''
+ Start all services that are wanted by active targets.
+ Additionally, stop obsolete services from the previous
+ generation.
+ '';
+ };
+
+ servicesStartTimeoutMs = mkOption {
+ default = 0;
+ type = types.int;
+ description = ''
+ How long to wait for started services to fail until their
+ start is considered successful.
+ '';
+ };
+
+ sessionVariables = mkOption {
+ default = {};
+ type = with types; attrsOf (either int str);
+ example = { EDITOR = "vim"; };
+ description = ''
+ Environment variables that will be set for the user session.
+ The variable values must be as described in
+ <citerefentry>
+ <refentrytitle>environment.d</refentrytitle>
+ <manvolnum>5</manvolnum>
+ </citerefentry>.
+ '';
+ };
+ };
+ };
+
+ config = mkMerge [
+ {
+ assertions = [
+ {
+ assertion = enabled -> pkgs.stdenv.isLinux;
+ message =
+ let
+ names = concatStringsSep ", " (
+ attrNames (
+ cfg.services // cfg.sockets // cfg.targets // cfg.timers // cfg.paths // cfg.sessionVariables
+ )
+ );
+ in
+ "Must use Linux for modules that require systemd: " + names;
+ }
+ ];
+ }
+
+ # If we run under a Linux system we assume that systemd is
+ # available, in particular we assume that systemctl is in PATH.
+ (mkIf pkgs.stdenv.isLinux {
+ xdg.configFile = mkMerge [
+ (listToAttrs (
+ (buildServices "service" cfg.services)
+ ++
+ (buildServices "socket" cfg.sockets)
+ ++
+ (buildServices "target" cfg.targets)
+ ++
+ (buildServices "timer" cfg.timers)
+ ++
+ (buildServices "path" cfg.paths)
+ ))
+
+ sessionVariables
+ ];
+
+ # Run systemd service reload if user is logged in. If we're
+ # running this from the NixOS module then XDG_RUNTIME_DIR is not
+ # set and systemd commands will fail. We'll therefore have to
+ # set it ourselves in that case.
+ home.activation.reloadSystemD = hm.dag.entryAfter ["linkGeneration"] (
+ let
+ autoReloadCmd = ''
+ ${pkgs.ruby}/bin/ruby ${./systemd-activate.rb} \
+ "''${oldGenPath=}" "$newGenPath" "${servicesStartTimeoutMs}"
+ '';
+
+ legacyReloadCmd = ''
+ bash ${./systemd-activate.sh} "''${oldGenPath=}" "$newGenPath"
+ '';
+
+ ensureRuntimeDir = "XDG_RUNTIME_DIR=\${XDG_RUNTIME_DIR:-/run/user/$(id -u)}";
+
+ systemctl = "${ensureRuntimeDir} ${cfg.systemctlPath}";
+ in
+ ''
+ systemdStatus=$(${systemctl} --user is-system-running 2>&1 || true)
+
+ if [[ $systemdStatus == 'running' || $systemdStatus == 'degraded' ]]; then
+ if [[ $systemdStatus == 'degraded' ]]; then
+ warnEcho "The user systemd session is degraded:"
+ ${systemctl} --user --state=failed
+ warnEcho "Attempting to reload services anyway..."
+ fi
+
+ ${ensureRuntimeDir} \
+ PATH=${dirOf cfg.systemctlPath}:$PATH \
+ ${if cfg.startServices then autoReloadCmd else legacyReloadCmd}
+ else
+ echo "User systemd daemon not running. Skipping reload."
+ fi
+
+ unset systemdStatus
+ ''
+ );
+ })
+ ];
+}
diff --git a/home-manager/modules/xcursor.nix b/home-manager/modules/xcursor.nix
new file mode 100644
index 00000000000..63ceef387df
--- /dev/null
+++ b/home-manager/modules/xcursor.nix
@@ -0,0 +1,83 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.xsession.pointerCursor;
+
+ cursorType = types.submodule {
+ options = {
+ package = mkOption {
+ type = types.package;
+ example = literalExample "pkgs.vanilla-dmz";
+ description = "Package providing the cursor theme.";
+ };
+
+ name = mkOption {
+ type = types.str;
+ example = "Vanilla-DMZ";
+ description = "The cursor name within the package.";
+ };
+
+ size = mkOption {
+ type = types.int;
+ default = 32;
+ example = 64;
+ description = "The cursor size.";
+ };
+
+ defaultCursor = mkOption {
+ type = types.str;
+ default = "left_ptr";
+ example = "X_cursor";
+ description = "The default cursor file to use within the package.";
+ };
+ };
+ };
+
+in {
+ meta.maintainers = [ maintainers.league ];
+
+ options = {
+ xsession.pointerCursor = mkOption {
+ type = types.nullOr cursorType;
+ default = null;
+ description = ''
+ The X cursor theme and settings. The package
+ <varname>xorg.xcursorthemes</varname> contains cursors named
+ whiteglass, redglass, and handhelds. The package
+ <varname>vanilla-dmz</varname> contains cursors named Vanilla-DMZ
+ and Vanilla-DMZ-AA. Note: handhelds does not seem to work at
+ custom sizes.
+ '';
+ };
+ };
+
+ config = mkIf (cfg != null) {
+
+ home.packages = [ cfg.package ];
+
+ xsession.initExtra = ''
+ ${pkgs.xorg.xsetroot}/bin/xsetroot -xcf ${cfg.package}/share/icons/${cfg.name}/cursors/${cfg.defaultCursor} ${
+ toString cfg.size
+ }
+ '';
+
+ xresources.properties = {
+ "Xcursor.theme" = cfg.name;
+ "Xcursor.size" = cfg.size;
+ };
+
+ gtk.gtk2.extraConfig = ''
+ gtk-cursor-theme-name="${cfg.name}"
+ gtk-cursor-theme-size=${toString cfg.size}
+ '';
+
+ gtk.gtk3.extraConfig = {
+ "gtk-cursor-theme-name" = cfg.name;
+ "gtk-cursor-theme-size" = cfg.size;
+ };
+
+ };
+}
diff --git a/home-manager/modules/xresources.nix b/home-manager/modules/xresources.nix
new file mode 100644
index 00000000000..b74d671befb
--- /dev/null
+++ b/home-manager/modules/xresources.nix
@@ -0,0 +1,87 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.xresources;
+
+ formatLine = n: v:
+ let
+ formatList = x:
+ if isList x then
+ throw "can not convert 2-dimensional lists to Xresources format"
+ else
+ formatValue x;
+
+ formatValue = v:
+ if isBool v then
+ (if v then "true" else "false")
+ else if isList v then
+ concatMapStringsSep ", " formatList v
+ else
+ toString v;
+ in "${n}: ${formatValue v}";
+
+in {
+ meta.maintainers = [ maintainers.rycee ];
+
+ options = {
+ xresources.properties = mkOption {
+ type = types.nullOr types.attrs;
+ default = null;
+ example = literalExample ''
+ {
+ "Emacs*toolBar" = 0;
+ "XTerm*faceName" = "dejavu sans mono";
+ "XTerm*charClass" = [ "37:48" "45-47:48" "58:48" "64:48" "126:48" ];
+ }
+ '';
+ description = ''
+ X server resources that should be set.
+ Booleans are formatted as "true" or "false" respectively.
+ List elements are recursively formatted as a string and joined by commas.
+ All other values are directly formatted using builtins.toString.
+ Note, that 2-dimensional lists are not supported and specifying one will throw an exception.
+ If this and all other xresources options are
+ <code>null</code>, then this feature is disabled and no
+ <filename>~/.Xresources</filename> link is produced.
+ '';
+ };
+
+ xresources.extraConfig = mkOption {
+ type = types.lines;
+ default = "";
+ example = literalExample ''
+ builtins.readFile (
+ pkgs.fetchFromGitHub {
+ owner = "solarized";
+ repo = "xresources";
+ rev = "025ceddbddf55f2eb4ab40b05889148aab9699fc";
+ sha256 = "0lxv37gmh38y9d3l8nbnsm1mskcv10g3i83j0kac0a2qmypv1k9f";
+ } + "/Xresources.dark"
+ )
+ '';
+ description = ''
+ Additional X server resources contents.
+ If this and all other xresources options are
+ <code>null</code>, then this feature is disabled and no
+ <filename>~/.Xresources</filename> link is produced.
+ '';
+ };
+ };
+
+ config = mkIf (cfg.properties != null || cfg.extraConfig != "") {
+ home.file.".Xresources" = {
+ text = concatStringsSep "\n" ([ ]
+ ++ optional (cfg.extraConfig != "") cfg.extraConfig
+ ++ optionals (cfg.properties != null)
+ (mapAttrsToList formatLine cfg.properties)) + "\n";
+ onChange = ''
+ if [[ -v DISPLAY ]] ; then
+ $DRY_RUN_CMD ${pkgs.xorg.xrdb}/bin/xrdb -merge $HOME/.Xresources
+ fi
+ '';
+ };
+ };
+}
diff --git a/home-manager/modules/xsession.nix b/home-manager/modules/xsession.nix
new file mode 100644
index 00000000000..d32c2849163
--- /dev/null
+++ b/home-manager/modules/xsession.nix
@@ -0,0 +1,168 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.xsession;
+
+in {
+ meta.maintainers = [ maintainers.rycee ];
+
+ options = {
+ xsession = {
+ enable = mkEnableOption "X Session";
+
+ scriptPath = mkOption {
+ type = types.str;
+ default = ".xsession";
+ example = ".xsession-hm";
+ description = ''
+ Path, relative <envar>HOME</envar>, where Home Manager
+ should write the X session script.
+ '';
+ };
+
+ windowManager.command = mkOption {
+ type = types.str;
+ example = literalExample ''
+ let
+ xmonad = pkgs.xmonad-with-packages.override {
+ packages = self: [ self.xmonad-contrib self.taffybar ];
+ };
+ in
+ "''${xmonad}/bin/xmonad";
+ '';
+ description = ''
+ Window manager start command.
+ '';
+ };
+
+ preferStatusNotifierItems = mkOption {
+ type = types.bool;
+ default = false;
+ example = true;
+ description = ''
+ Whether tray applets should prefer using the Status Notifier
+ Items (SNI) protocol, commonly called App Indicators. Note,
+ not all tray applets or status bars support SNI.
+ '';
+ };
+
+ profileExtra = mkOption {
+ type = types.lines;
+ default = "";
+ description = "Extra shell commands to run before session start.";
+ };
+
+ initExtra = mkOption {
+ type = types.lines;
+ default = "";
+ description = "Extra shell commands to run during initialization.";
+ };
+
+ importedVariables = mkOption {
+ type = types.listOf (types.strMatching "[a-zA-Z_][a-zA-Z0-9_]*");
+ example = [ "GDK_PIXBUF_ICON_LOADER" ];
+ visible = false;
+ description = ''
+ Environment variables to import into the user systemd
+ session. The will be available for use by graphical
+ services.
+ '';
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ xsession.importedVariables = [
+ "DBUS_SESSION_BUS_ADDRESS"
+ "DISPLAY"
+ "SSH_AUTH_SOCK"
+ "XAUTHORITY"
+ "XDG_DATA_DIRS"
+ "XDG_RUNTIME_DIR"
+ "XDG_SESSION_ID"
+ ];
+
+ systemd.user = {
+ services = mkIf (config.home.keyboard != null) {
+ setxkbmap = {
+ Unit = {
+ Description = "Set up keyboard in X";
+ After = [ "graphical-session-pre.target" ];
+ PartOf = [ "graphical-session.target" ];
+ };
+
+ Install = { WantedBy = [ "graphical-session.target" ]; };
+
+ Service = {
+ Type = "oneshot";
+ RemainAfterExit = true;
+ ExecStart = with config.home.keyboard;
+ let
+ args = optional (layout != null) "-layout '${layout}'"
+ ++ optional (variant != null) "-variant '${variant}'"
+ ++ optional (model != null) "-model '${model}'"
+ ++ map (v: "-option '${v}'") options;
+ in "${pkgs.xorg.setxkbmap}/bin/setxkbmap ${toString args}";
+ };
+ };
+ };
+
+ # A basic graphical session target for Home Manager.
+ targets.hm-graphical-session = {
+ Unit = {
+ Description = "Home Manager X session";
+ Requires = [ "graphical-session-pre.target" ];
+ BindsTo = [ "graphical-session.target" ];
+ };
+ };
+ };
+
+ home.file.".xprofile".text = ''
+ . "${config.home.profileDirectory}/etc/profile.d/hm-session-vars.sh"
+
+ if [ -e "$HOME/.profile" ]; then
+ . "$HOME/.profile"
+ fi
+
+ # If there are any running services from a previous session.
+ # Need to run this in xprofile because the NixOS xsession
+ # script starts up graphical-session.target.
+ systemctl --user stop graphical-session.target graphical-session-pre.target
+
+ ${optionalString (cfg.importedVariables != [ ])
+ ("systemctl --user import-environment "
+ + toString (unique cfg.importedVariables))}
+
+ ${cfg.profileExtra}
+
+ export HM_XPROFILE_SOURCED=1
+ '';
+
+ home.file.${cfg.scriptPath} = {
+ executable = true;
+ text = ''
+ if [ -z "$HM_XPROFILE_SOURCED" ]; then
+ . ~/.xprofile
+ fi
+ unset HM_XPROFILE_SOURCED
+
+ systemctl --user start hm-graphical-session.target
+
+ ${cfg.initExtra}
+
+ ${cfg.windowManager.command}
+
+ systemctl --user stop graphical-session.target
+ systemctl --user stop graphical-session-pre.target
+
+ # Wait until the units actually stop.
+ while [ -n "$(systemctl --user --no-legend --state=deactivating list-units)" ]; do
+ sleep 0.5
+ done
+ '';
+ };
+ };
+}
diff --git a/home-manager/nix-darwin/default.nix b/home-manager/nix-darwin/default.nix
new file mode 100644
index 00000000000..24f042a7f0b
--- /dev/null
+++ b/home-manager/nix-darwin/default.nix
@@ -0,0 +1,81 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.home-manager;
+
+ extendedLib = import ../modules/lib/stdlib-extended.nix pkgs.lib;
+
+ hmModule = types.submoduleWith {
+ specialArgs = { lib = extendedLib; };
+ modules = [(
+ {name, ...}: {
+ imports = import ../modules/modules.nix {
+ inherit pkgs;
+ lib = extendedLib;
+ };
+
+ config = {
+ submoduleSupport.enable = true;
+ submoduleSupport.externalPackageInstall = cfg.useUserPackages;
+
+ home.username = config.users.users.${name}.name;
+ home.homeDirectory = config.users.users.${name}.home;
+ };
+ }
+ )];
+ };
+
+in
+
+{
+ options = {
+ home-manager = {
+ useUserPackages = mkEnableOption ''
+ installation of user packages through the
+ <option>users.users.‹name?›.packages</option> option.
+ '';
+
+ users = mkOption {
+ type = types.attrsOf hmModule;
+ default = {};
+ description = ''
+ Per-user Home Manager configuration.
+ '';
+ };
+ };
+ };
+
+ config = mkIf (cfg.users != {}) {
+ warnings =
+ flatten (flip mapAttrsToList cfg.users (user: config:
+ flip map config.warnings (warning:
+ "${user} profile: ${warning}"
+ )
+ ));
+
+ assertions =
+ flatten (flip mapAttrsToList cfg.users (user: config:
+ flip map config.assertions (assertion:
+ {
+ inherit (assertion) assertion;
+ message = "${user} profile: ${assertion.message}";
+ }
+ )
+ ));
+
+ users.users = mkIf cfg.useUserPackages (
+ mapAttrs (username: usercfg: {
+ packages = usercfg.home.packages;
+ }) cfg.users
+ );
+
+ system.activationScripts.postActivation.text =
+ concatStringsSep "\n" (mapAttrsToList (username: usercfg: ''
+ echo Activating home-manager configuration for ${username}
+ sudo -u ${username} -i ${usercfg.home.activationPackage}/activate
+ '') cfg.users);
+ };
+}
diff --git a/home-manager/nixos/default.nix b/home-manager/nixos/default.nix
new file mode 100644
index 00000000000..f4e417bda71
--- /dev/null
+++ b/home-manager/nixos/default.nix
@@ -0,0 +1,110 @@
+{ config, lib, pkgs, utils, ... }:
+
+with lib;
+
+let
+
+ cfg = config.home-manager;
+
+ extendedLib = import ../modules/lib/stdlib-extended.nix pkgs.lib;
+
+ hmModule = types.submoduleWith {
+ specialArgs = { lib = extendedLib; };
+ modules = [
+ ({ name, ... }: {
+ imports = import ../modules/modules.nix {
+ inherit pkgs;
+ lib = extendedLib;
+ };
+
+ config = {
+ submoduleSupport.enable = true;
+ submoduleSupport.externalPackageInstall = cfg.useUserPackages;
+
+ # The per-user directory inside /etc/profiles is not known by
+ # fontconfig by default.
+ fonts.fontconfig.enable = cfg.useUserPackages
+ && config.fonts.fontconfig.enable;
+
+ home.username = config.users.users.${name}.name;
+ home.homeDirectory = config.users.users.${name}.home;
+ };
+ })
+ ];
+ };
+
+ serviceEnvironment = optionalAttrs (cfg.backupFileExtension != null) {
+ HOME_MANAGER_BACKUP_EXT = cfg.backupFileExtension;
+ } // optionalAttrs cfg.verbose { VERBOSE = "1"; };
+
+in {
+ options = {
+ home-manager = {
+ useUserPackages = mkEnableOption ''
+ installation of user packages through the
+ <option>users.users.‹name?›.packages</option> option.
+ '';
+
+ backupFileExtension = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ example = "backup";
+ description = ''
+ On activation move existing files by appending the given
+ file extension rather than exiting with an error.
+ '';
+ };
+
+ verbose = mkEnableOption "verbose output on activation";
+
+ users = mkOption {
+ type = types.attrsOf hmModule;
+ default = { };
+ description = ''
+ Per-user Home Manager configuration.
+ '';
+ };
+ };
+ };
+
+ config = mkIf (cfg.users != { }) {
+ warnings = flatten (flip mapAttrsToList cfg.users (user: config:
+ flip map config.warnings (warning: "${user} profile: ${warning}")));
+
+ assertions = flatten (flip mapAttrsToList cfg.users (user: config:
+ flip map config.assertions (assertion: {
+ inherit (assertion) assertion;
+ message = "${user} profile: ${assertion.message}";
+ })));
+
+ users.users = mkIf cfg.useUserPackages
+ (mapAttrs (username: usercfg: { packages = usercfg.home.packages; })
+ cfg.users);
+
+ systemd.services = mapAttrs' (_: usercfg:
+ let username = usercfg.home.username;
+ in nameValuePair ("home-manager-${utils.escapeSystemdPath username}") {
+ description = "Home Manager environment for ${username}";
+ wantedBy = [ "multi-user.target" ];
+ wants = [ "nix-daemon.socket" ];
+ after = [ "nix-daemon.socket" ];
+
+ environment = serviceEnvironment;
+
+ serviceConfig = {
+ User = usercfg.home.username;
+ Type = "oneshot";
+ RemainAfterExit = "yes";
+ SyslogIdentifier = "hm-activate-${username}";
+
+ # The activation script is run by a login shell to make sure
+ # that the user is given a sane Nix environment.
+ ExecStart = pkgs.writeScript "activate-${username}" ''
+ #! ${pkgs.runtimeShell} -el
+ echo Activating home-manager configuration for ${username}
+ exec ${usercfg.home.activationPackage}/activate
+ '';
+ };
+ }) cfg.users;
+ };
+}
diff --git a/home-manager/overlay.nix b/home-manager/overlay.nix
new file mode 100644
index 00000000000..35136cc8556
--- /dev/null
+++ b/home-manager/overlay.nix
@@ -0,0 +1,3 @@
+self: super: {
+ home-manager = super.callPackage ./home-manager { path = toString ./.; };
+}
diff --git a/home-manager/tests/default.nix b/home-manager/tests/default.nix
new file mode 100644
index 00000000000..49c27239730
--- /dev/null
+++ b/home-manager/tests/default.nix
@@ -0,0 +1,54 @@
+{ pkgs ? import <nixpkgs> {} }:
+
+let
+
+ lib = import ../modules/lib/stdlib-extended.nix pkgs.lib;
+
+ nmt = pkgs.fetchFromGitLab {
+ owner = "rycee";
+ repo = "nmt";
+ rev = "6f866d1acb89fa15cd3b62baa052deae1f685c0c";
+ sha256 = "1qr1shhapjn4nnd4k6hml69ri8vgz4l8lakjll5hc516shs9a9nn";
+ };
+
+ modules = import ../modules/modules.nix {
+ inherit lib pkgs;
+ check = false;
+ };
+
+in
+
+import nmt {
+ inherit lib pkgs modules;
+ testedAttrPath = [ "home" "activationPackage" ];
+ tests = builtins.foldl' (a: b: a // (import b)) { } ([
+ ./lib/types
+ ./modules/files
+ ./modules/home-environment
+ ./modules/misc/fontconfig
+ ./modules/programs/alacritty
+ ./modules/programs/bash
+ ./modules/programs/browserpass
+ ./modules/programs/git
+ ./modules/programs/gpg
+ ./modules/programs/mbsync
+ ./modules/programs/neomutt
+ ./modules/programs/newsboat
+ ./modules/programs/readline
+ ./modules/programs/ssh
+ ./modules/programs/texlive
+ ./modules/programs/tmux
+ ./modules/programs/zsh
+ ./modules/xresources
+ ] ++ lib.optionals pkgs.stdenv.hostPlatform.isLinux [
+ ./modules/misc/pam
+ ./modules/misc/xdg
+ ./modules/misc/xsession
+ ./modules/programs/firefox
+ ./modules/programs/getmail
+ ./modules/programs/rofi
+ ./modules/services/sxhkd
+ ./modules/services/window-managers/i3
+ ./modules/systemd
+ ]);
+}
diff --git a/home-manager/tests/lib/types/dag-merge-result.txt b/home-manager/tests/lib/types/dag-merge-result.txt
new file mode 100644
index 00000000000..9779ef13c0f
--- /dev/null
+++ b/home-manager/tests/lib/types/dag-merge-result.txt
@@ -0,0 +1,3 @@
+before:before
+between:between
+after:after
diff --git a/home-manager/tests/lib/types/dag-merge.nix b/home-manager/tests/lib/types/dag-merge.nix
new file mode 100644
index 00000000000..138a0b64fb7
--- /dev/null
+++ b/home-manager/tests/lib/types/dag-merge.nix
@@ -0,0 +1,32 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ dag = config.lib.dag;
+
+ result = let
+ sorted = dag.topoSort config.tested.dag;
+ data = map (e: "${e.name}:${e.data}") sorted.result;
+ in concatStringsSep "\n" data + "\n";
+
+in {
+ options.tested.dag = mkOption { type = hm.types.dagOf types.str; };
+
+ config = {
+ tested = mkMerge [
+ { dag.after = "after"; }
+ { dag.before = dag.entryBefore [ "after" ] "before"; }
+ { dag.between = dag.entryBetween [ "after" ] [ "before" ] "between"; }
+ ];
+
+ home.file."result.txt".text = result;
+
+ nmt.script = ''
+ assertFileContent \
+ home-files/result.txt \
+ ${./dag-merge-result.txt}
+ '';
+ };
+}
diff --git a/home-manager/tests/lib/types/default.nix b/home-manager/tests/lib/types/default.nix
new file mode 100644
index 00000000000..9fce65f88f0
--- /dev/null
+++ b/home-manager/tests/lib/types/default.nix
@@ -0,0 +1,4 @@
+{
+ lib-types-dag-merge = ./dag-merge.nix;
+ lib-types-list-or-dag-merge = ./list-or-dag-merge.nix;
+}
diff --git a/home-manager/tests/lib/types/list-or-dag-merge-result.txt b/home-manager/tests/lib/types/list-or-dag-merge-result.txt
new file mode 100644
index 00000000000..5fb67a5101c
--- /dev/null
+++ b/home-manager/tests/lib/types/list-or-dag-merge-result.txt
@@ -0,0 +1,15 @@
+before:before
+between:between
+after:after
+unnamed-1.1:k
+unnamed-1.2:l
+unnamed-2.01:a
+unnamed-2.02:b
+unnamed-2.03:c
+unnamed-2.04:d
+unnamed-2.05:e
+unnamed-2.06:f
+unnamed-2.07:g
+unnamed-2.08:h
+unnamed-2.09:i
+unnamed-2.10:j
diff --git a/home-manager/tests/lib/types/list-or-dag-merge.nix b/home-manager/tests/lib/types/list-or-dag-merge.nix
new file mode 100644
index 00000000000..08216140e53
--- /dev/null
+++ b/home-manager/tests/lib/types/list-or-dag-merge.nix
@@ -0,0 +1,34 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ dag = config.lib.dag;
+
+ result = let
+ sorted = dag.topoSort config.tested.dag;
+ data = map (e: "${e.name}:${e.data}") sorted.result;
+ in concatStringsSep "\n" data + "\n";
+
+in {
+ options.tested.dag = mkOption { type = hm.types.listOrDagOf types.str; };
+
+ config = {
+ tested = mkMerge [
+ { dag = [ "k" "l" ]; }
+ { dag = [ "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" ]; }
+ { dag.after = "after"; }
+ { dag.before = dag.entryBefore [ "after" ] "before"; }
+ { dag.between = dag.entryBetween [ "after" ] [ "before" ] "between"; }
+ ];
+
+ home.file."result.txt".text = result;
+
+ nmt.script = ''
+ assertFileContent \
+ home-files/result.txt \
+ ${./list-or-dag-merge-result.txt}
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/accounts/email-test-accounts.nix b/home-manager/tests/modules/accounts/email-test-accounts.nix
new file mode 100644
index 00000000000..9c9c90cf8fe
--- /dev/null
+++ b/home-manager/tests/modules/accounts/email-test-accounts.nix
@@ -0,0 +1,27 @@
+{ ... }:
+
+{
+ accounts.email = {
+ maildirBasePath = "Mail";
+
+ accounts = {
+ "hm@example.com" = {
+ address = "hm@example.com";
+ userName = "home.manager";
+ realName = "H. M. Test";
+ passwordCommand = "password-command";
+ imap.host = "imap.example.com";
+ smtp.host = "smtp.example.com";
+ };
+
+ hm-account = {
+ address = "hm@example.org";
+ userName = "home.manager.jr";
+ realName = "H. M. Test Jr.";
+ passwordCommand = "password-command 2";
+ imap.host = "imap.example.org";
+ smtp.host = "smtp.example.org";
+ };
+ };
+ };
+}
diff --git a/home-manager/tests/modules/files/.hidden b/home-manager/tests/modules/files/.hidden
new file mode 100644
index 00000000000..ca05448e7a0
--- /dev/null
+++ b/home-manager/tests/modules/files/.hidden
@@ -0,0 +1 @@
+The name of this file has a dot prefix.
diff --git a/home-manager/tests/modules/files/default.nix b/home-manager/tests/modules/files/default.nix
new file mode 100644
index 00000000000..04c61d3b886
--- /dev/null
+++ b/home-manager/tests/modules/files/default.nix
@@ -0,0 +1,6 @@
+{
+ files-executable = ./executable.nix;
+ files-hidden-source = ./hidden-source.nix;
+ files-source-with-spaces = ./source-with-spaces.nix;
+ files-text = ./text.nix;
+}
diff --git a/home-manager/tests/modules/files/executable.nix b/home-manager/tests/modules/files/executable.nix
new file mode 100644
index 00000000000..b286c2b499f
--- /dev/null
+++ b/home-manager/tests/modules/files/executable.nix
@@ -0,0 +1,17 @@
+{ config, lib, ... }:
+
+with lib;
+
+{
+ config = {
+ home.file."executable" = {
+ text = "";
+ executable = true;
+ };
+
+ nmt.script = ''
+ assertFileExists home-files/executable
+ assertFileIsExecutable home-files/executable;
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/files/hidden-source.nix b/home-manager/tests/modules/files/hidden-source.nix
new file mode 100644
index 00000000000..8169fedcd7f
--- /dev/null
+++ b/home-manager/tests/modules/files/hidden-source.nix
@@ -0,0 +1,19 @@
+{ config, lib, ... }:
+
+with lib;
+
+{
+ config = {
+ home.file.".hidden".source = ./.hidden;
+
+ nmt.script = ''
+ assertFileExists home-files/.hidden;
+ assertFileContent home-files/.hidden ${
+ builtins.path {
+ path = ./.hidden;
+ name = "expected";
+ }
+ }
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/files/source with spaces! b/home-manager/tests/modules/files/source with spaces!
new file mode 100644
index 00000000000..e1ace404174
--- /dev/null
+++ b/home-manager/tests/modules/files/source with spaces!
@@ -0,0 +1 @@
+Source with spaces!
diff --git a/home-manager/tests/modules/files/source-with-spaces.nix b/home-manager/tests/modules/files/source-with-spaces.nix
new file mode 100644
index 00000000000..1d593c64256
--- /dev/null
+++ b/home-manager/tests/modules/files/source-with-spaces.nix
@@ -0,0 +1,20 @@
+{ config, lib, ... }:
+
+with lib;
+
+{
+ config = {
+ home.file."source with spaces!".source = ./. + "/source with spaces!";
+
+ nmt.script = ''
+ assertFileExists 'home-files/source with spaces!';
+ assertFileContent 'home-files/source with spaces!' \
+ ${
+ builtins.path {
+ path = ./. + "/source with spaces!";
+ name = "source-with-spaces-expected";
+ }
+ }
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/files/text-expected.txt b/home-manager/tests/modules/files/text-expected.txt
new file mode 100644
index 00000000000..b3a0ff2db12
--- /dev/null
+++ b/home-manager/tests/modules/files/text-expected.txt
@@ -0,0 +1,2 @@
+This is the
+expected text.
diff --git a/home-manager/tests/modules/files/text.nix b/home-manager/tests/modules/files/text.nix
new file mode 100644
index 00000000000..6fc9a26fcb4
--- /dev/null
+++ b/home-manager/tests/modules/files/text.nix
@@ -0,0 +1,18 @@
+{ config, lib, ... }:
+
+with lib;
+
+{
+ config = {
+ home.file."using-text".text = ''
+ This is the
+ expected text.
+ '';
+
+ nmt.script = ''
+ assertFileExists home-files/using-text
+ assertFileIsNotExecutable home-files/using-text
+ assertFileContent home-files/using-text ${./text-expected.txt}
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/home-environment/default.nix b/home-manager/tests/modules/home-environment/default.nix
new file mode 100644
index 00000000000..2a1201a2f0a
--- /dev/null
+++ b/home-manager/tests/modules/home-environment/default.nix
@@ -0,0 +1,3 @@
+{
+ home-session-variables = ./session-variables.nix;
+}
diff --git a/home-manager/tests/modules/home-environment/session-variables-expected.txt b/home-manager/tests/modules/home-environment/session-variables-expected.txt
new file mode 100644
index 00000000000..5c3868c3901
--- /dev/null
+++ b/home-manager/tests/modules/home-environment/session-variables-expected.txt
@@ -0,0 +1,6 @@
+# Only source this once.
+if [ -n "$__HM_SESS_VARS_SOURCED" ]; then return; fi
+export __HM_SESS_VARS_SOURCED=1
+
+export V1="v1"
+export V2="v2-v1"
diff --git a/home-manager/tests/modules/home-environment/session-variables.nix b/home-manager/tests/modules/home-environment/session-variables.nix
new file mode 100644
index 00000000000..9f326ebc1b8
--- /dev/null
+++ b/home-manager/tests/modules/home-environment/session-variables.nix
@@ -0,0 +1,19 @@
+{ config, lib, ... }:
+
+with lib;
+
+{
+ config = {
+ home.sessionVariables = {
+ V1 = "v1";
+ V2 = "v2-${config.home.sessionVariables.V1}";
+ };
+
+ nmt.script = ''
+ assertFileExists home-path/etc/profile.d/hm-session-vars.sh
+ assertFileContent \
+ home-path/etc/profile.d/hm-session-vars.sh \
+ ${./session-variables-expected.txt}
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/misc/fontconfig/default.nix b/home-manager/tests/modules/misc/fontconfig/default.nix
new file mode 100644
index 00000000000..b669e1c343c
--- /dev/null
+++ b/home-manager/tests/modules/misc/fontconfig/default.nix
@@ -0,0 +1,5 @@
+{
+ fontconfig-no-font-package = ./no-font-package.nix;
+ fontconfig-single-font-package = ./single-font-package.nix;
+ fontconfig-multiple-font-packages = ./multiple-font-packages.nix;
+}
diff --git a/home-manager/tests/modules/misc/fontconfig/multiple-font-packages.nix b/home-manager/tests/modules/misc/fontconfig/multiple-font-packages.nix
new file mode 100644
index 00000000000..3845b4ba4b1
--- /dev/null
+++ b/home-manager/tests/modules/misc/fontconfig/multiple-font-packages.nix
@@ -0,0 +1,15 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ config = {
+ home.packages = [ pkgs.comic-relief pkgs.unifont ];
+
+ fonts.fontconfig.enable = true;
+
+ nmt.script = ''
+ assertDirectoryNotEmpty home-path/lib/fontconfig/cache
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/misc/fontconfig/no-font-package.nix b/home-manager/tests/modules/misc/fontconfig/no-font-package.nix
new file mode 100644
index 00000000000..c4c687a1320
--- /dev/null
+++ b/home-manager/tests/modules/misc/fontconfig/no-font-package.nix
@@ -0,0 +1,17 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ config = {
+ home.packages = [
+ # Look, no font!
+ ];
+
+ fonts.fontconfig.enable = true;
+
+ nmt.script = ''
+ assertPathNotExists home-path/lib/fontconfig/cache
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/misc/fontconfig/single-font-package.nix b/home-manager/tests/modules/misc/fontconfig/single-font-package.nix
new file mode 100644
index 00000000000..b70bdf8a9a7
--- /dev/null
+++ b/home-manager/tests/modules/misc/fontconfig/single-font-package.nix
@@ -0,0 +1,15 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ config = {
+ home.packages = [ pkgs.comic-relief ];
+
+ fonts.fontconfig.enable = true;
+
+ nmt.script = ''
+ assertDirectoryNotEmpty home-path/lib/fontconfig/cache
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/misc/pam/default.nix b/home-manager/tests/modules/misc/pam/default.nix
new file mode 100644
index 00000000000..81c435e7641
--- /dev/null
+++ b/home-manager/tests/modules/misc/pam/default.nix
@@ -0,0 +1 @@
+{ pam-session-variables = ./session-variables.nix; }
diff --git a/home-manager/tests/modules/misc/pam/session-variables-expected.txt b/home-manager/tests/modules/misc/pam/session-variables-expected.txt
new file mode 100644
index 00000000000..b84a12b7675
--- /dev/null
+++ b/home-manager/tests/modules/misc/pam/session-variables-expected.txt
@@ -0,0 +1,2 @@
+V1 OVERRIDE="v1"
+V2 OVERRIDE="v2-v1"
diff --git a/home-manager/tests/modules/misc/pam/session-variables.nix b/home-manager/tests/modules/misc/pam/session-variables.nix
new file mode 100644
index 00000000000..4fbec4163b5
--- /dev/null
+++ b/home-manager/tests/modules/misc/pam/session-variables.nix
@@ -0,0 +1,19 @@
+{ config, lib, ... }:
+
+with lib;
+
+{
+ config = {
+ pam.sessionVariables = {
+ V1 = "v1";
+ V2 = "v2-${config.pam.sessionVariables.V1}";
+ };
+
+ nmt.script = ''
+ assertFileExists home-files/.pam_environment
+ assertFileContent \
+ home-files/.pam_environment \
+ ${./session-variables-expected.txt}
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/misc/xdg/default.nix b/home-manager/tests/modules/misc/xdg/default.nix
new file mode 100644
index 00000000000..e5d015759d5
--- /dev/null
+++ b/home-manager/tests/modules/misc/xdg/default.nix
@@ -0,0 +1 @@
+{ xdg-mime-apps-basics = ./mime-apps-basics.nix; }
diff --git a/home-manager/tests/modules/misc/xdg/mime-apps-basics-expected.ini b/home-manager/tests/modules/misc/xdg/mime-apps-basics-expected.ini
new file mode 100644
index 00000000000..c27181eb58f
--- /dev/null
+++ b/home-manager/tests/modules/misc/xdg/mime-apps-basics-expected.ini
@@ -0,0 +1,9 @@
+[Added Associations]
+mimetype1=foo1.desktop;foo2.desktop;foo3.desktop
+mimetype2=foo4.desktop
+
+[Default Applications]
+mimetype1=default1.desktop;default2.desktop
+
+[Removed Associations]
+mimetype1=foo5.desktop
diff --git a/home-manager/tests/modules/misc/xdg/mime-apps-basics.nix b/home-manager/tests/modules/misc/xdg/mime-apps-basics.nix
new file mode 100644
index 00000000000..e181e8206f6
--- /dev/null
+++ b/home-manager/tests/modules/misc/xdg/mime-apps-basics.nix
@@ -0,0 +1,28 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ config = {
+ xdg.mimeApps = {
+ enable = true;
+ associations = {
+ added = {
+ "mimetype1" = [ "foo1.desktop" "foo2.desktop" "foo3.desktop" ];
+ "mimetype2" = "foo4.desktop";
+ };
+ removed = { mimetype1 = "foo5.desktop"; };
+ };
+ defaultApplications = {
+ "mimetype1" = [ "default1.desktop" "default2.desktop" ];
+ };
+ };
+
+ nmt.script = ''
+ assertFileExists home-files/.config/mimeapps.list
+ assertFileContent \
+ home-files/.config/mimeapps.list \
+ ${./mime-apps-basics-expected.ini}
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/misc/xsession/basic-setxkbmap-expected.service b/home-manager/tests/modules/misc/xsession/basic-setxkbmap-expected.service
new file mode 100644
index 00000000000..39f876dd60e
--- /dev/null
+++ b/home-manager/tests/modules/misc/xsession/basic-setxkbmap-expected.service
@@ -0,0 +1,12 @@
+[Install]
+WantedBy=graphical-session.target
+
+[Service]
+ExecStart=@setxkbmap@/bin/setxkbmap -layout 'us' -variant ''
+RemainAfterExit=true
+Type=oneshot
+
+[Unit]
+After=graphical-session-pre.target
+Description=Set up keyboard in X
+PartOf=graphical-session.target
diff --git a/home-manager/tests/modules/misc/xsession/basic-xprofile-expected.txt b/home-manager/tests/modules/misc/xsession/basic-xprofile-expected.txt
new file mode 100644
index 00000000000..05733a974ff
--- /dev/null
+++ b/home-manager/tests/modules/misc/xsession/basic-xprofile-expected.txt
@@ -0,0 +1,16 @@
+. "/test-home/.nix-profile/etc/profile.d/hm-session-vars.sh"
+
+if [ -e "$HOME/.profile" ]; then
+ . "$HOME/.profile"
+fi
+
+# If there are any running services from a previous session.
+# Need to run this in xprofile because the NixOS xsession
+# script starts up graphical-session.target.
+systemctl --user stop graphical-session.target graphical-session-pre.target
+
+systemctl --user import-environment DBUS_SESSION_BUS_ADDRESS DISPLAY SSH_AUTH_SOCK XAUTHORITY XDG_DATA_DIRS XDG_RUNTIME_DIR XDG_SESSION_ID EXTRA_IMPORTED_VARIABLE
+
+profile extra commands
+
+export HM_XPROFILE_SOURCED=1
diff --git a/home-manager/tests/modules/misc/xsession/basic-xsession-expected.txt b/home-manager/tests/modules/misc/xsession/basic-xsession-expected.txt
new file mode 100644
index 00000000000..c11b7c33048
--- /dev/null
+++ b/home-manager/tests/modules/misc/xsession/basic-xsession-expected.txt
@@ -0,0 +1,18 @@
+if [ -z "$HM_XPROFILE_SOURCED" ]; then
+ . ~/.xprofile
+fi
+unset HM_XPROFILE_SOURCED
+
+systemctl --user start hm-graphical-session.target
+
+init extra commands
+
+window manager command
+
+systemctl --user stop graphical-session.target
+systemctl --user stop graphical-session-pre.target
+
+# Wait until the units actually stop.
+while [ -n "$(systemctl --user --no-legend --state=deactivating list-units)" ]; do
+ sleep 0.5
+done
diff --git a/home-manager/tests/modules/misc/xsession/basic.nix b/home-manager/tests/modules/misc/xsession/basic.nix
new file mode 100644
index 00000000000..39df6362380
--- /dev/null
+++ b/home-manager/tests/modules/misc/xsession/basic.nix
@@ -0,0 +1,42 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ config = {
+ home.homeDirectory = "/test-home";
+
+ xsession = {
+ enable = true;
+ windowManager.command = "window manager command";
+ importedVariables = [ "EXTRA_IMPORTED_VARIABLE" ];
+ initExtra = "init extra commands";
+ profileExtra = "profile extra commands";
+ };
+
+ nixpkgs.overlays = [
+ (self: super: {
+ xorg = super.xorg // {
+ setxkbmap = super.xorg.setxkbmap // { outPath = "@setxkbmap@"; };
+ };
+ })
+ ];
+
+ nmt.script = ''
+ assertFileExists home-files/.xprofile
+ assertFileContent \
+ home-files/.xprofile \
+ ${./basic-xprofile-expected.txt}
+
+ assertFileExists home-files/.xsession
+ assertFileContent \
+ home-files/.xsession \
+ ${./basic-xsession-expected.txt}
+
+ assertFileExists home-files/.config/systemd/user/setxkbmap.service
+ assertFileContent \
+ home-files/.config/systemd/user/setxkbmap.service \
+ ${./basic-setxkbmap-expected.service}
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/misc/xsession/default.nix b/home-manager/tests/modules/misc/xsession/default.nix
new file mode 100644
index 00000000000..2ddbf47efac
--- /dev/null
+++ b/home-manager/tests/modules/misc/xsession/default.nix
@@ -0,0 +1,4 @@
+{
+ xsession-basic = ./basic.nix;
+ xsession-keyboard-without-layout = ./keyboard-without-layout.nix;
+}
diff --git a/home-manager/tests/modules/misc/xsession/keyboard-without-layout-expected.service b/home-manager/tests/modules/misc/xsession/keyboard-without-layout-expected.service
new file mode 100644
index 00000000000..a04af53dad7
--- /dev/null
+++ b/home-manager/tests/modules/misc/xsession/keyboard-without-layout-expected.service
@@ -0,0 +1,12 @@
+[Install]
+WantedBy=graphical-session.target
+
+[Service]
+ExecStart=@setxkbmap@/bin/setxkbmap -option 'ctrl:nocaps' -option 'altwin:no_win'
+RemainAfterExit=true
+Type=oneshot
+
+[Unit]
+After=graphical-session-pre.target
+Description=Set up keyboard in X
+PartOf=graphical-session.target
diff --git a/home-manager/tests/modules/misc/xsession/keyboard-without-layout.nix b/home-manager/tests/modules/misc/xsession/keyboard-without-layout.nix
new file mode 100644
index 00000000000..015efe6154a
--- /dev/null
+++ b/home-manager/tests/modules/misc/xsession/keyboard-without-layout.nix
@@ -0,0 +1,36 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ config = {
+ home.stateVersion = "19.09";
+
+ home.homeDirectory = "/test-home";
+
+ home.keyboard = { options = [ "ctrl:nocaps" "altwin:no_win" ]; };
+
+ xsession = {
+ enable = true;
+ windowManager.command = "window manager command";
+ importedVariables = [ "EXTRA_IMPORTED_VARIABLE" ];
+ initExtra = "init extra commands";
+ profileExtra = "profile extra commands";
+ };
+
+ nixpkgs.overlays = [
+ (self: super: {
+ xorg = super.xorg // {
+ setxkbmap = super.xorg.setxkbmap // { outPath = "@setxkbmap@"; };
+ };
+ })
+ ];
+
+ nmt.script = ''
+ assertFileExists home-files/.config/systemd/user/setxkbmap.service
+ assertFileContent \
+ home-files/.config/systemd/user/setxkbmap.service \
+ ${./keyboard-without-layout-expected.service}
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/programs/alacritty/default.nix b/home-manager/tests/modules/programs/alacritty/default.nix
new file mode 100644
index 00000000000..f63e033d846
--- /dev/null
+++ b/home-manager/tests/modules/programs/alacritty/default.nix
@@ -0,0 +1,4 @@
+{
+ alacritty-example-settings = ./example-settings.nix;
+ alacritty-empty-settings = ./empty-settings.nix;
+}
diff --git a/home-manager/tests/modules/programs/alacritty/empty-settings.nix b/home-manager/tests/modules/programs/alacritty/empty-settings.nix
new file mode 100644
index 00000000000..65470473c1a
--- /dev/null
+++ b/home-manager/tests/modules/programs/alacritty/empty-settings.nix
@@ -0,0 +1,17 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ config = {
+ programs.alacritty.enable = true;
+
+ nixpkgs.overlays = [
+ (self: super: { alacritty = pkgs.writeScriptBin "dummy-alacritty" ""; })
+ ];
+
+ nmt.script = ''
+ assertPathNotExists home-files/.config/alacritty
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/programs/alacritty/example-settings-expected.yml b/home-manager/tests/modules/programs/alacritty/example-settings-expected.yml
new file mode 100644
index 00000000000..061624192c3
--- /dev/null
+++ b/home-manager/tests/modules/programs/alacritty/example-settings-expected.yml
@@ -0,0 +1 @@
+{"key_bindings":[{"chars":"\x0c","key":"K","mods":"Control"}],"window":{"dimensions":{"columns":200,"lines":3}}} \ No newline at end of file
diff --git a/home-manager/tests/modules/programs/alacritty/example-settings.nix b/home-manager/tests/modules/programs/alacritty/example-settings.nix
new file mode 100644
index 00000000000..46be1064ce6
--- /dev/null
+++ b/home-manager/tests/modules/programs/alacritty/example-settings.nix
@@ -0,0 +1,34 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ config = {
+ programs.alacritty = {
+ enable = true;
+
+ settings = {
+ window.dimensions = {
+ lines = 3;
+ columns = 200;
+ };
+
+ key_bindings = [{
+ key = "K";
+ mods = "Control";
+ chars = "\\x0c";
+ }];
+ };
+ };
+
+ nixpkgs.overlays = [
+ (self: super: { alacritty = pkgs.writeScriptBin "dummy-alacritty" ""; })
+ ];
+
+ nmt.script = ''
+ assertFileContent \
+ home-files/.config/alacritty/alacritty.yml \
+ ${./example-settings-expected.yml}
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/programs/bash/default.nix b/home-manager/tests/modules/programs/bash/default.nix
new file mode 100644
index 00000000000..e9f431cd2b9
--- /dev/null
+++ b/home-manager/tests/modules/programs/bash/default.nix
@@ -0,0 +1,4 @@
+{
+ bash-logout = ./logout.nix;
+ bash-session-variables = ./session-variables.nix;
+}
diff --git a/home-manager/tests/modules/programs/bash/logout-expected.txt b/home-manager/tests/modules/programs/bash/logout-expected.txt
new file mode 100644
index 00000000000..9462f58f732
--- /dev/null
+++ b/home-manager/tests/modules/programs/bash/logout-expected.txt
@@ -0,0 +1,4 @@
+# -*- mode: sh -*-
+
+clear-console
+
diff --git a/home-manager/tests/modules/programs/bash/logout.nix b/home-manager/tests/modules/programs/bash/logout.nix
new file mode 100644
index 00000000000..8f96dc7e1ae
--- /dev/null
+++ b/home-manager/tests/modules/programs/bash/logout.nix
@@ -0,0 +1,22 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ config = {
+ programs.bash = {
+ enable = true;
+
+ logoutExtra = ''
+ clear-console
+ '';
+ };
+
+ nmt.script = ''
+ assertFileExists home-files/.bash_logout
+ assertFileContent \
+ home-files/.bash_logout \
+ ${./logout-expected.txt}
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/programs/bash/session-variables-expected.txt b/home-manager/tests/modules/programs/bash/session-variables-expected.txt
new file mode 100644
index 00000000000..e13d63d4c78
--- /dev/null
+++ b/home-manager/tests/modules/programs/bash/session-variables-expected.txt
@@ -0,0 +1,8 @@
+# -*- mode: sh -*-
+
+. "/home/testuser/.nix-profile/etc/profile.d/hm-session-vars.sh"
+
+export V1="v1"
+export V2="v2-v1"
+
+
diff --git a/home-manager/tests/modules/programs/bash/session-variables.nix b/home-manager/tests/modules/programs/bash/session-variables.nix
new file mode 100644
index 00000000000..ea789a1d061
--- /dev/null
+++ b/home-manager/tests/modules/programs/bash/session-variables.nix
@@ -0,0 +1,25 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ config = {
+ programs.bash = {
+ enable = true;
+
+ sessionVariables = {
+ V1 = "v1";
+ V2 = "v2-${config.programs.bash.sessionVariables.V1}";
+ };
+ };
+
+ home.homeDirectory = "/home/testuser";
+
+ nmt.script = ''
+ assertFileExists home-files/.profile
+ assertFileContent \
+ home-files/.profile \
+ ${./session-variables-expected.txt}
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/programs/browserpass/browserpass.nix b/home-manager/tests/modules/programs/browserpass/browserpass.nix
new file mode 100644
index 00000000000..9189a445ac0
--- /dev/null
+++ b/home-manager/tests/modules/programs/browserpass/browserpass.nix
@@ -0,0 +1,29 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ config = {
+ programs.browserpass = {
+ enable = true;
+ browsers = [ "chrome" "chromium" "firefox" "vivaldi" ];
+ };
+
+ nmt.script = if pkgs.stdenv.hostPlatform.isDarwin then ''
+ for dir in "Google/Chrome" "Chromium" "Mozilla" "Vivaldi"; do
+ assertFileExists "home-files/Library/Application Support/$dir/NativeMessagingHosts/com.github.browserpass.native.json"
+ done
+
+ for dir in "Google/Chrome" "Chromium" "Vivaldi"; do
+ assertFileExists "home-files/Library/Application Support/$dir/policies/managed/com.github.browserpass.native.json"
+ done
+ '' else ''
+ for dir in "google-chrome" "chromium" "vivaldi"; do
+ assertFileExists "home-files/.config/$dir/NativeMessagingHosts/com.github.browserpass.native.json"
+ assertFileExists "home-files/.config/$dir/policies/managed/com.github.browserpass.native.json"
+ done
+
+ assertFileExists "home-files/.mozilla/native-messaging-hosts/com.github.browserpass.native.json"
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/programs/browserpass/default.nix b/home-manager/tests/modules/programs/browserpass/default.nix
new file mode 100644
index 00000000000..fa40ddcab31
--- /dev/null
+++ b/home-manager/tests/modules/programs/browserpass/default.nix
@@ -0,0 +1 @@
+{ browserpass = ./browserpass.nix; }
diff --git a/home-manager/tests/modules/programs/firefox/default.nix b/home-manager/tests/modules/programs/firefox/default.nix
new file mode 100644
index 00000000000..6612a9ac978
--- /dev/null
+++ b/home-manager/tests/modules/programs/firefox/default.nix
@@ -0,0 +1,4 @@
+{
+ firefox-profile-settings = ./profile-settings.nix;
+ firefox-state-version-19_09 = ./state-version-19_09.nix;
+}
diff --git a/home-manager/tests/modules/programs/firefox/profile-settings-expected-user.js b/home-manager/tests/modules/programs/firefox/profile-settings-expected-user.js
new file mode 100644
index 00000000000..0edd47b9101
--- /dev/null
+++ b/home-manager/tests/modules/programs/firefox/profile-settings-expected-user.js
@@ -0,0 +1,6 @@
+// Generated by Home Manager.
+
+user_pref("general.smoothScroll", false);
+
+
+
diff --git a/home-manager/tests/modules/programs/firefox/profile-settings.nix b/home-manager/tests/modules/programs/firefox/profile-settings.nix
new file mode 100644
index 00000000000..8c5fb4ec1fc
--- /dev/null
+++ b/home-manager/tests/modules/programs/firefox/profile-settings.nix
@@ -0,0 +1,36 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ config = {
+ programs.firefox = {
+ enable = true;
+ profiles.test.settings = { "general.smoothScroll" = false; };
+ };
+
+ nixpkgs.overlays = [
+ (self: super: {
+ firefox-unwrapped = pkgs.runCommand "firefox-0" {
+ meta.description = "I pretend to be Firefox";
+ preferLocalBuild = true;
+ allowSubstitutes = false;
+ } ''
+ mkdir -p "$out/bin"
+ touch "$out/bin/firefox"
+ chmod 755 "$out/bin/firefox"
+ '';
+ })
+ ];
+
+ nmt.script = ''
+ assertFileRegex \
+ home-path/bin/firefox \
+ MOZ_APP_LAUNCHER
+
+ assertFileContent \
+ home-files/.mozilla/firefox/test/user.js \
+ ${./profile-settings-expected-user.js}
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/programs/firefox/state-version-19_09.nix b/home-manager/tests/modules/programs/firefox/state-version-19_09.nix
new file mode 100644
index 00000000000..27dc867ad29
--- /dev/null
+++ b/home-manager/tests/modules/programs/firefox/state-version-19_09.nix
@@ -0,0 +1,31 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ config = {
+ home.stateVersion = "19.09";
+
+ programs.firefox.enable = true;
+
+ nixpkgs.overlays = [
+ (self: super: {
+ firefox-unwrapped = pkgs.runCommand "firefox-0" {
+ meta.description = "I pretend to be Firefox";
+ preferLocalBuild = true;
+ allowSubstitutes = false;
+ } ''
+ mkdir -p "$out/bin"
+ touch "$out/bin/firefox"
+ chmod 755 "$out/bin/firefox"
+ '';
+ })
+ ];
+
+ nmt.script = ''
+ assertFileRegex \
+ home-path/bin/firefox \
+ MOZ_APP_LAUNCHER
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/programs/getmail/default.nix b/home-manager/tests/modules/programs/getmail/default.nix
new file mode 100644
index 00000000000..cb789a90d64
--- /dev/null
+++ b/home-manager/tests/modules/programs/getmail/default.nix
@@ -0,0 +1 @@
+{ getmail = ./getmail.nix; }
diff --git a/home-manager/tests/modules/programs/getmail/getmail-expected.conf b/home-manager/tests/modules/programs/getmail/getmail-expected.conf
new file mode 100644
index 00000000000..90dc963e574
--- /dev/null
+++ b/home-manager/tests/modules/programs/getmail/getmail-expected.conf
@@ -0,0 +1,16 @@
+# Generated by Home-Manager.
+[retriever]
+type = SimpleIMAPSSLRetriever
+server = imap.example.com
+port = 993
+username = home.manager
+password_command = ('password-command')
+mailboxes = ( 'INBOX', 'Sent', 'Work' )
+
+[destination]
+type = MDA_external
+path = /bin/maildrop
+
+[options]
+delete = false
+read_all = true
diff --git a/home-manager/tests/modules/programs/getmail/getmail.nix b/home-manager/tests/modules/programs/getmail/getmail.nix
new file mode 100644
index 00000000000..fe10b98f981
--- /dev/null
+++ b/home-manager/tests/modules/programs/getmail/getmail.nix
@@ -0,0 +1,31 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ imports = [ ../../accounts/email-test-accounts.nix ];
+
+ config = {
+ home.username = "hm-user";
+ home.homeDirectory = "/home/hm-user";
+
+ accounts.email.accounts = {
+ "hm@example.com" = {
+ getmail = {
+ enable = true;
+ mailboxes = [ "INBOX" "Sent" "Work" ];
+ destinationCommand = "/bin/maildrop";
+ delete = false;
+ };
+ imap.port = 993;
+ };
+ };
+
+ nmt.script = ''
+ assertFileExists home-files/.getmail/getmailhm@example.com
+ assertFileContent home-files/.getmail/getmailhm@example.com ${
+ ./getmail-expected.conf
+ }
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/programs/git/default.nix b/home-manager/tests/modules/programs/git/default.nix
new file mode 100644
index 00000000000..45aface8d26
--- /dev/null
+++ b/home-manager/tests/modules/programs/git/default.nix
@@ -0,0 +1,5 @@
+{
+ git-with-email = ./git-with-email.nix;
+ git-with-most-options = ./git.nix;
+ git-with-str-extra-config = ./git-with-str-extra-config.nix;
+}
diff --git a/home-manager/tests/modules/programs/git/git-expected.conf b/home-manager/tests/modules/programs/git/git-expected.conf
new file mode 100644
index 00000000000..d02ebf31649
--- /dev/null
+++ b/home-manager/tests/modules/programs/git/git-expected.conf
@@ -0,0 +1,42 @@
+[alias]
+a1=foo
+a2=baz
+
+[commit]
+gpgSign=true
+
+[extra]
+boolean=true
+integer=38
+multiple=1
+multiple=2
+name=value
+
+[extra "backcompat.with.dots"]
+previously=worked
+
+[extra "subsection"]
+value=test
+
+[filter "lfs"]
+clean=git-lfs clean -- %f
+process=git-lfs filter-process
+required=true
+smudge=git-lfs smudge -- %f
+
+[gpg]
+program=path-to-gpg
+
+[user]
+email=user@example.org
+name=John Doe
+signingKey=00112233445566778899AABBCCDDEEFF
+
+[include]
+path=~/path/to/config.inc
+
+[includeIf "gitdir:~/src/dir"]
+path=~/path/to/conditional.inc
+
+[includeIf "gitdir:~/src/dir"]
+path=@git_include_path@
diff --git a/home-manager/tests/modules/programs/git/git-with-email-expected.conf b/home-manager/tests/modules/programs/git/git-with-email-expected.conf
new file mode 100644
index 00000000000..01c1eec5823
--- /dev/null
+++ b/home-manager/tests/modules/programs/git/git-with-email-expected.conf
@@ -0,0 +1,15 @@
+[sendemail "hm-account"]
+from=hm@example.org
+smtpEncryption=tls
+smtpServer=smtp.example.org
+smtpUser=home.manager.jr
+
+[sendemail "hm@example.com"]
+from=hm@example.com
+smtpEncryption=tls
+smtpServer=smtp.example.com
+smtpUser=home.manager
+
+[user]
+email=hm@example.com
+name=H. M. Test
diff --git a/home-manager/tests/modules/programs/git/git-with-email.nix b/home-manager/tests/modules/programs/git/git-with-email.nix
new file mode 100644
index 00000000000..ca577eef4d3
--- /dev/null
+++ b/home-manager/tests/modules/programs/git/git-with-email.nix
@@ -0,0 +1,36 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ imports = [ ../../accounts/email-test-accounts.nix ];
+
+ config = {
+ programs.git = {
+ enable = true;
+ package = pkgs.gitMinimal;
+ userEmail = "hm@example.com";
+ userName = "H. M. Test";
+ };
+
+ nmt.script = ''
+ function assertGitConfig() {
+ local value
+ value=$(${pkgs.git}/bin/git config \
+ --file $TESTED/home-files/.config/git/config \
+ --get $1)
+ if [[ $value != $2 ]]; then
+ fail "Expected option '$1' to have value '$2' but it was '$value'"
+ fi
+ }
+
+ assertFileExists home-files/.config/git/config
+ assertFileContent home-files/.config/git/config ${
+ ./git-with-email-expected.conf
+ }
+
+ assertGitConfig "sendemail.hm@example.com.from" "hm@example.com"
+ assertGitConfig "sendemail.hm-account.from" "hm@example.org"
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/programs/git/git-with-str-extra-config-expected.conf b/home-manager/tests/modules/programs/git/git-with-str-extra-config-expected.conf
new file mode 100644
index 00000000000..957438de13a
--- /dev/null
+++ b/home-manager/tests/modules/programs/git/git-with-str-extra-config-expected.conf
@@ -0,0 +1,5 @@
+This can be anything.
+
+[user]
+email=user@example.org
+name=John Doe
diff --git a/home-manager/tests/modules/programs/git/git-with-str-extra-config.nix b/home-manager/tests/modules/programs/git/git-with-str-extra-config.nix
new file mode 100644
index 00000000000..3dbc497a5ea
--- /dev/null
+++ b/home-manager/tests/modules/programs/git/git-with-str-extra-config.nix
@@ -0,0 +1,23 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ config = {
+ programs.git = {
+ enable = true;
+ package = pkgs.gitMinimal;
+ extraConfig = ''
+ This can be anything.
+ '';
+ userEmail = "user@example.org";
+ userName = "John Doe";
+ };
+
+ nmt.script = ''
+ assertFileExists home-files/.config/git/config
+ assertFileContent home-files/.config/git/config \
+ ${./git-with-str-extra-config-expected.conf}
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/programs/git/git.nix b/home-manager/tests/modules/programs/git/git.nix
new file mode 100644
index 00000000000..7c0bf52de55
--- /dev/null
+++ b/home-manager/tests/modules/programs/git/git.nix
@@ -0,0 +1,77 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ gitInclude = {
+ user = {
+ name = "John Doe";
+ email = "user@example.org";
+ };
+ };
+
+ substituteExpected = path:
+ pkgs.substituteAll {
+ src = path;
+
+ git_include_path =
+ pkgs.writeText "contents" (generators.toINI { } gitInclude);
+ };
+
+in {
+ config = {
+ programs.git = mkMerge [
+ {
+ enable = true;
+ package = pkgs.gitMinimal;
+ aliases = {
+ a1 = "foo";
+ a2 = "bar";
+ };
+ extraConfig = {
+ extra = {
+ name = "value";
+ multiple = [ 1 ];
+ };
+ };
+ ignores = [ "*~" "*.swp" ];
+ includes = [
+ { path = "~/path/to/config.inc"; }
+ {
+ path = "~/path/to/conditional.inc";
+ condition = "gitdir:~/src/dir";
+ }
+ {
+ condition = "gitdir:~/src/dir";
+ contents = gitInclude;
+ }
+ ];
+ signing = {
+ gpgPath = "path-to-gpg";
+ key = "00112233445566778899AABBCCDDEEFF";
+ signByDefault = true;
+ };
+ userEmail = "user@example.org";
+ userName = "John Doe";
+ lfs.enable = true;
+ }
+
+ {
+ aliases.a2 = mkForce "baz";
+ extraConfig."extra \"backcompat.with.dots\"".previously = "worked";
+ extraConfig.extra.boolean = true;
+ extraConfig.extra.integer = 38;
+ extraConfig.extra.multiple = [ 2 ];
+ extraConfig.extra.subsection.value = "test";
+ }
+ ];
+
+ nmt.script = ''
+ assertFileExists home-files/.config/git/config
+ assertFileContent home-files/.config/git/config ${
+ substituteExpected ./git-expected.conf
+ }
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/programs/gpg/default.nix b/home-manager/tests/modules/programs/gpg/default.nix
new file mode 100644
index 00000000000..7fed2cdcc69
--- /dev/null
+++ b/home-manager/tests/modules/programs/gpg/default.nix
@@ -0,0 +1 @@
+{ gpg-override-defaults = ./override-defaults.nix; }
diff --git a/home-manager/tests/modules/programs/gpg/override-defaults-expected.conf b/home-manager/tests/modules/programs/gpg/override-defaults-expected.conf
new file mode 100644
index 00000000000..3198183f723
--- /dev/null
+++ b/home-manager/tests/modules/programs/gpg/override-defaults-expected.conf
@@ -0,0 +1,19 @@
+cert-digest-algo SHA512
+charset utf-8
+default-preference-list SHA512 SHA384 SHA256 AES256 AES192 AES ZLIB BZIP2 ZIP Uncompressed
+fixed-list-mode
+keyid-format 0xlong
+list-options show-uid-validity
+
+no-emit-version
+no-symkey-cache
+personal-cipher-preferences AES256 AES192 AES
+personal-compress-preferences ZLIB BZIP2 ZIP Uncompressed
+personal-digest-preferences SHA512 SHA384 SHA256
+require-cross-certification
+s2k-cipher-algo AES128
+s2k-digest-algo SHA512
+throw-keyids
+use-agent
+verify-options show-uid-validity
+with-fingerprint \ No newline at end of file
diff --git a/home-manager/tests/modules/programs/gpg/override-defaults.nix b/home-manager/tests/modules/programs/gpg/override-defaults.nix
new file mode 100644
index 00000000000..850334dc589
--- /dev/null
+++ b/home-manager/tests/modules/programs/gpg/override-defaults.nix
@@ -0,0 +1,22 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ config = {
+ programs.gpg = {
+ enable = true;
+
+ settings = {
+ no-comments = false;
+ s2k-cipher-algo = "AES128";
+ throw-keyids = true;
+ };
+ };
+
+ nmt.script = ''
+ assertFileExists home-files/.gnupg/gpg.conf
+ assertFileContent home-files/.gnupg/gpg.conf ${./override-defaults-expected.conf}
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/programs/mbsync/default.nix b/home-manager/tests/modules/programs/mbsync/default.nix
new file mode 100644
index 00000000000..9c369fa5018
--- /dev/null
+++ b/home-manager/tests/modules/programs/mbsync/default.nix
@@ -0,0 +1 @@
+{ mbsync = ./mbsync.nix; }
diff --git a/home-manager/tests/modules/programs/mbsync/mbsync-expected.conf b/home-manager/tests/modules/programs/mbsync/mbsync-expected.conf
new file mode 100644
index 00000000000..f1ca79fe738
--- /dev/null
+++ b/home-manager/tests/modules/programs/mbsync/mbsync-expected.conf
@@ -0,0 +1,55 @@
+# Generated by Home Manager.
+
+IMAPAccount hm-account
+CertificateFile /etc/ssl/certs/ca-certificates.crt
+Host imap.example.org
+PassCmd "password-command 2"
+SSLType IMAPS
+User home.manager.jr
+
+IMAPStore hm-account-remote
+Account hm-account
+
+MaildirStore hm-account-local
+Inbox /home/hm-user/Mail/hm-account/Inbox
+Path /home/hm-user/Mail/hm-account/
+SubFolders Verbatim
+
+Channel hm-account
+Create None
+Expunge None
+Master :hm-account-remote:
+Patterns *
+Remove None
+Slave :hm-account-local:
+SyncState *
+
+
+IMAPAccount hm@example.com
+CertificateFile /etc/ssl/certs/ca-certificates.crt
+Host imap.example.com
+PassCmd password-command
+SSLType IMAPS
+User home.manager
+
+IMAPStore hm@example.com-remote
+Account hm@example.com
+
+MaildirStore hm@example.com-local
+Inbox /home/hm-user/Mail/hm@example.com/Inbox
+Path /home/hm-user/Mail/hm@example.com/
+SubFolders Verbatim
+
+Channel hm@example.com
+Create None
+Expunge None
+Master :hm@example.com-remote:
+Patterns *
+Remove None
+Slave :hm@example.com-local:
+SyncState *
+
+
+Group inboxes
+Channel hm-account:Inbox
+Channel hm@example.com:Inbox1,Inbox2
diff --git a/home-manager/tests/modules/programs/mbsync/mbsync.nix b/home-manager/tests/modules/programs/mbsync/mbsync.nix
new file mode 100644
index 00000000000..03a54c178f5
--- /dev/null
+++ b/home-manager/tests/modules/programs/mbsync/mbsync.nix
@@ -0,0 +1,31 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ imports = [ ../../accounts/email-test-accounts.nix ];
+
+ config = {
+ home.username = "hm-user";
+ home.homeDirectory = "/home/hm-user";
+
+ programs.mbsync = {
+ enable = true;
+ groups.inboxes = {
+ "hm@example.com" = [ "Inbox1" "Inbox2" ];
+ hm-account = [ "Inbox" ];
+ };
+ };
+
+ accounts.email.accounts = {
+ "hm@example.com".mbsync = { enable = true; };
+
+ hm-account.mbsync = { enable = true; };
+ };
+
+ nmt.script = ''
+ assertFileExists home-files/.mbsyncrc
+ assertFileContent home-files/.mbsyncrc ${./mbsync-expected.conf}
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/programs/neomutt/default.nix b/home-manager/tests/modules/programs/neomutt/default.nix
new file mode 100644
index 00000000000..289f2705efa
--- /dev/null
+++ b/home-manager/tests/modules/programs/neomutt/default.nix
@@ -0,0 +1 @@
+{ neomutt-simple = ./neomutt.nix; }
diff --git a/home-manager/tests/modules/programs/neomutt/hm-example.com-expected b/home-manager/tests/modules/programs/neomutt/hm-example.com-expected
new file mode 100644
index 00000000000..430509c36bd
--- /dev/null
+++ b/home-manager/tests/modules/programs/neomutt/hm-example.com-expected
@@ -0,0 +1,37 @@
+# Generated by Home Manager.
+set ssl_force_tls = yes
+set certificate_file=/etc/ssl/certs/ca-certificates.crt
+
+# GPG section
+set crypt_use_gpgme = yes
+set crypt_autosign = no
+set pgp_use_gpg_agent = yes
+set mbox_type = Maildir
+set sort = "threads"
+
+# MTA section
+set smtp_pass='`password-command`'
+set smtp_url='smtps://home.manager@smtp.example.com'
+
+
+
+
+
+# MRA section
+set folder='/home/hm-user/Mail/hm@example.com'
+set from='hm@example.com'
+set postponed='+Drafts'
+set realname='H. M. Test'
+set record='+Sent'
+set spoolfile='+Inbox'
+set trash='+Trash'
+color status cyan default
+
+
+
+# Extra configuration
+color status cyan default
+
+# notmuch section
+set nm_default_uri = "notmuch:///home/hm-user/Mail"
+virtual-mailboxes "My INBOX" "notmuch://?query=tag:inbox"
diff --git a/home-manager/tests/modules/programs/neomutt/neomutt-expected.conf b/home-manager/tests/modules/programs/neomutt/neomutt-expected.conf
new file mode 100644
index 00000000000..7711aa5a652
--- /dev/null
+++ b/home-manager/tests/modules/programs/neomutt/neomutt-expected.conf
@@ -0,0 +1,27 @@
+# Generated by Home Manager.
+set header_cache = "/home/hm-user/.cache/neomutt/headers/"
+set message_cachedir = "/home/hm-user/.cache/neomutt/messages/"
+set editor = "$EDITOR"
+set implicit_autoview = yes
+
+alternative_order text/enriched text/plain text
+
+set delete = yes
+
+# Binds
+
+
+# Macros
+
+
+
+
+# Extra configuration
+
+
+
+# register account hm@example.com
+mailboxes "/home/hm-user/Mail/hm@example.com/Inbox"
+folder-hook /home/hm-user/Mail/hm@example.com/ " \
+ source /home/hm-user/.config/neomutt/hm@example.com "
+source /home/hm-user/.config/neomutt/hm@example.com \ No newline at end of file
diff --git a/home-manager/tests/modules/programs/neomutt/neomutt.nix b/home-manager/tests/modules/programs/neomutt/neomutt.nix
new file mode 100644
index 00000000000..91cb9dca249
--- /dev/null
+++ b/home-manager/tests/modules/programs/neomutt/neomutt.nix
@@ -0,0 +1,47 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ imports = [ ../../accounts/email-test-accounts.nix ];
+
+ config = {
+ home.username = "hm-user";
+ home.homeDirectory = "/home/hm-user";
+ xdg.configHome = mkForce "/home/hm-user/.config";
+ xdg.cacheHome = mkForce "/home/hm-user/.cache";
+
+ accounts.email.accounts = {
+ "hm@example.com" = {
+ primary = true;
+ notmuch.enable = true;
+ neomutt = {
+ enable = true;
+ extraConfig = ''
+ color status cyan default
+ '';
+ };
+ imap.port = 993;
+ };
+ };
+
+ programs.neomutt = {
+ enable = true;
+ vimKeys = false;
+ };
+
+ nixpkgs.overlays =
+ [ (self: super: { neomutt = pkgs.writeScriptBin "dummy-neomutt" ""; }) ];
+
+ nmt.script = ''
+ assertFileExists home-files/.config/neomutt/neomuttrc
+ assertFileExists home-files/.config/neomutt/hm@example.com
+ assertFileContent home-files/.config/neomutt/neomuttrc ${
+ ./neomutt-expected.conf
+ }
+ assertFileContent home-files/.config/neomutt/hm@example.com ${
+ ./hm-example.com-expected
+ }
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/programs/newsboat/default.nix b/home-manager/tests/modules/programs/newsboat/default.nix
new file mode 100644
index 00000000000..27f523a629c
--- /dev/null
+++ b/home-manager/tests/modules/programs/newsboat/default.nix
@@ -0,0 +1 @@
+{ newsboat-basics = ./newsboat-basics.nix; }
diff --git a/home-manager/tests/modules/programs/newsboat/newsboat-basics-urls.txt b/home-manager/tests/modules/programs/newsboat/newsboat-basics-urls.txt
new file mode 100644
index 00000000000..7f084961345
--- /dev/null
+++ b/home-manager/tests/modules/programs/newsboat/newsboat-basics-urls.txt
@@ -0,0 +1,3 @@
+http://example.org/feed.xml "tag1" "tag2" "~Cool feed"
+http://example.org/feed2.xml
+"query:foo:rssurl =~ \"example.com\""
diff --git a/home-manager/tests/modules/programs/newsboat/newsboat-basics.nix b/home-manager/tests/modules/programs/newsboat/newsboat-basics.nix
new file mode 100644
index 00000000000..e6eb4151776
--- /dev/null
+++ b/home-manager/tests/modules/programs/newsboat/newsboat-basics.nix
@@ -0,0 +1,33 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ config = {
+ programs.newsboat = {
+ enable = true;
+
+ urls = [
+ {
+ url = "http://example.org/feed.xml";
+ tags = [ "tag1" "tag2" ];
+ title = "Cool feed";
+ }
+
+ { url = "http://example.org/feed2.xml"; }
+ ];
+
+ queries = { "foo" = ''rssurl =~ "example.com"''; };
+ };
+
+ nixpkgs.overlays = [
+ (self: super: { newsboat = pkgs.writeScriptBin "dummy-newsboat" ""; })
+ ];
+
+ nmt.script = ''
+ assertFileContent \
+ home-files/.newsboat/urls \
+ ${./newsboat-basics-urls.txt}
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/programs/readline/default.nix b/home-manager/tests/modules/programs/readline/default.nix
new file mode 100644
index 00000000000..c95745d19cd
--- /dev/null
+++ b/home-manager/tests/modules/programs/readline/default.nix
@@ -0,0 +1 @@
+{ readline-using-all-options = ./using-all-options.nix; }
diff --git a/home-manager/tests/modules/programs/readline/using-all-options.nix b/home-manager/tests/modules/programs/readline/using-all-options.nix
new file mode 100644
index 00000000000..ab851020c2e
--- /dev/null
+++ b/home-manager/tests/modules/programs/readline/using-all-options.nix
@@ -0,0 +1,31 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ config = {
+ programs.readline = {
+ enable = true;
+
+ bindings = { "\\C-h" = "backward-kill-word"; };
+
+ variables = {
+ bell-style = "audible";
+ completion-map-case = true;
+ completion-prefix-display-length = 2;
+ };
+
+ extraConfig = ''
+ $if mode=emacs
+ "\e[1~": beginning-of-line
+ $endif
+ '';
+ };
+
+ nmt.script = ''
+ assertFileContent \
+ home-files/.inputrc \
+ ${./using-all-options.txt}
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/programs/readline/using-all-options.txt b/home-manager/tests/modules/programs/readline/using-all-options.txt
new file mode 100644
index 00000000000..6b4aef51e69
--- /dev/null
+++ b/home-manager/tests/modules/programs/readline/using-all-options.txt
@@ -0,0 +1,11 @@
+# Generated by Home Manager.
+
+$include /etc/inputrc
+set bell-style audible
+set completion-map-case on
+set completion-prefix-display-length 2
+"\C-h": backward-kill-word
+$if mode=emacs
+"\e[1~": beginning-of-line
+$endif
+
diff --git a/home-manager/tests/modules/programs/rofi/assert-on-both-theme-and-colors-expected.json b/home-manager/tests/modules/programs/rofi/assert-on-both-theme-and-colors-expected.json
new file mode 100644
index 00000000000..808288f4193
--- /dev/null
+++ b/home-manager/tests/modules/programs/rofi/assert-on-both-theme-and-colors-expected.json
@@ -0,0 +1 @@
+["Cannot use the rofi options 'theme' and 'colors' simultaneously.\n"] \ No newline at end of file
diff --git a/home-manager/tests/modules/programs/rofi/assert-on-both-theme-and-colors.nix b/home-manager/tests/modules/programs/rofi/assert-on-both-theme-and-colors.nix
new file mode 100644
index 00000000000..0f9cfc39a6c
--- /dev/null
+++ b/home-manager/tests/modules/programs/rofi/assert-on-both-theme-and-colors.nix
@@ -0,0 +1,29 @@
+{ config, lib, ... }:
+
+with lib;
+
+{
+ config = {
+ programs.rofi = {
+ enable = true;
+ theme = "foo";
+ colors = {
+ window = {
+ background = "background";
+ border = "border";
+ separator = "separator";
+ };
+ rows = { };
+ };
+ };
+
+ home.file.result.text = builtins.toJSON
+ (map (a: a.message) (filter (a: !a.assertion) config.assertions));
+
+ nmt.script = ''
+ assertFileContent \
+ home-files/result \
+ ${./assert-on-both-theme-and-colors-expected.json}
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/programs/rofi/default.nix b/home-manager/tests/modules/programs/rofi/default.nix
new file mode 100644
index 00000000000..c18c3b0ed6a
--- /dev/null
+++ b/home-manager/tests/modules/programs/rofi/default.nix
@@ -0,0 +1,3 @@
+{
+ rofi-assert-on-both-theme-and-colors = ./assert-on-both-theme-and-colors.nix;
+}
diff --git a/home-manager/tests/modules/programs/ssh/default-config-expected.conf b/home-manager/tests/modules/programs/ssh/default-config-expected.conf
new file mode 100644
index 00000000000..55748ea6c82
--- /dev/null
+++ b/home-manager/tests/modules/programs/ssh/default-config-expected.conf
@@ -0,0 +1,15 @@
+
+
+
+
+Host *
+ ForwardAgent no
+ Compression no
+ ServerAliveInterval 0
+ HashKnownHosts no
+ UserKnownHostsFile ~/.ssh/known_hosts
+ ControlMaster no
+ ControlPath ~/.ssh/master-%r@%n:%p
+ ControlPersist no
+
+
diff --git a/home-manager/tests/modules/programs/ssh/default-config.nix b/home-manager/tests/modules/programs/ssh/default-config.nix
new file mode 100644
index 00000000000..6d7e5508a2f
--- /dev/null
+++ b/home-manager/tests/modules/programs/ssh/default-config.nix
@@ -0,0 +1,18 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ config = {
+ programs.ssh = { enable = true; };
+
+ home.file.assertions.text = builtins.toJSON
+ (map (a: a.message) (filter (a: !a.assertion) config.assertions));
+
+ nmt.script = ''
+ assertFileExists home-files/.ssh/config
+ assertFileContent home-files/.ssh/config ${./default-config-expected.conf}
+ assertFileContent home-files/assertions ${./no-assertions.json}
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/programs/ssh/default.nix b/home-manager/tests/modules/programs/ssh/default.nix
new file mode 100644
index 00000000000..507eef0bdb8
--- /dev/null
+++ b/home-manager/tests/modules/programs/ssh/default.nix
@@ -0,0 +1,17 @@
+{
+ ssh-defaults = ./default-config.nix;
+ ssh-match-blocks = ./match-blocks-attrs.nix;
+
+ ssh-forwards-dynamic-valid-bind-no-asserts =
+ ./forwards-dynamic-valid-bind-no-asserts.nix;
+ ssh-forwards-dynamic-bind-path-with-port-asserts =
+ ./forwards-dynamic-bind-path-with-port-asserts.nix;
+ ssh-forwards-local-bind-path-with-port-asserts =
+ ./forwards-local-bind-path-with-port-asserts.nix;
+ ssh-forwards-local-host-path-with-port-asserts =
+ ./forwards-local-host-path-with-port-asserts.nix;
+ ssh-forwards-remote-bind-path-with-port-asserts =
+ ./forwards-remote-bind-path-with-port-asserts.nix;
+ ssh-forwards-remote-host-path-with-port-asserts =
+ ./forwards-remote-host-path-with-port-asserts.nix;
+}
diff --git a/home-manager/tests/modules/programs/ssh/forwards-dynamic-bind-path-with-port-asserts.nix b/home-manager/tests/modules/programs/ssh/forwards-dynamic-bind-path-with-port-asserts.nix
new file mode 100644
index 00000000000..cf2efe5a5a5
--- /dev/null
+++ b/home-manager/tests/modules/programs/ssh/forwards-dynamic-bind-path-with-port-asserts.nix
@@ -0,0 +1,29 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ config = {
+ programs.ssh = {
+ enable = true;
+ matchBlocks = {
+ dynamicBindPathWithPort = {
+ dynamicForwards = [{
+ # Error:
+ address = "/run/user/1000/gnupg/S.gpg-agent.extra";
+ port = 3000;
+ }];
+ };
+ };
+ };
+
+ home.file.result.text = builtins.toJSON
+ (map (a: a.message) (filter (a: !a.assertion) config.assertions));
+
+ nmt.script = ''
+ assertFileContent home-files/result ${
+ ./forwards-paths-with-ports-error.json
+ }
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/programs/ssh/forwards-dynamic-valid-bind-no-asserts-expected.conf b/home-manager/tests/modules/programs/ssh/forwards-dynamic-valid-bind-no-asserts-expected.conf
new file mode 100644
index 00000000000..5213d282c28
--- /dev/null
+++ b/home-manager/tests/modules/programs/ssh/forwards-dynamic-valid-bind-no-asserts-expected.conf
@@ -0,0 +1,19 @@
+
+
+Host dynamicBindAddressWithPort
+ DynamicForward [127.0.0.1]:3000
+
+Host dynamicBindPathNoPort
+ DynamicForward /run/user/1000/gnupg/S.gpg-agent.extra
+
+Host *
+ ForwardAgent no
+ Compression no
+ ServerAliveInterval 0
+ HashKnownHosts no
+ UserKnownHostsFile ~/.ssh/known_hosts
+ ControlMaster no
+ ControlPath ~/.ssh/master-%r@%n:%p
+ ControlPersist no
+
+
diff --git a/home-manager/tests/modules/programs/ssh/forwards-dynamic-valid-bind-no-asserts.nix b/home-manager/tests/modules/programs/ssh/forwards-dynamic-valid-bind-no-asserts.nix
new file mode 100644
index 00000000000..d0c3a732256
--- /dev/null
+++ b/home-manager/tests/modules/programs/ssh/forwards-dynamic-valid-bind-no-asserts.nix
@@ -0,0 +1,38 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ config = {
+ programs.ssh = {
+ enable = true;
+ matchBlocks = {
+ dynamicBindPathNoPort = {
+ dynamicForwards = [{
+ # OK:
+ address = "/run/user/1000/gnupg/S.gpg-agent.extra";
+ }];
+ };
+
+ dynamicBindAddressWithPort = {
+ dynamicForwards = [{
+ # OK:
+ address = "127.0.0.1";
+ port = 3000;
+ }];
+ };
+ };
+ };
+
+ home.file.result.text = builtins.toJSON
+ (map (a: a.message) (filter (a: !a.assertion) config.assertions));
+
+ nmt.script = ''
+ assertFileExists home-files/.ssh/config
+ assertFileContent \
+ home-files/.ssh/config \
+ ${./forwards-dynamic-valid-bind-no-asserts-expected.conf}
+ assertFileContent home-files/result ${./no-assertions.json}
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/programs/ssh/forwards-local-bind-path-with-port-asserts.nix b/home-manager/tests/modules/programs/ssh/forwards-local-bind-path-with-port-asserts.nix
new file mode 100644
index 00000000000..f9d8e2daf85
--- /dev/null
+++ b/home-manager/tests/modules/programs/ssh/forwards-local-bind-path-with-port-asserts.nix
@@ -0,0 +1,33 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ config = {
+ programs.ssh = {
+ enable = true;
+ matchBlocks = {
+ localBindPathWithPort = {
+ localForwards = [{
+ # OK:
+ host.address = "127.0.0.1";
+ host.port = 3000;
+
+ # Error:
+ bind.address = "/run/user/1000/gnupg/S.gpg-agent.extra";
+ bind.port = 3000;
+ }];
+ };
+ };
+ };
+
+ home.file.result.text = builtins.toJSON
+ (map (a: a.message) (filter (a: !a.assertion) config.assertions));
+
+ nmt.script = ''
+ assertFileContent home-files/result ${
+ ./forwards-paths-with-ports-error.json
+ }
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/programs/ssh/forwards-local-host-path-with-port-asserts.nix b/home-manager/tests/modules/programs/ssh/forwards-local-host-path-with-port-asserts.nix
new file mode 100644
index 00000000000..02a7e5b168d
--- /dev/null
+++ b/home-manager/tests/modules/programs/ssh/forwards-local-host-path-with-port-asserts.nix
@@ -0,0 +1,33 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ config = {
+ programs.ssh = {
+ enable = true;
+ matchBlocks = {
+ localHostPathWithPort = {
+ localForwards = [{
+ # OK:
+ bind.address = "127.0.0.1";
+ bind.port = 3000;
+
+ # Error:
+ host.address = "/run/user/1000/gnupg/S.gpg-agent.extra";
+ host.port = 3000;
+ }];
+ };
+ };
+ };
+
+ home.file.result.text = builtins.toJSON
+ (map (a: a.message) (filter (a: !a.assertion) config.assertions));
+
+ nmt.script = ''
+ assertFileContent home-files/result ${
+ ./forwards-paths-with-ports-error.json
+ }
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/programs/ssh/forwards-paths-with-ports-error.json b/home-manager/tests/modules/programs/ssh/forwards-paths-with-ports-error.json
new file mode 100644
index 00000000000..e7e3a374ecc
--- /dev/null
+++ b/home-manager/tests/modules/programs/ssh/forwards-paths-with-ports-error.json
@@ -0,0 +1 @@
+["Forwarded paths cannot have ports."] \ No newline at end of file
diff --git a/home-manager/tests/modules/programs/ssh/forwards-remote-bind-path-with-port-asserts.nix b/home-manager/tests/modules/programs/ssh/forwards-remote-bind-path-with-port-asserts.nix
new file mode 100644
index 00000000000..61ce9ae0385
--- /dev/null
+++ b/home-manager/tests/modules/programs/ssh/forwards-remote-bind-path-with-port-asserts.nix
@@ -0,0 +1,33 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ config = {
+ programs.ssh = {
+ enable = true;
+ matchBlocks = {
+ remoteBindPathWithPort = {
+ remoteForwards = [{
+ # OK:
+ host.address = "127.0.0.1";
+ host.port = 3000;
+
+ # Error:
+ bind.address = "/run/user/1000/gnupg/S.gpg-agent.extra";
+ bind.port = 3000;
+ }];
+ };
+ };
+ };
+
+ home.file.result.text = builtins.toJSON
+ (map (a: a.message) (filter (a: !a.assertion) config.assertions));
+
+ nmt.script = ''
+ assertFileContent home-files/result ${
+ ./forwards-paths-with-ports-error.json
+ }
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/programs/ssh/forwards-remote-host-path-with-port-asserts.nix b/home-manager/tests/modules/programs/ssh/forwards-remote-host-path-with-port-asserts.nix
new file mode 100644
index 00000000000..71bdbcb70fd
--- /dev/null
+++ b/home-manager/tests/modules/programs/ssh/forwards-remote-host-path-with-port-asserts.nix
@@ -0,0 +1,33 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ config = {
+ programs.ssh = {
+ enable = true;
+ matchBlocks = {
+ remoteHostPathWithPort = {
+ remoteForwards = [{
+ # OK:
+ bind.address = "127.0.0.1";
+ bind.port = 3000;
+
+ # Error:
+ host.address = "/run/user/1000/gnupg/S.gpg-agent.extra";
+ host.port = 3000;
+ }];
+ };
+ };
+ };
+
+ home.file.result.text = builtins.toJSON
+ (map (a: a.message) (filter (a: !a.assertion) config.assertions));
+
+ nmt.script = ''
+ assertFileContent home-files/result ${
+ ./forwards-paths-with-ports-error.json
+ }
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/programs/ssh/match-blocks-attrs-expected.conf b/home-manager/tests/modules/programs/ssh/match-blocks-attrs-expected.conf
new file mode 100644
index 00000000000..f0d768375f0
--- /dev/null
+++ b/home-manager/tests/modules/programs/ssh/match-blocks-attrs-expected.conf
@@ -0,0 +1,29 @@
+
+
+Host * !github.com
+ Port 516
+ IdentityFile file1
+ IdentityFile file2
+
+Host abc
+ ProxyJump jump-host
+
+Host xyz
+ ServerAliveInterval 60
+ IdentityFile file
+ LocalForward [localhost]:8080 [10.0.0.1]:80
+ RemoteForward [localhost]:8081 [10.0.0.2]:80
+ RemoteForward /run/user/1000/gnupg/S.gpg-agent.extra /run/user/1000/gnupg/S.gpg-agent
+ DynamicForward [localhost]:2839
+
+Host *
+ ForwardAgent no
+ Compression no
+ ServerAliveInterval 0
+ HashKnownHosts no
+ UserKnownHostsFile ~/.ssh/known_hosts
+ ControlMaster no
+ ControlPath ~/.ssh/master-%r@%n:%p
+ ControlPersist no
+
+
diff --git a/home-manager/tests/modules/programs/ssh/match-blocks-attrs.nix b/home-manager/tests/modules/programs/ssh/match-blocks-attrs.nix
new file mode 100644
index 00000000000..a84a703e89d
--- /dev/null
+++ b/home-manager/tests/modules/programs/ssh/match-blocks-attrs.nix
@@ -0,0 +1,55 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ config = {
+ programs.ssh = {
+ enable = true;
+ matchBlocks = {
+ abc = {
+ identityFile = null;
+ proxyJump = "jump-host";
+ };
+
+ xyz = {
+ identityFile = "file";
+ serverAliveInterval = 60;
+ localForwards = [{
+ bind.port = 8080;
+ host.address = "10.0.0.1";
+ host.port = 80;
+ }];
+ remoteForwards = [
+ {
+ bind.port = 8081;
+ host.address = "10.0.0.2";
+ host.port = 80;
+ }
+ {
+ bind.address = "/run/user/1000/gnupg/S.gpg-agent.extra";
+ host.address = "/run/user/1000/gnupg/S.gpg-agent";
+ }
+ ];
+ dynamicForwards = [{ port = 2839; }];
+ };
+
+ "* !github.com" = {
+ identityFile = [ "file1" "file2" ];
+ port = 516;
+ };
+ };
+ };
+
+ home.file.assertions.text = builtins.toJSON
+ (map (a: a.message) (filter (a: !a.assertion) config.assertions));
+
+ nmt.script = ''
+ assertFileExists home-files/.ssh/config
+ assertFileContent \
+ home-files/.ssh/config \
+ ${./match-blocks-attrs-expected.conf}
+ assertFileContent home-files/assertions ${./no-assertions.json}
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/programs/ssh/no-assertions.json b/home-manager/tests/modules/programs/ssh/no-assertions.json
new file mode 100644
index 00000000000..0637a088a01
--- /dev/null
+++ b/home-manager/tests/modules/programs/ssh/no-assertions.json
@@ -0,0 +1 @@
+[] \ No newline at end of file
diff --git a/home-manager/tests/modules/programs/texlive/default.nix b/home-manager/tests/modules/programs/texlive/default.nix
new file mode 100644
index 00000000000..23bfe2daf0a
--- /dev/null
+++ b/home-manager/tests/modules/programs/texlive/default.nix
@@ -0,0 +1 @@
+{ texlive-minimal = ./texlive-minimal.nix; }
diff --git a/home-manager/tests/modules/programs/texlive/texlive-minimal.nix b/home-manager/tests/modules/programs/texlive/texlive-minimal.nix
new file mode 100644
index 00000000000..df143dbc660
--- /dev/null
+++ b/home-manager/tests/modules/programs/texlive/texlive-minimal.nix
@@ -0,0 +1,13 @@
+{ config, lib, ... }:
+
+with lib;
+
+{
+ config = {
+ programs.texlive.enable = true;
+
+ nmt.script = ''
+ assertFileExists home-path/bin/tex
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/programs/tmux/default.nix b/home-manager/tests/modules/programs/tmux/default.nix
new file mode 100644
index 00000000000..d4501c60981
--- /dev/null
+++ b/home-manager/tests/modules/programs/tmux/default.nix
@@ -0,0 +1,7 @@
+{
+ tmux-emacs-with-plugins = ./emacs-with-plugins.nix;
+ tmux-not-enabled = ./not-enabled.nix;
+ tmux-vi-all-true = ./vi-all-true.nix;
+ tmux-secure-socket-enabled = ./secure-socket-enabled.nix;
+ tmux-disable-confirmation-prompt = ./disable-confirmation-prompt.nix;
+}
diff --git a/home-manager/tests/modules/programs/tmux/disable-confirmation-prompt.conf b/home-manager/tests/modules/programs/tmux/disable-confirmation-prompt.conf
new file mode 100644
index 00000000000..b599e603e4a
--- /dev/null
+++ b/home-manager/tests/modules/programs/tmux/disable-confirmation-prompt.conf
@@ -0,0 +1,31 @@
+# ============================================= #
+# Start with defaults from the Sensible plugin #
+# --------------------------------------------- #
+run-shell @sensible_rtp@
+# ============================================= #
+
+set -g default-terminal "screen"
+set -g base-index 0
+setw -g pane-base-index 0
+
+
+
+
+
+set -g status-keys emacs
+set -g mode-keys emacs
+
+
+
+
+
+bind-key & kill-window
+bind-key x kill-pane
+
+
+setw -g aggressive-resize off
+setw -g clock-mode-style 12
+set -s escape-time 500
+set -g history-limit 2000
+
+
diff --git a/home-manager/tests/modules/programs/tmux/disable-confirmation-prompt.nix b/home-manager/tests/modules/programs/tmux/disable-confirmation-prompt.nix
new file mode 100644
index 00000000000..e3d13a4b1b2
--- /dev/null
+++ b/home-manager/tests/modules/programs/tmux/disable-confirmation-prompt.nix
@@ -0,0 +1,28 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ config = {
+ programs.tmux = {
+ enable = true;
+ disableConfirmationPrompt = true;
+ };
+
+ nixpkgs.overlays = [
+ (self: super: {
+ tmuxPlugins = super.tmuxPlugins // {
+ sensible = super.tmuxPlugins.sensible // {
+ rtp = "@sensible_rtp@";
+ };
+ };
+ })
+ ];
+
+ nmt.script = ''
+ assertFileExists home-files/.tmux.conf
+ assertFileContent home-files/.tmux.conf \
+ ${./disable-confirmation-prompt.conf}
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/programs/tmux/emacs-with-plugins.conf b/home-manager/tests/modules/programs/tmux/emacs-with-plugins.conf
new file mode 100644
index 00000000000..66b10183750
--- /dev/null
+++ b/home-manager/tests/modules/programs/tmux/emacs-with-plugins.conf
@@ -0,0 +1,54 @@
+# ============================================= #
+# Start with defaults from the Sensible plugin #
+# --------------------------------------------- #
+run-shell @tmuxplugin_sensible_rtp@
+# ============================================= #
+
+set -g default-terminal "screen"
+set -g base-index 0
+setw -g pane-base-index 0
+
+new-session
+
+bind v split-window -h
+bind s split-window -v
+
+
+set -g status-keys emacs
+set -g mode-keys emacs
+
+
+
+
+
+
+
+setw -g aggressive-resize on
+setw -g clock-mode-style 24
+set -s escape-time 500
+set -g history-limit 2000
+
+
+
+# ============================================= #
+# Load plugins with Home Manager #
+# --------------------------------------------- #
+
+# tmuxplugin-logging
+# ---------------------
+
+run-shell @tmuxplugin_logging_rtp@
+
+
+# tmuxplugin-prefix-highlight
+# ---------------------
+
+run-shell @tmuxplugin_prefix_highlight_rtp@
+
+
+# tmuxplugin-fzf-tmux-url
+# ---------------------
+
+run-shell @tmuxplugin_fzf_tmux_url_rtp@
+
+# ============================================= #
diff --git a/home-manager/tests/modules/programs/tmux/emacs-with-plugins.nix b/home-manager/tests/modules/programs/tmux/emacs-with-plugins.nix
new file mode 100644
index 00000000000..f9bccaa2ce4
--- /dev/null
+++ b/home-manager/tests/modules/programs/tmux/emacs-with-plugins.nix
@@ -0,0 +1,49 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ config = {
+ programs.tmux = {
+ aggressiveResize = true;
+ clock24 = true;
+ enable = true;
+ keyMode = "emacs";
+ newSession = true;
+ reverseSplit = true;
+
+ plugins = with pkgs.tmuxPlugins; [
+ logging
+ prefix-highlight
+ fzf-tmux-url
+ ];
+ };
+
+ nixpkgs.overlays = [
+ (self: super: {
+ tmuxPlugins = super.tmuxPlugins // {
+ fzf-tmux-url = super.tmuxPlugins.fzf-tmux-url // {
+ rtp = "@tmuxplugin_fzf_tmux_url_rtp@";
+ };
+
+ logging = super.tmuxPlugins.logging // {
+ rtp = "@tmuxplugin_logging_rtp@";
+ };
+
+ prefix-highlight = super.tmuxPlugins.prefix-highlight // {
+ rtp = "@tmuxplugin_prefix_highlight_rtp@";
+ };
+
+ sensible = super.tmuxPlugins.sensible // {
+ rtp = "@tmuxplugin_sensible_rtp@";
+ };
+ };
+ })
+ ];
+
+ nmt.script = ''
+ assertFileExists home-files/.tmux.conf
+ assertFileContent home-files/.tmux.conf ${./emacs-with-plugins.conf}
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/programs/tmux/hm-session-vars.sh b/home-manager/tests/modules/programs/tmux/hm-session-vars.sh
new file mode 100644
index 00000000000..40d9c24b50d
--- /dev/null
+++ b/home-manager/tests/modules/programs/tmux/hm-session-vars.sh
@@ -0,0 +1,5 @@
+# Only source this once.
+if [ -n "$__HM_SESS_VARS_SOURCED" ]; then return; fi
+export __HM_SESS_VARS_SOURCED=1
+
+export TMUX_TMPDIR="${XDG_RUNTIME_DIR:-"/run/user/\$(id -u)"}"
diff --git a/home-manager/tests/modules/programs/tmux/not-enabled.nix b/home-manager/tests/modules/programs/tmux/not-enabled.nix
new file mode 100644
index 00000000000..b7c675a83a2
--- /dev/null
+++ b/home-manager/tests/modules/programs/tmux/not-enabled.nix
@@ -0,0 +1,13 @@
+{ config, lib, ... }:
+
+with lib;
+
+{
+ config = {
+ programs.tmux = { enable = false; };
+
+ nmt.script = ''
+ assertPathNotExists home-files/.tmux.conf
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/programs/tmux/secure-socket-enabled.nix b/home-manager/tests/modules/programs/tmux/secure-socket-enabled.nix
new file mode 100644
index 00000000000..34e9129c5a4
--- /dev/null
+++ b/home-manager/tests/modules/programs/tmux/secure-socket-enabled.nix
@@ -0,0 +1,17 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ config = {
+ programs.tmux = {
+ enable = true;
+ secureSocket = true;
+ };
+
+ nmt.script = ''
+ assertFileExists home-path/etc/profile.d/hm-session-vars.sh
+ assertFileContent home-path/etc/profile.d/hm-session-vars.sh ${./hm-session-vars.sh}
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/programs/tmux/vi-all-true.conf b/home-manager/tests/modules/programs/tmux/vi-all-true.conf
new file mode 100644
index 00000000000..08e37e19b97
--- /dev/null
+++ b/home-manager/tests/modules/programs/tmux/vi-all-true.conf
@@ -0,0 +1,31 @@
+# ============================================= #
+# Start with defaults from the Sensible plugin #
+# --------------------------------------------- #
+run-shell @sensible_rtp@
+# ============================================= #
+
+set -g default-terminal "screen"
+set -g base-index 0
+setw -g pane-base-index 0
+
+new-session
+
+bind v split-window -h
+bind s split-window -v
+
+
+set -g status-keys vi
+set -g mode-keys vi
+
+
+
+
+
+
+
+setw -g aggressive-resize on
+setw -g clock-mode-style 24
+set -s escape-time 500
+set -g history-limit 2000
+
+
diff --git a/home-manager/tests/modules/programs/tmux/vi-all-true.nix b/home-manager/tests/modules/programs/tmux/vi-all-true.nix
new file mode 100644
index 00000000000..bce032fd654
--- /dev/null
+++ b/home-manager/tests/modules/programs/tmux/vi-all-true.nix
@@ -0,0 +1,29 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ config = {
+ programs.tmux = {
+ aggressiveResize = true;
+ clock24 = true;
+ enable = true;
+ keyMode = "vi";
+ newSession = true;
+ reverseSplit = true;
+ };
+
+ nixpkgs.overlays = [
+ (self: super: {
+ tmuxPlugins = super.tmuxPlugins // {
+ sensible = super.tmuxPlugins.sensible // { rtp = "@sensible_rtp@"; };
+ };
+ })
+ ];
+
+ nmt.script = ''
+ assertFileExists home-files/.tmux.conf
+ assertFileContent home-files/.tmux.conf ${./vi-all-true.conf}
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/programs/zsh/default.nix b/home-manager/tests/modules/programs/zsh/default.nix
new file mode 100644
index 00000000000..37339598e35
--- /dev/null
+++ b/home-manager/tests/modules/programs/zsh/default.nix
@@ -0,0 +1,7 @@
+{
+ zsh-session-variables = ./session-variables.nix;
+ zsh-history-path-new-default = ./history-path-new-default.nix;
+ zsh-history-path-new-custom = ./history-path-new-custom.nix;
+ zsh-history-path-old-default = ./history-path-old-default.nix;
+ zsh-history-path-old-custom = ./history-path-old-custom.nix;
+}
diff --git a/home-manager/tests/modules/programs/zsh/history-path-new-custom.nix b/home-manager/tests/modules/programs/zsh/history-path-new-custom.nix
new file mode 100644
index 00000000000..0c052d6949e
--- /dev/null
+++ b/home-manager/tests/modules/programs/zsh/history-path-new-custom.nix
@@ -0,0 +1,20 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ config = {
+ home.stateVersion = "20.03";
+ programs.zsh = {
+ enable = true;
+ history.path = "$HOME/some/directory/zsh_history";
+ };
+
+ nixpkgs.overlays =
+ [ (self: super: { zsh = pkgs.writeScriptBin "dummy-zsh" ""; }) ];
+
+ nmt.script = ''
+ assertFileRegex home-files/.zshrc '^HISTFILE="$HOME/some/directory/zsh_history"$'
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/programs/zsh/history-path-new-default.nix b/home-manager/tests/modules/programs/zsh/history-path-new-default.nix
new file mode 100644
index 00000000000..6d1f58a29dc
--- /dev/null
+++ b/home-manager/tests/modules/programs/zsh/history-path-new-default.nix
@@ -0,0 +1,17 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ config = {
+ home.stateVersion = "20.03";
+ programs.zsh.enable = true;
+
+ nixpkgs.overlays =
+ [ (self: super: { zsh = pkgs.writeScriptBin "dummy-zsh" ""; }) ];
+
+ nmt.script = ''
+ assertFileRegex home-files/.zshrc '^HISTFILE="$HOME/.zsh_history"$'
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/programs/zsh/history-path-old-custom.nix b/home-manager/tests/modules/programs/zsh/history-path-old-custom.nix
new file mode 100644
index 00000000000..f5b178b5e97
--- /dev/null
+++ b/home-manager/tests/modules/programs/zsh/history-path-old-custom.nix
@@ -0,0 +1,20 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ config = {
+ home.stateVersion = "19.09";
+ programs.zsh = {
+ enable = true;
+ history.path = "some/directory/zsh_history";
+ };
+
+ nixpkgs.overlays =
+ [ (self: super: { zsh = pkgs.writeScriptBin "dummy-zsh" ""; }) ];
+
+ nmt.script = ''
+ assertFileRegex home-files/.zshrc '^HISTFILE="$HOME/some/directory/zsh_history"$'
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/programs/zsh/history-path-old-default.nix b/home-manager/tests/modules/programs/zsh/history-path-old-default.nix
new file mode 100644
index 00000000000..d880d966454
--- /dev/null
+++ b/home-manager/tests/modules/programs/zsh/history-path-old-default.nix
@@ -0,0 +1,17 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ config = {
+ home.stateVersion = "19.03";
+ programs.zsh.enable = true;
+
+ nixpkgs.overlays =
+ [ (self: super: { zsh = pkgs.writeScriptBin "dummy-zsh" ""; }) ];
+
+ nmt.script = ''
+ assertFileRegex home-files/.zshrc '^HISTFILE="$HOME/.zsh_history"$'
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/programs/zsh/session-variables.nix b/home-manager/tests/modules/programs/zsh/session-variables.nix
new file mode 100644
index 00000000000..ca903619d68
--- /dev/null
+++ b/home-manager/tests/modules/programs/zsh/session-variables.nix
@@ -0,0 +1,28 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ config = {
+ programs.zsh = {
+ enable = true;
+
+ sessionVariables = {
+ V1 = "v1";
+ V2 = "v2-${config.programs.zsh.sessionVariables.V1}";
+ };
+ };
+
+ nixpkgs.overlays = [
+ (self: super: {
+ zsh = pkgs.writeScriptBin "dummy-zsh" "";
+ })
+ ];
+
+ nmt.script = ''
+ assertFileExists home-files/.zshrc
+ assertFileRegex home-files/.zshrc 'export V1="v1"'
+ assertFileRegex home-files/.zshrc 'export V2="v2-v1"'
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/services/sxhkd/configuration.nix b/home-manager/tests/modules/services/sxhkd/configuration.nix
new file mode 100644
index 00000000000..992c4b18a94
--- /dev/null
+++ b/home-manager/tests/modules/services/sxhkd/configuration.nix
@@ -0,0 +1,30 @@
+{ config, ... }: {
+ config = {
+ services.sxhkd = {
+ enable = true;
+
+ keybindings = {
+ "super + a" = "run command a";
+ "super + b" = null;
+ "super + Shift + b" = "run command b";
+ };
+
+ extraConfig = ''
+ super + c
+ call command c
+
+ # comment
+ super + d
+ call command d
+ '';
+ };
+
+ nmt.script = ''
+ local sxhkdrc=home-files/.config/sxhkd/sxhkdrc
+
+ assertFileExists $sxhkdrc
+
+ assertFileContent $sxhkdrc ${./sxhkdrc}
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/services/sxhkd/default.nix b/home-manager/tests/modules/services/sxhkd/default.nix
new file mode 100644
index 00000000000..ec25252cee0
--- /dev/null
+++ b/home-manager/tests/modules/services/sxhkd/default.nix
@@ -0,0 +1,4 @@
+{
+ sxhkd-configuration = ./configuration.nix;
+ sxhkd-service = ./service.nix;
+}
diff --git a/home-manager/tests/modules/services/sxhkd/service.nix b/home-manager/tests/modules/services/sxhkd/service.nix
new file mode 100644
index 00000000000..46ce259a718
--- /dev/null
+++ b/home-manager/tests/modules/services/sxhkd/service.nix
@@ -0,0 +1,20 @@
+{ config, ... }:
+{
+ config = {
+ services.sxhkd = {
+ enable = true;
+ extraPath = "/home/the-user/bin:/extra/path/bin";
+ };
+
+ nmt.script = ''
+ local serviceFile=home-files/.config/systemd/user/sxhkd.service
+
+ assertFileExists $serviceFile
+
+ assertFileRegex $serviceFile 'ExecStart=.*/bin/sxhkd'
+
+ assertFileRegex $serviceFile \
+ 'Environment=PATH=.*\.nix-profile/bin:/home/the-user/bin:/extra/path/bin'
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/services/sxhkd/sxhkdrc b/home-manager/tests/modules/services/sxhkd/sxhkdrc
new file mode 100644
index 00000000000..c8883464b29
--- /dev/null
+++ b/home-manager/tests/modules/services/sxhkd/sxhkdrc
@@ -0,0 +1,13 @@
+super + Shift + b
+ run command b
+
+super + a
+ run command a
+
+
+super + c
+ call command c
+
+# comment
+super + d
+ call command d
diff --git a/home-manager/tests/modules/services/window-managers/i3/default.nix b/home-manager/tests/modules/services/window-managers/i3/default.nix
new file mode 100644
index 00000000000..e239d6c07f1
--- /dev/null
+++ b/home-manager/tests/modules/services/window-managers/i3/default.nix
@@ -0,0 +1 @@
+{ i3-keybindings = ./i3-keybindings.nix; }
diff --git a/home-manager/tests/modules/services/window-managers/i3/i3-keybindings-expected.conf b/home-manager/tests/modules/services/window-managers/i3/i3-keybindings-expected.conf
new file mode 100644
index 00000000000..1e385e8c734
--- /dev/null
+++ b/home-manager/tests/modules/services/window-managers/i3/i3-keybindings-expected.conf
@@ -0,0 +1,106 @@
+font pango:monospace 8
+floating_modifier Mod1
+new_window normal 2
+new_float normal 2
+hide_edge_borders none
+force_focus_wrapping no
+focus_follows_mouse yes
+focus_on_window_activation smart
+mouse_warping output
+workspace_layout default
+workspace_auto_back_and_forth no
+
+client.focused #4c7899 #285577 #ffffff #2e9ef4 #285577
+client.focused_inactive #333333 #5f676a #ffffff #484e50 #5f676a
+client.unfocused #333333 #222222 #888888 #292d2e #222222
+client.urgent #2f343a #900000 #ffffff #900000 #900000
+client.placeholder #000000 #0c0c0c #ffffff #000000 #0c0c0c
+client.background #ffffff
+
+bindsym Mod1+0 workspace number 10
+bindsym Mod1+1 workspace number 1
+bindsym Mod1+2 workspace number 2
+bindsym Mod1+3 workspace number 3
+bindsym Mod1+4 workspace number 4
+bindsym Mod1+5 workspace number 5
+bindsym Mod1+6 workspace number 6
+bindsym Mod1+7 workspace number 7
+bindsym Mod1+8 workspace number 8
+bindsym Mod1+9 workspace number 9
+bindsym Mod1+Down focus down
+bindsym Mod1+Invented invented-key-command
+bindsym Mod1+Left overridden-command
+bindsym Mod1+Return exec i3-sensible-terminal
+
+bindsym Mod1+Shift+0 move container to workspace number 10
+bindsym Mod1+Shift+1 move container to workspace number 1
+bindsym Mod1+Shift+2 move container to workspace number 2
+bindsym Mod1+Shift+3 move container to workspace number 3
+bindsym Mod1+Shift+4 move container to workspace number 4
+bindsym Mod1+Shift+5 move container to workspace number 5
+bindsym Mod1+Shift+6 move container to workspace number 6
+bindsym Mod1+Shift+7 move container to workspace number 7
+bindsym Mod1+Shift+8 move container to workspace number 8
+bindsym Mod1+Shift+9 move container to workspace number 9
+bindsym Mod1+Shift+Down move down
+bindsym Mod1+Shift+Left move left
+bindsym Mod1+Shift+Right move right
+bindsym Mod1+Shift+Up move up
+bindsym Mod1+Shift+c reload
+bindsym Mod1+Shift+e exec i3-nagbar -t warning -m 'Do you want to exit i3?' -b 'Yes' 'i3-msg exit'
+bindsym Mod1+Shift+minus move scratchpad
+bindsym Mod1+Shift+q kill
+bindsym Mod1+Shift+r restart
+bindsym Mod1+Shift+space floating toggle
+bindsym Mod1+Up focus up
+bindsym Mod1+a focus parent
+bindsym Mod1+d exec @dmenu@/bin/dmenu_run
+bindsym Mod1+e layout toggle split
+bindsym Mod1+f fullscreen toggle
+bindsym Mod1+h split h
+bindsym Mod1+minus scratchpad show
+bindsym Mod1+r mode resize
+bindsym Mod1+s layout stacking
+bindsym Mod1+space focus mode_toggle
+bindsym Mod1+v split v
+bindsym Mod1+w layout tabbed
+
+mode "resize" {
+bindsym Down resize grow height 10 px or 10 ppt
+bindsym Escape mode default
+bindsym Left resize shrink width 10 px or 10 ppt
+bindsym Return mode default
+bindsym Right resize grow width 10 px or 10 ppt
+bindsym Up resize shrink height 10 px or 10 ppt
+}
+
+
+bar {
+
+ font pango:monospace 8
+ mode dock
+ hidden_state hide
+ position bottom
+ status_command @i3status@/bin/i3status
+ i3bar_command @i3@/bin/i3bar
+ workspace_buttons yes
+ strip_workspace_numbers no
+ tray_output primary
+ colors {
+ background #000000
+ statusline #ffffff
+ separator #666666
+ focused_workspace #4c7899 #285577 #ffffff
+ active_workspace #333333 #5f676a #ffffff
+ inactive_workspace #333333 #222222 #888888
+ urgent_workspace #2f343a #900000 #ffffff
+ binding_mode #2f343a #900000 #ffffff
+ }
+
+}
+
+
+
+
+
+
diff --git a/home-manager/tests/modules/services/window-managers/i3/i3-keybindings.nix b/home-manager/tests/modules/services/window-managers/i3/i3-keybindings.nix
new file mode 100644
index 00000000000..4f8515e61ff
--- /dev/null
+++ b/home-manager/tests/modules/services/window-managers/i3/i3-keybindings.nix
@@ -0,0 +1,35 @@
+{ config, lib, ... }:
+
+with lib;
+
+{
+ config = {
+ xsession.windowManager.i3 = {
+ enable = true;
+
+ config.keybindings =
+ let modifier = config.xsession.windowManager.i3.config.modifier;
+ in lib.mkOptionDefault {
+ "${modifier}+Left" = "overridden-command";
+ "${modifier}+Right" = null;
+ "${modifier}+Invented" = "invented-key-command";
+ };
+ };
+
+ nixpkgs.overlays = [
+ (self: super: {
+ dmenu = super.dmenu // { outPath = "@dmenu@"; };
+
+ i3 = super.i3 // { outPath = "@i3@"; };
+
+ i3status = super.i3status // { outPath = "@i3status@"; };
+ })
+ ];
+
+ nmt.script = ''
+ assertFileExists home-files/.config/i3/config
+ assertFileContent home-files/.config/i3/config \
+ ${./i3-keybindings-expected.conf}
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/systemd/default.nix b/home-manager/tests/modules/systemd/default.nix
new file mode 100644
index 00000000000..c1779ac59fb
--- /dev/null
+++ b/home-manager/tests/modules/systemd/default.nix
@@ -0,0 +1,5 @@
+{
+ systemd-services = ./services.nix;
+ systemd-session-variables = ./session-variables.nix;
+ systemd-timers = ./timers.nix;
+}
diff --git a/home-manager/tests/modules/systemd/services-expected.conf b/home-manager/tests/modules/systemd/services-expected.conf
new file mode 100644
index 00000000000..34b9618d6d3
--- /dev/null
+++ b/home-manager/tests/modules/systemd/services-expected.conf
@@ -0,0 +1,5 @@
+[Service]
+ExecStart=/some/exec/start/command --with-arguments "%i"
+
+[Unit]
+Description=A basic test service
diff --git a/home-manager/tests/modules/systemd/services.nix b/home-manager/tests/modules/systemd/services.nix
new file mode 100644
index 00000000000..f1f7a42bb1a
--- /dev/null
+++ b/home-manager/tests/modules/systemd/services.nix
@@ -0,0 +1,23 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ config = {
+ systemd.user.services."test-service@" = {
+ Unit = {
+ Description = "A basic test service";
+ };
+
+ Service = {
+ ExecStart = ''/some/exec/start/command --with-arguments "%i"'';
+ };
+ };
+
+ nmt.script = ''
+ local serviceFile=home-files/.config/systemd/user/test-service@.service
+ assertFileExists $serviceFile
+ assertFileContent $serviceFile ${./services-expected.conf}
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/systemd/session-variables-expected.conf b/home-manager/tests/modules/systemd/session-variables-expected.conf
new file mode 100644
index 00000000000..5b6e80bc32b
--- /dev/null
+++ b/home-manager/tests/modules/systemd/session-variables-expected.conf
@@ -0,0 +1,2 @@
+V_int=1
+V_str=2
diff --git a/home-manager/tests/modules/systemd/session-variables.nix b/home-manager/tests/modules/systemd/session-variables.nix
new file mode 100644
index 00000000000..7cfb4a6c7c3
--- /dev/null
+++ b/home-manager/tests/modules/systemd/session-variables.nix
@@ -0,0 +1,18 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ config = {
+ systemd.user.sessionVariables = {
+ V_int = 1;
+ V_str = "2";
+ };
+
+ nmt.script = ''
+ local envFile=home-files/.config/environment.d/10-home-manager.conf
+ assertFileExists $envFile
+ assertFileContent $envFile ${./session-variables-expected.conf}
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/systemd/timers-expected.conf b/home-manager/tests/modules/systemd/timers-expected.conf
new file mode 100644
index 00000000000..b19f044cc0b
--- /dev/null
+++ b/home-manager/tests/modules/systemd/timers-expected.conf
@@ -0,0 +1,8 @@
+[Install]
+WantedBy=timers.target
+
+[Timer]
+OnUnitActiveSec=1h 30m
+
+[Unit]
+Description=A basic test timer
diff --git a/home-manager/tests/modules/systemd/timers.nix b/home-manager/tests/modules/systemd/timers.nix
new file mode 100644
index 00000000000..eaa43f17695
--- /dev/null
+++ b/home-manager/tests/modules/systemd/timers.nix
@@ -0,0 +1,25 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+ config = {
+ systemd.user.timers.test-timer = {
+ Unit = { Description = "A basic test timer"; };
+
+ Timer = { OnUnitActiveSec = "1h 30m"; };
+
+ Install = { WantedBy = [ "timers.target" ]; };
+ };
+
+ nmt.script = ''
+ local unitDir=home-files/.config/systemd/user
+ local timerFile=$unitDir/test-timer.timer
+
+ assertFileExists $timerFile
+ assertFileContent $timerFile ${./timers-expected.conf}
+
+ assertFileExists $unitDir/timers.target.wants/test-timer.timer
+ '';
+ };
+}
diff --git a/home-manager/tests/modules/xresources/default.nix b/home-manager/tests/modules/xresources/default.nix
new file mode 100644
index 00000000000..afd15fbd300
--- /dev/null
+++ b/home-manager/tests/modules/xresources/default.nix
@@ -0,0 +1 @@
+{ xresources = ./xresources.nix; }
diff --git a/home-manager/tests/modules/xresources/xresources-expected.conf b/home-manager/tests/modules/xresources/xresources-expected.conf
new file mode 100644
index 00000000000..20b47e5080b
--- /dev/null
+++ b/home-manager/tests/modules/xresources/xresources-expected.conf
@@ -0,0 +1,5 @@
+Test*boolean1: true
+Test*boolean2: false
+Test*int: 10
+Test*list: list-str, true, false, 10
+Test*string: test-string
diff --git a/home-manager/tests/modules/xresources/xresources.nix b/home-manager/tests/modules/xresources/xresources.nix
new file mode 100644
index 00000000000..f73e326f31e
--- /dev/null
+++ b/home-manager/tests/modules/xresources/xresources.nix
@@ -0,0 +1,22 @@
+{ config, lib, ... }:
+
+with lib;
+
+{
+ config = {
+ xresources = {
+ properties = {
+ "Test*string" = "test-string";
+ "Test*boolean1" = true;
+ "Test*boolean2" = false;
+ "Test*int" = 10;
+ "Test*list" = [ "list-str" true false 10 ];
+ };
+ };
+
+ nmt.script = ''
+ assertFileExists home-files/.Xresources
+ assertFileContent home-files/.Xresources ${./xresources-expected.conf}
+ '';
+ };
+}