Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

optnix

optnix is a fast, terminal-based options searcher for Nix module systems.

a demo of optnix

There are multiple module systems that Nix users use on a daily basis:

These systems can have difficult-to-navigate documentation, especially for options in external modules.

optnix solves that problem, and lets users inspect option values if possible; just like nix repl in most cases, but prettier.

Installation

Use the provided flake input:

{
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";

    optnix.url = "github:water-sucks/optnix";
  };

  outputs = inputs: {
    nixosConfigurations.jdoe = inputs.nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      modules = [
        ({pkgs, ...}: {
          environment.systemPackages = [
            inputs.optnix.packages.${pkgs.system}.optnix
          ];
        })
      ];
    };
  };
}

Or import it inside a Nix expression through fetchTarball:

{pkgs, ...}: let
  optnix-url = "https://github.com/water-sucks/optnix/archive/GITREVORBRANCHDEADBEEFDEADBEEF0000.tar.gz";
  optnix = (import "${builtins.fetchTarball optnix}").packages.${pkgs.system}.optnix;
in {
  environment.systemPackages = [
    optnix
    # ...
  ];
}

When will this be in nixpkgs/home-manager/nix-darwin/etc.?

I'm working on it.

Ideally I'll want to get the whole project into nix-community too, once it gains some popularity.

Cache

There is a Cachix cache available. Add the following to your Nix configuration to avoid lengthy rebuilds and fetching extra build-time dependencies:

{
  nix.settings = {
    substituters = [ "https://watersucks.cachix.org" ];
    trusted-public-keys = [
      "watersucks.cachix.org-1:6gadPC5R8iLWQ3EUtfu3GFrVY7X6I4Fwz/ihW25Jbv8="
    ];
  };
}

Or if using the Cachix CLI outside a NixOS environment:

$ cachix use watersucks

Extras

There are also some packaged Nix modules for easy usage that all function in the same manner, available at the following paths:

  • nixosModules.optnix :: for NixOS systems
  • darwinModules.optnix :: for nix-darwin systems
  • homeModules.optnix :: for home-manager systems

Alongside these modules is a function mkLib; this function is the entry point for the optnix Nix library for generating lists.

More information on these resources can be found on the module page, as well as the API Reference for what functions are available in the optnix library.

Additionally, some examples configuring optnix for different module systems are available on the recipes page.

Usage

The next few sections describe the different optnix concepts.

But first:

What's a module system, even?

A module system is a Nix library that allows you to configure a set of exposed options. All the systems mentioned above allow you to configure their respective options with your own values.

