diff options
author | Katharina Fey <kookie@spacekookie.de> | 2019-10-05 12:06:29 +0000 |
---|---|---|
committer | Katharina Fey <kookie@spacekookie.de> | 2019-10-05 12:42:50 +0000 |
commit | 1148b1d122bc03e9a3665856c9b7bb96bd4e3994 (patch) | |
tree | 1a9586de593790e236349d5caa0abdff7f3f6856 /home-manager | |
parent | 919d4e75699aa4ba456fd2d3d416a0522c9c7294 (diff) | |
parent | 8bddc1adab0f7a51476f819fa2197353e8e1d136 (diff) |
Add 'home-manager/' from commit '8bddc1adab0f7a51476f819fa2197353e8e1d136'
git-subtree-dir: home-manager
git-subtree-mainline: 919d4e75699aa4ba456fd2d3d416a0522c9c7294
git-subtree-split: 8bddc1adab0f7a51476f819fa2197353e8e1d136
Diffstat (limited to 'home-manager')
279 files changed, 24516 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..73c00f30510 --- /dev/null +++ b/home-manager/.gitlab-ci.yml @@ -0,0 +1,37 @@ +image: nixos/nix:latest + +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..df9c468be64 --- /dev/null +++ b/home-manager/.travis.yml @@ -0,0 +1,12 @@ +language: nix + +os: + - linux + - osx + +before_script: + - mkdir -m 0755 -p /nix/var/nix/{profiles,gcroots}/per-user/$USER + +script: + - 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..156d0c9daf6 --- /dev/null +++ b/home-manager/CONTRIBUTING.md @@ -0,0 +1,158 @@ +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 should follow the [Nixpkgs syntax +guidelines][]. 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 + (without any condition) that has a message along the lines of + + > A new service is available: 'services.foo'. + + or + + > A new program configuration is available: 'program.foo'. + + depending on the type of module. + +[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 +[Nixpkgs syntax guidelines]: https://nixos.org/nixpkgs/manual/#sec-syntax diff --git a/home-manager/FAQ.md b/home-manager/FAQ.md new file mode 100644 index 00000000000..141ecc6ea6b --- /dev/null +++ b/home-manager/FAQ.md @@ -0,0 +1,121 @@ +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. 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..58593f7b374 --- /dev/null +++ b/home-manager/README.md @@ -0,0 +1,314 @@ +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. + +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.03 (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.03.tar.gz home-manager + $ nix-channel --update + ``` + + if you follow a Nixpkgs version 19.03 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..1e3ff09e0bc --- /dev/null +++ b/home-manager/default.nix @@ -0,0 +1,13 @@ +{ 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..acfa1f1e49c --- /dev/null +++ b/home-manager/doc/default.nix @@ -0,0 +1,72 @@ +{ pkgs }: + +let + + lib = pkgs.lib; + + nmdSrc = pkgs.fetchFromGitLab { + name = "nmd"; + owner = "rycee"; + repo = "nmd"; + rev = "9751ca5ef6eb2ef27470010208d4c0a20e89443d"; + sha256 = "0rbx10n8kk0bvp1nl5c8q79lz1w0p1b8103asbvwps3gmqd070hi"; + }; + + nmd = import nmdSrc { inherit 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; } + ++ [ 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..0e4c904e570 --- /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 '<nixpkgs>' -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.03.tar.gz home-manager</userinput> +<prompt>$</prompt> <userinput>nix-channel --update</userinput> +</screen> + <para> + if you follow a Nixpkgs version 19.03 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 '<home-manager>' -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"> +. "$HOME/.nix-profile/etc/profile.d/hm-session-vars.sh" +</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.03.tar.gz home-manager</userinput> +<prompt>#</prompt> <userinput>nix-channel --update</userinput> +</screen> + + <para> + if you follow a Nixpkgs version 19.03 channel. + </para> + + <para> + It is then possible to add + </para> + +<programlisting language="nix"> +imports = [ <home-manager/nixos> ]; +</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.03.tar.gz home-manager</userinput> +<prompt>#</prompt> <userinput>nix-channel --update</userinput> +</screen> + + <para> + if you follow a Nixpkgs version 19.03 channel. + </para> + + <para> + It is then possible to add + </para> + +<programlisting language="nix"> +imports = [ <home-manager/nix-darwin> ]; +</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..a0f55d1106a --- /dev/null +++ b/home-manager/doc/man-home-manager.xml @@ -0,0 +1,497 @@ +<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> + <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><nixpkgs></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>-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..4675f0bfa5c --- /dev/null +++ b/home-manager/doc/release-notes/release-notes.xml @@ -0,0 +1,14 @@ +<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-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..a33d629767e --- /dev/null +++ b/home-manager/doc/release-notes/rl-1909.adoc @@ -0,0 +1,19 @@ +[[sec-release-19.09]] +== Release 19.09 (unreleased) + +This is the current unstable branch and the information in this +section is therefore not final. + +[[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/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..6fe4f13c7f0 --- /dev/null +++ b/home-manager/home-manager/home-manager @@ -0,0 +1,577 @@ +#!@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 + 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) + 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..c58c0ba7478 --- /dev/null +++ b/home-manager/home-manager/install.nix @@ -0,0 +1,60 @@ +{ 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; + } + 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..cc9d0dc2813 --- /dev/null +++ b/home-manager/modules/accounts/email.nix @@ -0,0 +1,422 @@ +{ 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/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..9f6404ff9cc --- /dev/null +++ b/home-manager/modules/default.nix @@ -0,0 +1,56 @@ +{ 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 "[1;31mwarning: ${w}[0m" x; + in + fold f res res.config.warnings; + + rawModule = lib.evalModules { + modules = + [ configuration ] + ++ (import ./modules.nix { inherit check lib pkgs; }); + 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..ac946976faf --- /dev/null +++ b/home-manager/modules/files.nix @@ -0,0 +1,297 @@ +{ pkgs, config, lib, ... }: + +with lib; + +let + + cfg = config.home.file; + + dag = config.lib.dag; + + 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; }; + + # A symbolic link whose target path matches this pattern will be + # considered part of a Home Manager generation. + homeFilePattern = "${builtins.storeDir}/*-home-manager-files/*"; + +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 = dag.entryBefore ["writeBoundary"] ( + let + check = pkgs.writeText "check" '' + . ${./lib-bash/color-echo.sh} + + 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 = 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} + + 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 = 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 = 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 + + 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 "$out/$relTarget")" + + # Target path must be within $HOME. + if [[ ! $target == $out* ]] ; 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..f12e86d4104 --- /dev/null +++ b/home-manager/modules/home-environment.nix @@ -0,0 +1,438 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.home; + + dag = config.lib.dag; + + 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 { + internal = true; + default = {}; + type = types.attrs; + description = '' + Activation scripts for the home environment. + </para><para> + Any script should respect the <varname>DRY_RUN</varname> + variable, if it is set then no actual action should be taken. + The variable <varname>DRY_RUN_CMD</varname> is set to + <code>echo</code> if dry run is enabled. Thus, many cases you + can use the idiom <code>$DRY_RUN_CMD rm -rf /</code>. + ''; + }; + + 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 = 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 = 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 = 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..535dec35ad4 --- /dev/null +++ b/home-manager/modules/lib/dag.nix @@ -0,0 +1,124 @@ +# 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..754713cab5d --- /dev/null +++ b/home-manager/modules/lib/default.nix @@ -0,0 +1,23 @@ +{ lib }: + +{ + 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; }; + + 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..efbb33b231d --- /dev/null +++ b/home-manager/modules/lib/file-type.nix @@ -0,0 +1,102 @@ +{ homeDirectory, lib, pkgs }: + +with lib; + +let + + stringsExtra = import ./strings.nix { inherit lib; }; + +in + +{ + # 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 = stringsExtra.storeFileName name; + }) + ); + }; + } + )); +} diff --git a/home-manager/modules/lib/shell.nix b/home-manager/modules/lib/shell.nix new file mode 100644 index 00000000000..f1443c5466a --- /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/strings.nix b/home-manager/modules/lib/strings.nix new file mode 100644 index 00000000000..13d6bb03be6 --- /dev/null +++ b/home-manager/modules/lib/strings.nix @@ -0,0 +1,27 @@ +{ 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.nix b/home-manager/modules/lib/types.nix new file mode 100644 index 00000000000..1b514d20a82 --- /dev/null +++ b/home-manager/modules/lib/types.nix @@ -0,0 +1,28 @@ +{ lib }: + +with lib; + +{ + + 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..1d3e96b54bb --- /dev/null +++ b/home-manager/modules/lib/zsh.nix @@ -0,0 +1,28 @@ +{ 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..e4c127c0d47 --- /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 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><profile directory>/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..ef87f8972ff --- /dev/null +++ b/home-manager/modules/misc/dconf.nix @@ -0,0 +1,89 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.dconf; + dag = config.lib.dag; + + 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 = 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..8dbcce53c22 --- /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..a0907545314 --- /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..4949b757fff --- /dev/null +++ b/home-manager/modules/misc/news.nix @@ -0,0 +1,1212 @@ +{ 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'. + ''; + } + ]; + }; +} diff --git a/home-manager/modules/misc/nixpkgs.nix b/home-manager/modules/misc/nixpkgs.nix new file mode 100644 index 00000000000..e7c0d8f25ea --- /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 = _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..77149d123ec --- /dev/null +++ b/home-manager/modules/misc/numlock.nix @@ -0,0 +1,35 @@ +{ 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..6ace2bfdaac --- /dev/null +++ b/home-manager/modules/misc/pam.nix @@ -0,0 +1,36 @@ +{ 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..60de8774231 --- /dev/null +++ b/home-manager/modules/misc/qt.nix @@ -0,0 +1,77 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.qt; + dag = config.lib.dag; + +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 = 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..18bb28f7603 --- /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" ]; + 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..979c7ea48be --- /dev/null +++ b/home-manager/modules/misc/xdg-mime-apps.nix @@ -0,0 +1,92 @@ +{ 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-user-dirs.nix b/home-manager/modules/misc/xdg-user-dirs.nix new file mode 100644 index 00000000000..4d034d7fe43 --- /dev/null +++ b/home-manager/modules/misc/xdg-user-dirs.nix @@ -0,0 +1,100 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.xdg.userDirs; + +in + +{ + meta.maintainers = with maintainers; [ pacien ]; + + 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."; + }; + + publishShare = 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.publishShare; + 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..2dc89ebc012 --- /dev/null +++ b/home-manager/modules/modules.nix @@ -0,0 +1,167 @@ +{ pkgs +, 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-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/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/pidgin.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/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/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/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/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/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/window-managers/awesome.nix { }) + (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 <nixpkgs/nixos/modules/misc/assertions.nix> { }) + (loadModule <nixpkgs/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 = import ./lib { inherit lib; }; + 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..84675cb1c8a --- /dev/null +++ b/home-manager/modules/programs/alacritty.nix @@ -0,0 +1,53 @@ +{ 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..8f3ffdfb31e --- /dev/null +++ b/home-manager/modules/programs/alot-accounts.nix @@ -0,0 +1,61 @@ +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..bc94a301db0 --- /dev/null +++ b/home-manager/modules/programs/astroid-accounts.nix @@ -0,0 +1,33 @@ +{ 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..0463cd15528 --- /dev/null +++ b/home-manager/modules/programs/astroid.nix @@ -0,0 +1,139 @@ +{ 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 = folders.drafts; + save_sent = "true"; + save_sent_to = 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 { + assertions = [ + { + assertion = config.programs.notmuch.maildir.synchronizeFlags; + message = "The astroid module requires" + + " 'programs.notmuch.maildir.synchronizeFlags = true'."; + } + ]; + + 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..4514a5b5d58 --- /dev/null +++ b/home-manager/modules/programs/autorandr.nix @@ -0,0 +1,349 @@ +{ 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..860c5e82f54 --- /dev/null +++ b/home-manager/modules/programs/bat.nix @@ -0,0 +1,40 @@ +{ 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..152bfd304a4 --- /dev/null +++ b/home-manager/modules/programs/beets.nix @@ -0,0 +1,48 @@ +{ 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. + ''; + }; + + settings = mkOption { + type = types.attrs; + default = {}; + description = '' + Configuration written to + <filename>~/.config/beets/config.yaml</filename> + ''; + }; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ pkgs.beets ]; + + 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..f6d3cd7f920 --- /dev/null +++ b/home-manager/modules/programs/broot.nix @@ -0,0 +1,261 @@ +{ 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..7af5e8f8756 --- /dev/null +++ b/home-manager/modules/programs/browserpass.nix @@ -0,0 +1,80 @@ +{ 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 = builtins.concatLists (with pkgs.stdenv; map (x: + if x == "chrome" then + let dir = if isDarwin + then "Library/Application Support/Google/Chrome/NativeMessagingHosts" + else ".config/google-chrome/NativeMessagingHosts"; + in [ + { + target = "${dir}/com.github.browserpass.native.json"; + source = "${pkgs.browserpass}/lib/browserpass/hosts/chromium/com.github.browserpass.native.json"; + } + { + target = "${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 [ + { + target = "${dir}/com.github.browserpass.native.json"; + source = "${pkgs.browserpass}/lib/browserpass/hosts/chromium/com.github.browserpass.native.json"; + } + { + target = "${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 + [ { + target = (if isDarwin + then "Library/Application Support/Mozilla/NativeMessagingHosts" + else ".mozilla/native-messaging-hosts") + + "/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 [ + { + target = "${dir}/com.github.browserpass.native.json"; + source = "${pkgs.browserpass}/lib/browserpass/hosts/chromium/com.github.browserpass.native.json"; + } + { + target = "${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..83a827a0ed0 --- /dev/null +++ b/home-manager/modules/programs/chromium.nix @@ -0,0 +1,93 @@ +{ 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: { + target = "${configDir}/External Extensions/${ext}.json"; + text = builtins.toJSON { + external_update_url = "https://clients2.google.com/service/update2/crx"; + }; + }; + + in + mkIf cfg.enable { + home.packages = [ cfg.package ]; + home.file = 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..0053fe36ad7 --- /dev/null +++ b/home-manager/modules/programs/command-not-found/command-not-found.nix @@ -0,0 +1,57 @@ +# 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..e4c17239c58 --- /dev/null +++ b/home-manager/modules/programs/direnv.nix @@ -0,0 +1,107 @@ +{ 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..4a432c9fe1a --- /dev/null +++ b/home-manager/modules/programs/eclipse.nix @@ -0,0 +1,54 @@ +{ 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..78c136c9868 --- /dev/null +++ b/home-manager/modules/programs/emacs.nix @@ -0,0 +1,79 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + hmTypes = import ../lib/types.nix { inherit lib; }; + + cfg = config.programs.emacs; + + # Copied from all-packages.nix, with modifications to support + # overrides. + emacsPackages = + let + epkgs = pkgs.emacsPackagesNgGen 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 = hmTypes.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 '<nixpkgs>' -qaP -A emacsPackagesNg</command>. + ''; + }; + + overrides = mkOption { + default = self: super: {}; + type = hmTypes.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..4342181fa4a --- /dev/null +++ b/home-manager/modules/programs/feh.nix @@ -0,0 +1,41 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.feh; + + disableBinding = func: key: func; + enableBinding = func: key: "${func} ${key}"; + +in + +{ + options.programs.feh = { + enable = mkEnableOption "feh - a fast and light image viewer"; + + keybindings = mkOption { + default = {}; + type = types.attrsOf types.str; + example = { zoom_in = "plus"; zoom_out = "minus"; }; + description = '' + Set keybindings. + See <link xlink:href="https://man.finalrewind.org/1/feh/#x4b455953"/> for + default bindings and available commands. + ''; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ pkgs.feh ]; + + xdg.configFile."feh/keys".text = '' + # Disable default keybindings + ${concatStringsSep "\n" (mapAttrsToList disableBinding cfg.keybindings)} + + # Enable new keybindings + ${concatStringsSep "\n" (mapAttrsToList enableBinding cfg.keybindings)} + ''; + }; +} diff --git a/home-manager/modules/programs/firefox.nix b/home-manager/modules/programs/firefox.nix new file mode 100644 index 00000000000..708b05417d6 --- /dev/null +++ b/home-manager/modules/programs/firefox.nix @@ -0,0 +1,282 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.firefox; + + extensionPath = "extensions/{ec8030f7-c20a-464f-9b0e-13a3a9e97384}"; + + profiles = + flip mapAttrs' cfg.profiles (_: profile: + nameValuePair "Profile${toString profile.id}" { + Name = profile.name; + Path = 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 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 ( + [{ + ".mozilla/${extensionPath}" = mkIf (cfg.extensions != []) ( + let + extensionsEnv = pkgs.buildEnv { + name = "hm-firefox-extensions"; + paths = cfg.extensions; + }; + in { + source = "${extensionsEnv}/share/mozilla/${extensionPath}"; + recursive = true; + } + ); + + ".mozilla/firefox/profiles.ini" = mkIf (cfg.profiles != {}) { + text = profilesIni; + }; + }] + ++ flip mapAttrsToList cfg.profiles (_: profile: { + ".mozilla/firefox/${profile.path}/chrome/userChrome.css" = + mkIf (profile.userChrome != "") { + text = profile.userChrome; + }; + + ".mozilla/firefox/${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..832c0bfa3e4 --- /dev/null +++ b/home-manager/modules/programs/fzf.nix @@ -0,0 +1,138 @@ +{ 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..32e1312dc8f --- /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..8c1ac5e021e --- /dev/null +++ b/home-manager/modules/programs/getmail.nix @@ -0,0 +1,59 @@ +{ 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} + 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 = map (a: + { target = 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..913f86f71ce --- /dev/null +++ b/home-manager/modules/programs/git.nix @@ -0,0 +1,325 @@ +{ 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..9a44364491d --- /dev/null +++ b/home-manager/modules/programs/gnome-terminal.nix @@ -0,0 +1,242 @@ +{ 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..06c25c9b82a --- /dev/null +++ b/home-manager/modules/programs/go.nix @@ -0,0 +1,75 @@ +{ 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 = "GOPATH relative to HOME"; + }; + + 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: { + target = "${goPath}/src/${n}"; + source = v; + }; + in + mapAttrsToList mkSrc cfg.packages; + } + (mkIf (cfg.goPath != null) { + home.sessionVariables.GOPATH = builtins.toPath "${config.home.homeDirectory}/${cfg.goPath}"; + }) + (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..42e3c8a384f --- /dev/null +++ b/home-manager/modules/programs/home-manager.nix @@ -0,0 +1,42 @@ +{ 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..d700c4855fe --- /dev/null +++ b/home-manager/modules/programs/htop.nix @@ -0,0 +1,327 @@ +{ 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 = types.coercedTo + (types.enum (attrNames meters)) + (m: { kind = m; mode = meters.${m}; }) + (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)."; + }; + }; + }); + + 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..93dcaf474af --- /dev/null +++ b/home-manager/modules/programs/info.nix @@ -0,0 +1,78 @@ +# 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; + + dag = config.lib.dag; + + # 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 = 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..56c3adf0654 --- /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..e48f0e295a8 --- /dev/null +++ b/home-manager/modules/programs/kakoune.nix @@ -0,0 +1,574 @@ +{ 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 + + 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..6dbf83a872e --- /dev/null +++ b/home-manager/modules/programs/keychain.nix @@ -0,0 +1,88 @@ +{ 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 = '' + eval "$(${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. + ''; + }; + + 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 shellCommand; + programs.zsh.initExtra = mkIf cfg.enableZshIntegration 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..5e145e8c69b --- /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; + }; +} 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..48ff6e60d68 --- /dev/null +++ b/home-manager/modules/programs/matplotlib.nix @@ -0,0 +1,64 @@ +{ 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..c586481df4d --- /dev/null +++ b/home-manager/modules/programs/mbsync-accounts.nix @@ -0,0 +1,107 @@ +{ 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..908a1add715 --- /dev/null +++ b/home-manager/modules/programs/mbsync.nix @@ -0,0 +1,188 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + dag = config.lib.dag; + + 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.\n" ] + ++ optional (cfg.extraConfig != "") cfg.extraConfig + ++ accountsConfig + ++ groupsConfig + ) + "\n"; + + home.activation.createMaildir = + 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..fa6e7b3e5ba --- /dev/null +++ b/home-manager/modules/programs/mercurial.nix @@ -0,0 +1,102 @@ +{ 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\n" + concatStringsSep "\n" cfg.ignores + "\n" + + "syntax: regexp\n" + 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..1051f71ccd6 --- /dev/null +++ b/home-manager/modules/programs/mpv.nix @@ -0,0 +1,152 @@ +{ 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 = types.listOf types.package; + 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..277710f4cba --- /dev/null +++ b/home-manager/modules/programs/msmtp-accounts.nix @@ -0,0 +1,47 @@ +{ 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..1ff3139ef36 --- /dev/null +++ b/home-manager/modules/programs/msmtp.nix @@ -0,0 +1,79 @@ +{ 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 "\naccount 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/neovim.nix b/home-manager/modules/programs/neovim.nix new file mode 100644 index 00000000000..dadda2c7118 --- /dev/null +++ b/home-manager/modules/programs/neovim.nix @@ -0,0 +1,212 @@ +{ 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 deprecated. Please use the options <varname>extraConfig</varname> + and <varname>plugins</varname> which are mutually exclusive with this option. + ''; + }; + + 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."; + } + ]; + + warnings = optional (cfg.configure != {}) '' + The programs.neovim.configure option is deprecated. Please use + extraConfig and package option. + ''; + + 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..84c64dfa607 --- /dev/null +++ b/home-manager/modules/programs/newsboat.nix @@ -0,0 +1,98 @@ +{ 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.attrs; + default = []; + example = [{url = "http://example.com"; tags = ["foo" "bar"];}]; + description = "List of urls and tokens."; + }; + + 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 + urls = builtins.concatStringsSep "\n" ( + map (u: builtins.concatStringsSep " " ([u.url] ++ (map wrapQuote u.tags))) + cfg.urls); + queries = builtins.concatStringsSep "\n" ( + mapAttrsToList (n: v: "\"query:${n}:${escape ["\""] v}\"") cfg.queries); + + in + + '' + ${urls} + + ${queries} + ''; + 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..476c2eb1978 --- /dev/null +++ b/home-manager/modules/programs/noti.nix @@ -0,0 +1,53 @@ +{ 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..7c9c93d3f95 --- /dev/null +++ b/home-manager/modules/programs/notmuch-accounts.nix @@ -0,0 +1,7 @@ +{ 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..cd0b1384ad9 --- /dev/null +++ b/home-manager/modules/programs/notmuch.nix @@ -0,0 +1,211 @@ +{ 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.\n\n" + + toIni notmuchIni; + + home.file = + let + hook = name: cmds: + { + target = "${notmuchIni.database.path}/.notmuch/hooks/${name}"; + source = pkgs.writeScript name '' + #!${pkgs.runtimeShell} + + export PATH="${pkgs.notmuch}/bin''${PATH:+:}$PATH" + export NOTMUCH_CONFIG="${config.xdg.configHome}/notmuch/notmuchrc" + export NMBGIT="${config.xdg.dataHome}/notmuch/nmbug" + + ${cmds} + ''; + executable = true; + }; + in + optional (cfg.hooks.preNew != "") + (hook "pre-new" cfg.hooks.preNew) + ++ + optional (cfg.hooks.postNew != "") + (hook "post-new" cfg.hooks.postNew) + ++ + optional (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..f0dfecb63cc --- /dev/null +++ b/home-manager/modules/programs/obs-studio.nix @@ -0,0 +1,53 @@ +{ 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..015a5974ab3 --- /dev/null +++ b/home-manager/modules/programs/offlineimap-accounts.nix @@ -0,0 +1,57 @@ +{ 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..82143b630ad --- /dev/null +++ b/home-manager/modules/programs/offlineimap.nix @@ -0,0 +1,207 @@ +{ 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..4de2e82da55 --- /dev/null +++ b/home-manager/modules/programs/opam.nix @@ -0,0 +1,52 @@ +{ 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/pidgin.nix b/home-manager/modules/programs/pidgin.nix new file mode 100644 index 00000000000..8dcb2122172 --- /dev/null +++ b/home-manager/modules/programs/pidgin.nix @@ -0,0 +1,36 @@ +{ 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/rofi.nix b/home-manager/modules/programs/rofi.nix new file mode 100644 index 00000000000..e64e5d4782e --- /dev/null +++ b/home-manager/modules/programs/rofi.nix @@ -0,0 +1,339 @@ +{ 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..6300969a519 --- /dev/null +++ b/home-manager/modules/programs/rtorrent.nix @@ -0,0 +1,37 @@ +{ 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..de1bff30fce --- /dev/null +++ b/home-manager/modules/programs/skim.nix @@ -0,0 +1,128 @@ +{ 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..ab61c0dcbc4 --- /dev/null +++ b/home-manager/modules/programs/ssh.nix @@ -0,0 +1,457 @@ +{ 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 = types.nullOr types.path; + default = null; + description = '' + Specifies a file 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.certificateFile != null) " CertificateFile ${cf.certificateFile}" + ++ 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 (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..81793c7a6f6 --- /dev/null +++ b/home-manager/modules/programs/starship.nix @@ -0,0 +1,91 @@ +{ 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"; + + 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 = [ pkgs.starship ]; + + xdg.configFile."starship.toml" = mkIf (cfg.settings != {}) { + source = configFile cfg.settings; + }; + + programs.bash.initExtra = mkIf cfg.enableBashIntegration '' + if [[ -z $INSIDE_EMACS ]]; then + eval "$(${pkgs.starship}/bin/starship init bash)" + fi + ''; + + programs.zsh.initExtra = mkIf cfg.enableZshIntegration '' + if [ -z "$INSIDE_EMACS" ]; then + eval "$(${pkgs.starship}/bin/starship init zsh)" + fi + ''; + + programs.fish.shellInit = mkIf cfg.enableFishIntegration '' + if test -z "$INSIDE_EMACS" + eval (${pkgs.starship}/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..eeacc77da29 --- /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..6eab39edb3a --- /dev/null +++ b/home-manager/modules/programs/termite.nix @@ -0,0 +1,378 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.termite; + + vteInitStr = '' + # See https://github.com/thestinger/termite#id1 + if [[ $TERM == xterm-termite ]]; then + . ${pkgs.gnome3.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 = "Scroll to the bottom when the shell generates output."; + }; + + 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..0f8953e9f91 --- /dev/null +++ b/home-manager/modules/programs/texlive.nix @@ -0,0 +1,50 @@ +{ 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..6f4eb3ff7ba --- /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..a14a9562ac1 --- /dev/null +++ b/home-manager/modules/programs/vim.nix @@ -0,0 +1,191 @@ +{ 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 '<nixpkgs>' -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..3a82816f588 --- /dev/null +++ b/home-manager/modules/programs/vscode.nix @@ -0,0 +1,58 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.vscode; + + configFilePath = + if pkgs.stdenv.hostPlatform.isDarwin then + "Library/Application Support/Code/User/settings.json" + else + "${config.xdg.configHome}/Code/User/settings.json"; + +in + +{ + options = { + programs.vscode = { + enable = mkEnableOption "Visual Studio Code"; + + 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 = [ + (pkgs.vscode-with-extensions.override { + vscodeExtensions = cfg.extensions; + }) + ]; + + home.file."${configFilePath}".text = builtins.toJSON cfg.userSettings; + }; +} diff --git a/home-manager/modules/programs/vscode/haskell.nix b/home-manager/modules/programs/vscode/haskell.nix new file mode 100644 index 00000000000..c8ea10d473e --- /dev/null +++ b/home-manager/modules/programs/vscode/haskell.nix @@ -0,0 +1,66 @@ +{ 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..245eff6a51e --- /dev/null +++ b/home-manager/modules/programs/z-lua.nix @@ -0,0 +1,86 @@ +{ 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..f01bd501c39 --- /dev/null +++ b/home-manager/modules/programs/zathura.nix @@ -0,0 +1,61 @@ +{ 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}\t\"${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..ffe5f4960b6 --- /dev/null +++ b/home-manager/modules/programs/zsh.nix @@ -0,0 +1,439 @@ +{ 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"; + }; + + 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 = relToDotDir ".zsh_history"; + defaultText = ".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. + ''; + }; + + 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}" + HISTFILE="$HOME/${cfg.history.path}" + SAVEHIST="${toString cfg.history.save}" + + setopt HIST_FCNTL_LOCK + ${if cfg.history.ignoreDups then "setopt" else "unsetopt"} HIST_IGNORE_DUPS + ${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 = map (plugin: { + target = "${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..186dc7454f9 --- /dev/null +++ b/home-manager/modules/services/blueman-applet.nix @@ -0,0 +1,37 @@ +{ config, lib, pkgs, ... }: + +with lib; + +{ + options = { + services.blueman-applet = { + enable = mkEnableOption '' + Blueman applet. + + Note, for the applet to work, 'blueman' package should also be installed system-wide + since it requires running 'blueman-mechanism' service activated via dbus. + You can add it to the dbus packages in system configuration: + + services.dbus.packages = [ pkgs.blueman ]; + ''; + }; + }; + + 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/compton.nix b/home-manager/modules/services/compton.nix new file mode 100644 index 00000000000..c227f0a8c07 --- /dev/null +++ b/home-manager/modules/services/compton.nix @@ -0,0 +1,311 @@ +{ 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..96b1f71a2fa --- /dev/null +++ b/home-manager/modules/services/dunst.nix @@ -0,0 +1,172 @@ +{ 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..2b010cec1e1 --- /dev/null +++ b/home-manager/modules/services/dwm-status.nix @@ -0,0 +1,70 @@ +{ 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..33d6871c61b --- /dev/null +++ b/home-manager/modules/services/emacs.nix @@ -0,0 +1,48 @@ +{ 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..87b494d0fcd --- /dev/null +++ b/home-manager/modules/services/flameshot.nix @@ -0,0 +1,47 @@ +{ 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..46d4c1752d4 --- /dev/null +++ b/home-manager/modules/services/getmail.nix @@ -0,0 +1,61 @@ +{ 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..4ca6c7cacf2 --- /dev/null +++ b/home-manager/modules/services/gnome-keyring.nix @@ -0,0 +1,55 @@ +{ 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..5dc942fef63 --- /dev/null +++ b/home-manager/modules/services/gpg-agent.nix @@ -0,0 +1,258 @@ +{ 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. + ''; + }; + }; + }; + + 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}" + ++ + [ 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/hound.nix b/home-manager/modules/services/hound.nix new file mode 100644 index 00000000000..a252a68d271 --- /dev/null +++ b/home-manager/modules/services/hound.nix @@ -0,0 +1,84 @@ +{ 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..1c780bf28c3 --- /dev/null +++ b/home-manager/modules/services/imapnotify-accounts.nix @@ -0,0 +1,30 @@ +{ 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..fbb0713e978 --- /dev/null +++ b/home-manager/modules/services/imapnotify.nix @@ -0,0 +1,107 @@ +{ 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..bd698fcf836 --- /dev/null +++ b/home-manager/modules/services/kdeconnect.nix @@ -0,0 +1,75 @@ +{ 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..ad791786f05 --- /dev/null +++ b/home-manager/modules/services/keepassx.nix @@ -0,0 +1,31 @@ +{ 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/mbsync.nix b/home-manager/modules/services/mbsync.nix new file mode 100644 index 00000000000..73c3b326695 --- /dev/null +++ b/home-manager/modules/services/mbsync.nix @@ -0,0 +1,110 @@ +{ 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..450f84c5912 --- /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 = { + Unit = { + Description = "MPRIS 2 support for MPD"; + After = [ "graphical-session-pre.target" "mpd.service" ]; + PartOf = [ "graphical-session.target" ]; + }; + + Service = { + Type = "simple"; + ExecStart = "${cfg.package}/bin/mpDris2"; + }; + }; + }; +} diff --git a/home-manager/modules/services/muchsync.nix b/home-manager/modules/services/muchsync.nix new file mode 100644 index 00000000000..72bf737c27d --- /dev/null +++ b/home-manager/modules/services/muchsync.nix @@ -0,0 +1,211 @@ +{ 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..72a4711e39a --- /dev/null +++ b/home-manager/modules/services/network-manager-applet.nix @@ -0,0 +1,42 @@ +{ 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..3d8dc0bc80b --- /dev/null +++ b/home-manager/modules/services/nextcloud-client.nix @@ -0,0 +1,30 @@ +{ 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..d98a508f088 --- /dev/null +++ b/home-manager/modules/services/owncloud-client.nix @@ -0,0 +1,30 @@ +{ 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..455989ffe07 --- /dev/null +++ b/home-manager/modules/services/parcellite.nix @@ -0,0 +1,47 @@ +{ 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 = { + # PATH have been added in nixpkgs.parcellite, keeping it here for + # backward compatibility. XDG_DATA_DIRS is necessary to make it pick up + # icons correctly. + Environment = '' + PATH=${package}/bin:${pkgs.which}/bin:${pkgs.xdotool}/bin XDG_DATA_DIRS=${pkgs.hicolor_icon_theme}/share + ''; + ExecStart = "${package}/bin/parcellite"; + Restart = "on-abort"; + }; + }; + }; +} diff --git a/home-manager/modules/services/pasystray.nix b/home-manager/modules/services/pasystray.nix new file mode 100644 index 00000000000..8f92f34c091 --- /dev/null +++ b/home-manager/modules/services/pasystray.nix @@ -0,0 +1,36 @@ +{ 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..4225ed9b38c --- /dev/null +++ b/home-manager/modules/services/polybar.nix @@ -0,0 +1,143 @@ +{ 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..cbec97ae7cb --- /dev/null +++ b/home-manager/modules/services/random-background.nix @@ -0,0 +1,103 @@ +{ 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 "random desktop background"; + + 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..1452fcc95ed --- /dev/null +++ b/home-manager/modules/services/redshift.nix @@ -0,0 +1,172 @@ +# 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..242e03432e8 --- /dev/null +++ b/home-manager/modules/services/rsibreak.nix @@ -0,0 +1,36 @@ +{ 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..e3da14069bc --- /dev/null +++ b/home-manager/modules/services/screen-locker.nix @@ -0,0 +1,76 @@ +{ 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); + }; + }; + + # xss-lock will run specified screen locker when the session is locked via loginctl + # can't be started as a systemd service, + # see https://bitbucket.org/raymonad/xss-lock/issues/13/allow-operation-as-systemd-user-unit + xsession.initExtra = "${pkgs.xss-lock}/bin/xss-lock ${concatStringsSep " " cfg.xssLockExtraOptions} -- ${cfg.lockCmd} &"; + }; + +} diff --git a/home-manager/modules/services/stalonetray.nix b/home-manager/modules/services/stalonetray.nix new file mode 100644 index 00000000000..934e78c99a1 --- /dev/null +++ b/home-manager/modules/services/stalonetray.nix @@ -0,0 +1,94 @@ +{ 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}\n") 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..8a2ded8720a --- /dev/null +++ b/home-manager/modules/services/status-notifier-watcher.nix @@ -0,0 +1,46 @@ +{ 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..7fc556c5234 --- /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..69531a19dc9 --- /dev/null +++ b/home-manager/modules/services/taffybar.nix @@ -0,0 +1,48 @@ +{ 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..bb7be8d7db9 --- /dev/null +++ b/home-manager/modules/services/tahoe-lafs.nix @@ -0,0 +1,25 @@ +{ 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..4179ac8aa85 --- /dev/null +++ b/home-manager/modules/services/taskwarrior-sync.nix @@ -0,0 +1,58 @@ +{ 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..c058a23de6c --- /dev/null +++ b/home-manager/modules/services/udiskie.nix @@ -0,0 +1,95 @@ +{ 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..6b5ac866ec5 --- /dev/null +++ b/home-manager/modules/services/unclutter.nix @@ -0,0 +1,63 @@ +{ 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/window-managers/awesome.nix b/home-manager/modules/services/window-managers/awesome.nix new file mode 100644 index 00000000000..fe914864e2a --- /dev/null +++ b/home-manager/modules/services/window-managers/awesome.nix @@ -0,0 +1,57 @@ +{ 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/i3.nix b/home-manager/modules/services/window-managers/i3.nix new file mode 100644 index 00000000000..6c52ff6c335 --- /dev/null +++ b/home-manager/modules/services/window-managers/i3.nix @@ -0,0 +1,836 @@ +{ 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. + ''; + }; + + 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}+1" = "workspace 1"; + "${cfg.config.modifier}+2" = "workspace 2"; + "${cfg.config.modifier}+3" = "workspace 3"; + "${cfg.config.modifier}+4" = "workspace 4"; + "${cfg.config.modifier}+5" = "workspace 5"; + "${cfg.config.modifier}+6" = "workspace 6"; + "${cfg.config.modifier}+7" = "workspace 7"; + "${cfg.config.modifier}+8" = "workspace 8"; + "${cfg.config.modifier}+9" = "workspace 9"; + + "${cfg.config.modifier}+Shift+1" = "move container to workspace 1"; + "${cfg.config.modifier}+Shift+2" = "move container to workspace 2"; + "${cfg.config.modifier}+Shift+3" = "move container to workspace 3"; + "${cfg.config.modifier}+Shift+4" = "move container to workspace 4"; + "${cfg.config.modifier}+Shift+5" = "move container to workspace 5"; + "${cfg.config.modifier}+Shift+6" = "move container to workspace 6"; + "${cfg.config.modifier}+Shift+7" = "move container to workspace 7"; + "${cfg.config.modifier}+Shift+8" = "move container to workspace 8"; + "${cfg.config.modifier}+Shift+9" = "move container to workspace 9"; + + "${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} + + 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..6b3426b963b --- /dev/null +++ b/home-manager/modules/services/window-managers/xmonad.nix @@ -0,0 +1,103 @@ +{ 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..26115a93062 --- /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..d9e5ae783f9 --- /dev/null +++ b/home-manager/modules/services/xembed-sni-proxy.nix @@ -0,0 +1,49 @@ +{ 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..4001c294e86 --- /dev/null +++ b/home-manager/modules/services/xscreensaver.nix @@ -0,0 +1,56 @@ +{ 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..22a5ca536a5 --- /dev/null +++ b/home-manager/modules/services/xsuspender.nix @@ -0,0 +1,198 @@ +{ 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..cdc4486f542 --- /dev/null +++ b/home-manager/modules/systemd.nix @@ -0,0 +1,258 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.systemd.user; + + dag = config.lib.dag; + + 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 = 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)}"; + in + '' + if ${ensureRuntimeDir} ${cfg.systemctlPath} --quiet --user is-system-running 2> /dev/null; then + ${ensureRuntimeDir} \ + PATH=${dirOf cfg.systemctlPath}:$PATH \ + ${if cfg.startServices then autoReloadCmd else legacyReloadCmd} + else + echo "User systemd daemon not running. Skipping reload." + fi + '' + ); + }) + ]; +} diff --git a/home-manager/modules/xcursor.nix b/home-manager/modules/xcursor.nix new file mode 100644 index 00000000000..171586028cd --- /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..384008e2450 --- /dev/null +++ b/home-manager/modules/xresources.nix @@ -0,0 +1,85 @@ +{ 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 = { + "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..e1cf9942e7c --- /dev/null +++ b/home-manager/modules/xsession.nix @@ -0,0 +1,176 @@ +{ 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..1482c7bb245 --- /dev/null +++ b/home-manager/nix-darwin/default.nix @@ -0,0 +1,71 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.home-manager; + + hmModule = types.submodule ({name, ...}: { + imports = import ../modules/modules.nix { inherit lib pkgs; }; + + 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..f53b4d4d6b7 --- /dev/null +++ b/home-manager/nixos/default.nix @@ -0,0 +1,119 @@ +{ config, lib, pkgs, utils, ... }: + +with lib; + +let + + cfg = config.home-manager; + + hmModule = types.submodule ({name, ...}: { + imports = import ../modules/modules.nix { inherit lib pkgs; }; + + 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..fa779fe0370 --- /dev/null +++ b/home-manager/overlay.nix @@ -0,0 +1,5 @@ +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..4bda0474f88 --- /dev/null +++ b/home-manager/tests/default.nix @@ -0,0 +1,52 @@ +{ pkgs ? import <nixpkgs> {} }: + +let + + nmt = pkgs.fetchFromGitLab { + owner = "rycee"; + repo = "nmt"; + rev = "89fb12a2aaa8ec671e22a033162c7738be714305"; + sha256 = "07yc1jkgw8vhskzk937k9hfba401q8rn4sgj9baw3fkjl9zrbcyf"; + }; + +in + +import nmt { + inherit pkgs; + modules = import ../modules/modules.nix { inherit pkgs; lib = pkgs.lib; }; + testedAttrPath = [ "home" "activationPackage" ]; + tests = { + browserpass = ./modules/programs/browserpass.nix; + files-executable = ./modules/files/executable.nix; + files-hidden-source = ./modules/files/hidden-source.nix; + files-source-with-spaces = ./modules/files/source-with-spaces.nix; + files-text = ./modules/files/text.nix; + git-with-email = ./modules/programs/git-with-email.nix; + git-with-most-options = ./modules/programs/git.nix; + git-with-str-extra-config = ./modules/programs/git-with-str-extra-config.nix; + mbsync = ./modules/programs/mbsync.nix; + texlive-minimal = ./modules/programs/texlive-minimal.nix; + xresources = ./modules/xresources.nix; + } + // pkgs.lib.optionalAttrs pkgs.stdenv.hostPlatform.isLinux ( + { + getmail = ./modules/programs/getmail.nix; + i3-keybindings = ./modules/services/window-managers/i3-keybindings.nix; + } + // import ./modules/misc/pam + // import ./modules/misc/xdg + // import ./modules/misc/xsession + // import ./modules/programs/firefox + // import ./modules/programs/rofi + // import ./modules/services/sxhkd + // import ./modules/systemd + ) + // import ./modules/home-environment + // import ./modules/misc/fontconfig + // import ./modules/programs/alacritty + // import ./modules/programs/bash + // import ./modules/programs/gpg + // import ./modules/programs/ssh + // import ./modules/programs/tmux + // import ./modules/programs/zsh; +} 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/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..a8635398f48 --- /dev/null +++ b/home-manager/tests/modules/files/hidden-source.nix @@ -0,0 +1,16 @@ +{ 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..8a64f831caf --- /dev/null +++ b/home-manager/tests/modules/misc/pam/default.nix @@ -0,0 +1,3 @@ +{ + 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..5772becd712 --- /dev/null +++ b/home-manager/tests/modules/misc/xdg/default.nix @@ -0,0 +1,3 @@ +{ + 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..2c32071064c --- /dev/null +++ b/home-manager/tests/modules/misc/xdg/mime-apps-basics.nix @@ -0,0 +1,30 @@ +{ 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..60623d1bf6b --- /dev/null +++ b/home-manager/tests/modules/misc/xsession/basic.nix @@ -0,0 +1,37 @@ +{ 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"; + }; + + 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 \ + ${pkgs.substituteAll { + src = ./basic-setxkbmap-expected.service; + inherit (pkgs.xorg) setxkbmap; + }} + ''; + }; +} 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..b7eb3decebb --- /dev/null +++ b/home-manager/tests/modules/misc/xsession/keyboard-without-layout.nix @@ -0,0 +1,33 @@ +{ 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"; + }; + + nmt.script = '' + assertFileExists home-files/.config/systemd/user/setxkbmap.service + assertFileContent \ + home-files/.config/systemd/user/setxkbmap.service \ + ${pkgs.substituteAll { + src = ./keyboard-without-layout-expected.service; + inherit (pkgs.xorg) setxkbmap; + }} + ''; + }; +} 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..f3f8486ad3d --- /dev/null +++ b/home-manager/tests/modules/programs/alacritty/empty-settings.nix @@ -0,0 +1,13 @@ +{ config, lib, pkgs, ... }: + +with lib; + +{ + config = { + programs.alacritty.enable = true; + + 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..2c84710d100 --- /dev/null +++ b/home-manager/tests/modules/programs/alacritty/example-settings.nix @@ -0,0 +1,32 @@ +{ 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"; + } + ]; + }; + }; + + 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..c586477ec4d --- /dev/null +++ b/home-manager/tests/modules/programs/bash/session-variables-expected.txt @@ -0,0 +1,8 @@ +# -*- mode: sh -*- + +. "@homeDirectory@/.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..a7a69a2a1f8 --- /dev/null +++ b/home-manager/tests/modules/programs/bash/session-variables.nix @@ -0,0 +1,28 @@ +{ config, lib, pkgs, ... }: + +with lib; + +{ + config = { + programs.bash = { + enable = true; + + sessionVariables = { + V1 = "v1"; + V2 = "v2-${config.programs.bash.sessionVariables.V1}"; + }; + }; + + nmt.script = '' + assertFileExists home-files/.profile + assertFileContent \ + home-files/.profile \ + ${ + pkgs.substituteAll { + src = ./session-variables-expected.txt; + inherit (config.home) homeDirectory; + } + } + ''; + }; +} diff --git a/home-manager/tests/modules/programs/browserpass.nix b/home-manager/tests/modules/programs/browserpass.nix new file mode 100644 index 00000000000..229392e171b --- /dev/null +++ b/home-manager/tests/modules/programs/browserpass.nix @@ -0,0 +1,35 @@ +{ 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/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..45465b1d0bf --- /dev/null +++ b/home-manager/tests/modules/programs/firefox/profile-settings.nix @@ -0,0 +1,24 @@ +{ config, lib, pkgs, ... }: + +with lib; + +{ + config = { + programs.firefox = { + enable = true; + profiles.test.settings = { + "general.smoothScroll" = false; + }; + }; + + 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..0c93096190e --- /dev/null +++ b/home-manager/tests/modules/programs/firefox/state-version-19_09.nix @@ -0,0 +1,17 @@ +{ config, lib, pkgs, ... }: + +with lib; + +{ + config = { + home.stateVersion = "19.09"; + + programs.firefox.enable = true; + + nmt.script = '' + assertFileRegex \ + home-path/bin/firefox \ + MOZ_APP_LAUNCHER + ''; + }; +} diff --git a/home-manager/tests/modules/programs/getmail-expected.conf b/home-manager/tests/modules/programs/getmail-expected.conf new file mode 100644 index 00000000000..da54e709236 --- /dev/null +++ b/home-manager/tests/modules/programs/getmail-expected.conf @@ -0,0 +1,15 @@ +# Generated by Home-Manager. +[retriever] +type = SimpleIMAPSSLRetriever +server = imap.example.com +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.nix b/home-manager/tests/modules/programs/getmail.nix new file mode 100644 index 00000000000..12806c25679 --- /dev/null +++ b/home-manager/tests/modules/programs/getmail.nix @@ -0,0 +1,26 @@ +{ 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; + }; + }; + + 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-expected.conf b/home-manager/tests/modules/programs/git-expected.conf new file mode 100644 index 00000000000..d02ebf31649 --- /dev/null +++ b/home-manager/tests/modules/programs/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-with-email-expected.conf b/home-manager/tests/modules/programs/git-with-email-expected.conf new file mode 100644 index 00000000000..01c1eec5823 --- /dev/null +++ b/home-manager/tests/modules/programs/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-with-email.nix b/home-manager/tests/modules/programs/git-with-email.nix new file mode 100644 index 00000000000..f8a762dcceb --- /dev/null +++ b/home-manager/tests/modules/programs/git-with-email.nix @@ -0,0 +1,33 @@ +{ config, lib, pkgs, ... }: + +with lib; + +{ + imports = [ ../accounts/email-test-accounts.nix ]; + + config = { + programs.git = { + enable = true; + 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-with-str-extra-config-expected.conf b/home-manager/tests/modules/programs/git-with-str-extra-config-expected.conf new file mode 100644 index 00000000000..957438de13a --- /dev/null +++ b/home-manager/tests/modules/programs/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-with-str-extra-config.nix b/home-manager/tests/modules/programs/git-with-str-extra-config.nix new file mode 100644 index 00000000000..734c5ee764c --- /dev/null +++ b/home-manager/tests/modules/programs/git-with-str-extra-config.nix @@ -0,0 +1,22 @@ +{ config, lib, ... }: + +with lib; + +{ + config = { + programs.git = { + enable = true; + 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.nix b/home-manager/tests/modules/programs/git.nix new file mode 100644 index 00000000000..c5203e417dc --- /dev/null +++ b/home-manager/tests/modules/programs/git.nix @@ -0,0 +1,74 @@ +{ 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; + 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..5cb24817cb0 --- /dev/null +++ b/home-manager/tests/modules/programs/gpg/default.nix @@ -0,0 +1,3 @@ +{ + 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-expected.conf b/home-manager/tests/modules/programs/mbsync-expected.conf new file mode 100644 index 00000000000..f1ca79fe738 --- /dev/null +++ b/home-manager/tests/modules/programs/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.nix b/home-manager/tests/modules/programs/mbsync.nix new file mode 100644 index 00000000000..fa9768a2fe1 --- /dev/null +++ b/home-manager/tests/modules/programs/mbsync.nix @@ -0,0 +1,35 @@ +{ 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/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..2558a25832f --- /dev/null +++ b/home-manager/tests/modules/programs/rofi/assert-on-both-theme-and-colors.nix @@ -0,0 +1,33 @@ +{ 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..b42b43e949a --- /dev/null +++ b/home-manager/tests/modules/programs/rofi/default.nix @@ -0,0 +1,3 @@ +{ + rofi-assert-on-both-theme-and-colors = import ./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..266bc9d1f5d --- /dev/null +++ b/home-manager/tests/modules/programs/ssh/default-config.nix @@ -0,0 +1,23 @@ +{ 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..2e9082de378 --- /dev/null +++ b/home-manager/tests/modules/programs/ssh/forwards-dynamic-bind-path-with-port-asserts.nix @@ -0,0 +1,32 @@ +{ 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..15ab59e82ca --- /dev/null +++ b/home-manager/tests/modules/programs/ssh/forwards-dynamic-valid-bind-no-asserts.nix @@ -0,0 +1,45 @@ +{ 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..c05cba82791 --- /dev/null +++ b/home-manager/tests/modules/programs/ssh/forwards-local-bind-path-with-port-asserts.nix @@ -0,0 +1,36 @@ +{ 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..8cecc5e5121 --- /dev/null +++ b/home-manager/tests/modules/programs/ssh/forwards-local-host-path-with-port-asserts.nix @@ -0,0 +1,36 @@ +{ 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..a0473147bd3 --- /dev/null +++ b/home-manager/tests/modules/programs/ssh/forwards-remote-bind-path-with-port-asserts.nix @@ -0,0 +1,36 @@ +{ 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..770b8ab2870 --- /dev/null +++ b/home-manager/tests/modules/programs/ssh/forwards-remote-host-path-with-port-asserts.nix @@ -0,0 +1,36 @@ +{ 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..94263ef9d27 --- /dev/null +++ b/home-manager/tests/modules/programs/ssh/match-blocks-attrs.nix @@ -0,0 +1,64 @@ +{ 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-minimal.nix b/home-manager/tests/modules/programs/texlive-minimal.nix new file mode 100644 index 00000000000..df143dbc660 --- /dev/null +++ b/home-manager/tests/modules/programs/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..82c53438b00 --- /dev/null +++ b/home-manager/tests/modules/programs/tmux/disable-confirmation-prompt.nix @@ -0,0 +1,28 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + substituteExpected = path: pkgs.substituteAll { + src = path; + + sensible_rtp = pkgs.tmuxPlugins.sensible.rtp; + }; + +in + +{ + config = { + programs.tmux = { + enable = true; + disableConfirmationPrompt = true; + }; + + nmt.script = '' + assertFileExists home-files/.tmux.conf + assertFileContent home-files/.tmux.conf \ + ${substituteExpected ./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..b047c97a766 --- /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@/share/tmux-plugins/logging/logging.tmux + + +# tmuxplugin-prefix-highlight +# --------------------- + +run-shell @tmuxplugin_prefix_highlight@/share/tmux-plugins/prefix-highlight/prefix_highlight.tmux + + +# tmuxplugin-fzf-tmux-url +# --------------------- + +run-shell @tmuxplugin_fzf_tmux_url@/share/tmux-plugins/fzf-tmux-url/fzf-url.tmux + +# ============================================= # 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..5e147b7290e --- /dev/null +++ b/home-manager/tests/modules/programs/tmux/emacs-with-plugins.nix @@ -0,0 +1,41 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + substituteExpected = path: pkgs.substituteAll { + src = path; + + tmuxplugin_fzf_tmux_url = pkgs.tmuxPlugins.fzf-tmux-url; + tmuxplugin_logging = pkgs.tmuxPlugins.logging; + tmuxplugin_prefix_highlight = pkgs.tmuxPlugins.prefix-highlight; + tmuxplugin_sensible_rtp = pkgs.tmuxPlugins.sensible.rtp; + }; + +in + +{ + 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 + ]; + }; + + nmt.script = '' + assertFileExists home-files/.tmux.conf + assertFileContent home-files/.tmux.conf \ + ${substituteExpected ./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..e88ed587c03 --- /dev/null +++ b/home-manager/tests/modules/programs/tmux/vi-all-true.nix @@ -0,0 +1,30 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + substituteExpected = path: pkgs.substituteAll { + src = path; + + sensible_rtp = pkgs.tmuxPlugins.sensible.rtp; + }; + +in { + config = { + programs.tmux = { + aggressiveResize = true; + clock24 = true; + enable = true; + keyMode = "vi"; + newSession = true; + reverseSplit = true; + }; + + nmt.script = '' + assertFileExists home-files/.tmux.conf + assertFileContent home-files/.tmux.conf \ + ${substituteExpected ./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..da5dd5b55ed --- /dev/null +++ b/home-manager/tests/modules/programs/zsh/default.nix @@ -0,0 +1,3 @@ +{ + zsh-session-variables = ./session-variables.nix; +} 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..a87d39820cf --- /dev/null +++ b/home-manager/tests/modules/programs/zsh/session-variables.nix @@ -0,0 +1,22 @@ +{ config, lib, ... }: + +with lib; + +{ + config = { + programs.zsh = { + enable = true; + + sessionVariables = { + V1 = "v1"; + V2 = "v2-${config.programs.zsh.sessionVariables.V1}"; + }; + }; + + 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..ac04a7ecf30 --- /dev/null +++ b/home-manager/tests/modules/services/sxhkd/configuration.nix @@ -0,0 +1,31 @@ +{ 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-keybindings.nix b/home-manager/tests/modules/services/window-managers/i3-keybindings.nix new file mode 100644 index 00000000000..b5ee4fd8765 --- /dev/null +++ b/home-manager/tests/modules/services/window-managers/i3-keybindings.nix @@ -0,0 +1,34 @@ +{ 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"; + }; + }; + + nmt.script = '' + assertFileExists home-files/.config/i3/config + + assertFileRegex home-files/.config/i3/config \ + 'bindsym Mod1+Left overridden-command' + + assertFileNotRegex home-files/.config/i3/config \ + 'Mod1+Right' + + assertFileRegex home-files/.config/i3/config \ + 'bindsym Mod1+Invented invented-key-command' + ''; + }; +} 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..0fa0070cd77 --- /dev/null +++ b/home-manager/tests/modules/systemd/timers.nix @@ -0,0 +1,31 @@ +{ 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-expected.conf b/home-manager/tests/modules/xresources-expected.conf new file mode 100644 index 00000000000..20b47e5080b --- /dev/null +++ b/home-manager/tests/modules/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.nix b/home-manager/tests/modules/xresources.nix new file mode 100644 index 00000000000..f73e326f31e --- /dev/null +++ b/home-manager/tests/modules/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} + ''; + }; +} |