While this can be a powerful paradigm for modeling any configuration system, these options can be rather hard to discover. Some of these options are found through web interfaces (like https://search.nixos.org), but many options can remain out of sight without reading source code, such as external module options or external module systems.

More information on how module systems work can be found on nix.dev.

How optnix Works

optnix works by ingesting option lists that are generated from these module systems.

An option list is a list of JSON objects; each JSON object describes a single option, with the following values for an example option:

{
  "name": "services.nginx.enable",
  "description": "Whether to enable Nginx Web Server.",
  "type": "boolean",
  "default": {
    "_type": "literalExpression",
    "text": "false"
  },
  "example": {
    "_type": "literalExpression",
    "text": "true"
  },
  "loc": ["services", "nginx", "enable"],
  "readOnly": false,
  "declarations": [
    "/nix/store/path/nixos/modules/services/web-servers/nginx/default.nix"
  ]
}

Given an options attribute set, a list of these options can be generated using lib.optionAttrSetToDocList from nixpkgs, or by using wrapper functions provided with optnix as a Nix library. This will be seen in later examples.

Operation

There are two modes of operation: interactive (the default) and non-interactive.

Interactive mode will display a search UI that allows looking for options using fuzzy search keywords. Selected options in the list can also be evaluated in order to preview their values.

Non-interactive mode requires a valid option name as input, and will display the option and its values (if applicable) without any user interaction. This kind of output is useful when an option name is known, such as for scripting.

optnix is controlled through its configuration file (or files) that define "scopes". For more, look at the following pages:

Scopes

A "scope", in the optnix context, is a combination of a module system option list, as well as a module system instantiation.

For example, a "scope" for a NixOS configuration located at the flake ref github:water-sucks/nixed#nixosConfigurations.CharlesWoodson would be:

  • The module system's options set (nixosConfigurations.CharlesWoodson.options)
  • The module system's config set (nixosConfigurations.CharlesWoodson.config)

options would be used to generate the option list, while config would be used to evaluate and preview values of those options.

Components

In optnix, scopes are defined from the configuration file, and have a few components.

An example of the fields for a scope in optnix can be found on the configuration page, and real configuration examples can be found on the recipes page.

scopes.<name>.description

A small description of what the purpose of this scope is. Optional, but useful for command-line completion and listing scopes more descriptively.

scopes.<name>.options-list-{file,cmd}

The option list can be specified in two different ways:

  • A path to a JSON file containing the option list (options-list-file)
  • A command that prints the option list to stdout (options-list-cmd)

Specifying at least one of these two is mandatory for every scope.

options-list-file is preferred over options-list-cmd, but both can be specified; if the file does not exist or cannot be accessed/is incorrect, then the command is used as a fallback.

The command will usually end up being some invocation of Nix, but this command is evaluated using a shell (/bin/sh), which means it supports POSIX shell constructs/available commands as long as they are in $PATH.

Prefer using options-list-file when creating configurations, since this is almost always faster than running the equivalent options-list-cmd, since options-list-cmd is not cached.

Generating options list files can be done using the optnix Nix library, and examples can be seen on the recipes page.

scopes.<name>.evaluator

An evaluator is a command template that can be used to evaluate a Nix configuration to retrieve values.

It is a shell command (always some invocation of a Nix command), but with a twist: exactly one placeholder of {{ .Option }} for the option to evaluate is required. This will be filled in with the option to evaluate.

An example evaluator for a Nix flake would be:

nix eval "/path/to/flake#nixosConfigurations.nixos.config.{{ .Option }}"

Specifying an evaluator for a scope is optional.

Configuration

Configurations are defined in TOML format, and are merged together in order of priority.

There are four possible locations for configurations (in order of highest to lowest priority, and only if they exist):

  • Configuration paths specified on the command line with --config
  • optnix.toml in the current directory
  • $XDG_CONFIG_HOME/optnix/config.toml or $HOME/.config/optnix/config.toml
  • /etc/optnix/config.toml

Schema

A more in-depth explanation for scope configuration values is on the scopes page.

# Minimum score required for search matches; the higher the score,
# the more fuzzy matches are required before it is displayed in the list.
# Higher scores will generally lead to less (but more relevant) results, but
# this has diminishing returns.
min_score = 1
# Debounce time for search, in ms
debounce_time = 25
# Default scope to use if not specified on the command line
default_scope = ""
# Formatter command to use for evaluated values, if available. Takes input on
# stdin and outputs the formatted code back to stdout.
formatter_cmd = "nixfmt"

# <name> is a placeholder for the name of the scope.
# This is not a working scope! See the recipes page.
# for real examples.
[scopes.<name>]
# Description of this scope
description = "NixOS configuration for `nixos` system"
# A path to the options list file. Preferred over options-list-cmd.
options-list-file = "/path/to/file"
# A command to run to generate the options list file. The list must be
# printed on stdout.
# Check the recipes page for some example commands that can generate this.
# This is not always needed for every scope.
# The following is only an example.
options-list-cmd = """
nix eval /path/to/flake#homeConfigurations.nixos --json --apply 'input: let
  inherit (input) options pkgs;

  optionsList = builtins.filter
    (v: v.visible && !v.internal)
    (pkgs.lib.optionAttrSetToDocList options);
in
  optionsList'
"""
# Go template for what to run in order to evaluate the option. Optional, but
# useful for previewing values.
# Check the scopes page for an explanation of this value.
evaluator = "nix eval /path/to/flake#nixosConfigurations.nixos.config.{{ .Option }}"

Module

There are some packaged Nix modules for easy usage that all function in the same manner, available at the following paths (for both flake and legacy-style configs):

  • nixosModules.optnix :: for NixOS systems
  • darwinModules.optnix :: for nix-darwin systems
  • homeModules.optnix :: for home-manager systems

They all contain the same options.

Library

When using the Nix modules, it is extremely useful to instantiate the Nix library provided with optnix.

This can be done using the exported optnix.mkLib function:

{pkgs, ...}:
let
  # Assume `optnix` is imported already.
  optnixLib = optnix.mkLib pkgs;
in {
  programs.optnix = {
    # whatever options
  };
}

The functions creates option lists from Nix code ahead of time.

See the API Reference for what functions are available, as well as the recipes page for some real-life examples on how to use the module and the corresponding functions from an instantiated optnix library.

Options

programs.optnix.enable

CLI searcher for Nix module system options

Type: boolean

Default: false

Example: true

programs.optnix.package

Package that provides optnix

Type: lib.types.package

Default: self.packages.${pkgs.system}.optnix

programs.optnix.settings

Settings to put into optnix.toml

Type: lib.types.attrs

Default: {}


Generated with nix-options-doc

Recipes

There are four popular sets of Nix module systems:

Even despite their popularity, it can be a little hard to get things up and running without knowing how those module systems work first.

The following sections are a set of common scope configurations that you can use in your configurations, specified in both Nix form using optnix.mkLib and raw TOML, whenever necessary.

Make sure to look at the API Reference to check what arguments can be passed to optnix library functions.

⚠️ CAUTION: Do not assume that these will automatically work with your setup. Tweak as needed.

Feel free to contribute more examples, or request more for different module systems, as needed.

NixOS Recipes

optnix recipes for the NixOS module system.

optnix Module Examples

A simple nix-darwin module showcasing the programs.optnix option and using optnixLib.mkOptionList for option list generation:

{ options, pkgs, ... }: let
  # Assume `optnix` is correctly instantiated.
  optnixLib = inputs.optnix.mkLib pkgs;
in {
  programs.optnix = {
    enable = true;
    settings = {
      min_score = 3;

      scopes = {
        CharlesWoodson = {
          description = "NixOS configuration for CharlesWoodson";
          options-list-file = optnixLib.mkOptionsList { inherit options; };
          # For flake systems
          # evaluator = "nix eval /path/to/flake#nixosConfigurations.CharlesWoodson.config.{{ .Option }}";
          # For legacy systems
          # evaluator = "nix-instantiate --eval '<nixpkgs/nixos>' -A config.{{ .Option }}";
        };
      };
    };
  };
}

Raw TOML Examples

Flakes

Inside a flake directory /path/to/flake with a NixOS system named CharlesWoodson:

[scopes.nixos]
description = "NixOS flake configuration for CharlesWoodson"
options-list-cmd = '''
nix eval "/path/to/flake#nixosConfigurations.CharlesWoodson" --json --apply 'input: let
  inherit (input) options pkgs;

  optionsList = builtins.filter
    (v: v.visible && !v.internal)
    (pkgs.lib.optionAttrSetToDocList options);
in
  optionsList'
'''
evaluator = "nix eval /path/to/flake#nixosConfigurations.CharlesWoodson.config.{{ .Option }}"

Legacy

This uses the local NixOS system attributes located in <nixos/nixpkgs>.

[scopes.nixos-legacy]
description = "NixOS flake configuration on local host"
options-list-cmd = '''
nix-instantiate --eval --expr --strict --json 'let
  system = import <nixpkgs/nixos> {};
  pkgs = system.pkgs;
  optionsList = pkgs.lib.optionAttrSetToDocList system.options;
in
  optionsList'
'''
evaluator = "nix-instantiate --eval '<nixpkgs/nixos>' -A config.{{ .Option }}"

nix-darwin Recipes

optnix recipes for the nix-darwin module system.

optnix Module Examples

A simple nix-darwin module showcasing the programs.optnix option and using optnixLib.mkOptionList for option list generation:

{ options, pkgs, ...
}: let
  # Assume `optnix` is correctly instantiated.
  optnixLib = inputs.optnix.mkLib pkgs;
in {
  programs.optnix = {
    enable = true;
    settings = {
      min_score = 3;

      scopes = {
        TimBrown = {
          description = "nix-darwin configuration for TimBrown";
          options-list-file = optnixLib.mkOptionsList { inherit options; };
          # For flake systems
          # evaluator = "nix eval /path/to/flake#darwinConfigurations.TimBrown.config.{{ .Option }}";
          # For legacy systems
          # evaluator = "nix-instantiate --eval '<darwin-config>' -A {{ .Option }}";
        };
      };
    };
  };
}

Raw TOML Examples

Flakes

Inside a flake directory /path/to/flake with a nix-darwin system named TimBrown:

[scopes.nix-darwin]
description = "nix-darwin flake configuration for TimBrown"
options-list-cmd = '''
nix eval "/path/to/flake#darwinConfigurations.TimBrown" --json --apply 'input: let
  inherit (input) options pkgs;

  optionsList = builtins.filter
    (v: v.visible && !v.internal)
    (pkgs.lib.optionAttrSetToDocList options);
in
  optionsList'
'''
evaluator = "nix eval /path/to/flake#darwinConfigurations.TimBrown.config.{{ .Option }}"

Legacy

TODO: add an example of legacy, non-flake nix-darwin configuration with raw TOML.

home-manager Recipes

optnix recipes for home-manager (HM), a popular module system that manages user configurations.

HM has some quirks that can make it rather weird to use in optnix. Absolutely prefer using the optnix modules + the optnix Nix library to generate HM options. If using optnix with HM with raw TOML, you are on your own; I have not been able to create good examples at the time of writing.

Standalone/Inside home-manager Module

Inside of home-manager, the options attribute is available and can be used directly.

{ options, config, pkgs, lib, ... }: let
  # Assume `optnix` is correctly instantiated.
  optnixLib = optnix.mkLib pkgs;
in {
  programs.optnix = {
    enable = true;
    scopes = {
      home-manager = {
        description = "home-manager configuration for all systems";
        options-list-file = optnixLib.mkOptionsList {
          inherit options;
          transform = o:
            o
            // {
              name = lib.removePrefix "home-manager.users.${config.home.username}." o.name;
            };
        };
        evaluator = "";
      };
    }
  };
}

NOTE: This may create a separate configuration file for ALL users depending on, so it may not necessarily be what you want on multi-user systems. Look at the next section for more on an alternative.

NixOS/nix-darwin Module

home-manager does not expose a proper options attribute set on NixOS and nix-darwin systems, which makes option introspection a little harder than it should be.

Instead, an unexposed function from the type of the home-manager.users option itself, getSubOptions, can be used to obtain an options attribute set for HM.

However, this is impossible to evaluate due to the fact that it relies on other settings that may not exist, such as usernames and such.

When this is the case, the following scope declaration using the special function optnixLib.mkOptionsListFromHMSource can be used in any module: NixOS, nix-darwin, or even in standalone HM.

This function is adapted from home-manager's documentation facilities, and inserts some dummy modules that allow for proper option list generation without evaluation failing.

{
  inputs,
  config,
  pkgs,
  ...
}: let
  optnixLib = inputs.optnix.mkLib pkgs;
in {
  programs.optnix = {
    enable = true;
    settings = {
      scopes = {
        home-manager = {
          description = "home-manager options for all systems";
          options-list-file = optnixLib.hm.mkOptionsListFromHMSource {
            home-manager = inputs.home;
            modules = with inputs; [
              # Other extra modules that may exist in your source tree
              # optnix.homeModules.optnix
            ];
          };
        };
      };
    };
  };
}

flake-parts Recipes

flake-parts is a framework/module system for writing flakes using Nix module system semantics.

Singe flake-parts configurations can wildly vary in module selection, most users will want to define them on a per-flake basis. This is well-supported through the usage of a local optnix.toml file, relative to the flake.

Exposing Documentation Through Flake

Use the following flake module code to expose a flake attribute called debug.options-doc:

{
  lib,
  options,
  ...
}: {
  # Required for evaluating module option values.
  debug = true;
  flake = {
    debug.options-doc = builtins.unsafeDiscardStringContext
      (builtins.toJSON (lib.optionAttrSetToDocList options));
  };
}

OR, if you do not want to copy-paste code, use the optnix.flakeModules.flake-parts-doc in an import like so:

{
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";

    flake-parts.url = "github:hercules-ci/flake-parts";

    optnix.url = "github:water-sucks/optnix";
  };

  outputs = { optnix, ... }@inputs:
    flake-parts.lib.mkFlake {inherit inputs;} {
      imports = [
        optnix.flakeModules.flake-parts-doc
      ];

      # ...
    };
}

Then, configure an optnix.toml (in the same directory as the top-level flake.nix) for this is rather trivial:

[scopes.flake-parts]
description = "flake-parts config for NixOS configuration"
options-list-cmd = "nix eval --json .#debug.options-doc"
evaluator = "nix eval .#debug.config.{{ .Option }}"

Despite the usage of options-list-cmd, flake-parts evaluates decently fast at most times.

If using options-list-file is a non-negotiable, exposing a package with pkgs.writeText and using the above code as a base is also possible. But you're on your own. If you really want an example, congrats on reading this! I love that you're taking the time to read through this application's documentation and trying to use it, so please file an issue, I was just too lazy to do this right now at time of writing.

API Reference

mkLib

mkLib :: pkgs -> set

Creates an optnixLib instance using an instantiated pkgs set.

optnixLib

optnixLib represents an instantiated value crated by mkLib above.

optnixLib.mkOptionsList

mkOptionsList :: AttrSet -> Derivation

Generates an options.json derivation that contains a JSON options list from a provided options attribute set that can be linked into various Nix module systems.

A base (but common pattern) of usage:

{options, pkgs, ...}: let
  # Adapt this to wherever `optnix` came from
  optnixLib = optnix.mkLib pkgs;
  optionsList = mkOptionsList {
    inherit options;
    excluded = []; # Add problematic eval'd paths here
  };
in {
  # use here...
}

Arguments are passed as a single attribute set in a named manner.

Required Attributes

  • options :: AttrSet :: The options attrset to generate an option list for

Optional Attributes

  • transform :: AttrSet -> AttrSet :: A function to apply to each generated option in the list, useful for stripping prefixes, where AttrSet is a single option in the list
  • excluded :: [String] :: A list of dot-paths in the options attribute set to exclude from the generated options list using optnixLib.removeNestedAttrs

optnixLib.mkOptionsListFromModules

mkOptionsListFromModules :: AttrSet -> Derivation

Generates an options.json derivation that contains a JSON options list from a provided list of modules that can be linked into various Nix module systems.

This can be useful when generating documentation for external modules that are not necessarily part of the configuration, such as generating option lists for usage outside of optnix, or for generating a system-agnostic documentation list.

Arguments are passed as a single attribute set in a named manner.

Arguments

  • modules :: List[Module] :: A list of modules to generate an option list for

optnixLib.combineLists

combineLists :: [Derivation] -> Derivation

Combines together multiple JSON file derivations containing option lists (such as those created by mkOptionsList) into a single JSON file.

combineLists [optionsList1 optionsList2]

Internally uses jq --slurp add to merge JSON arrays.

Arguments

  • [Derivation] :: A list of JSON file derivations, each containing an option list, preferably created using mkOptionsList

optnixLib.removeAtPath

removeAtPath :: String -> AttrSet -> AttrSet

Recursively remove a nested attribute from an attrset, following a string array containing the path to remove.

This can be useful for removing problematic options (i.e. ones that fail evaluation) in a custom manner, and is also used internally by the excluded parameter of mkOptionsList.

removeAtPath "services.nginx" options

This example will return the same attrset config, but with the services.nginx subtree removed. If a path does not exist or is not an attrset, it is left untouched.

Arguments

  • String :: A dot-path string representing the attribute path to remove from the attrset
  • AttrSet :: The attrset to remove attributes recursively from

optnixLib.removeNestedAttrs

removeNestedAttrs :: [String] -> AttrSet -> AttrSet

Recursively remove multiple attributes using optnixLib.removeAtPath.

removeNestedAttrs ["programs.chromium" "services.emacs"] options

This example will return the same attrset config, but with the services.emacs and programs.chromium subtrees removed. If a path does not exist or is not an attrset, it is left untouched.

Arguments

  • [String] :: A list of dot-path strings representing the attribute paths to remove from the attrset
  • AttrSet :: The attrset to remove attributes recursively from

optnixLib.hm

Functions for optnixLib specifically related to home-manager.

optnixLib.hm.mkOptionsListFromHMSource

mkOptionsListFromHMSource :: AttrSet -> Derivation

Generate a JSON options list given a home-manager source tree, alongside additional home-manager modules if desired.

This implementation is adapted from Home Manager's internal documentation generation pipeline and simplified for optnix usage. Prefer using mkOptionsList with an explicit instantiated options attribute set if possible.

Required Attributes

  • home-manager :: Derivation :: The derivation containing home-manager, usually sourced using fetchTarball, a Nix channel, or a flake input

Optional Attributes

  • modules :: List[Module] :: A list of extra modules to additionally evaluate when generating the option list