Extending Hjem

The core strength of Hjem lies in its simplicity. No more intricate APIs for you to parse through just to understand some basic feature. This should, however, not be seen as a weakness of Hjem.

Projects such as Hjem Rum should be seen as proof just how much you may build upon Hjem, and just how robust it is.

Writing your own modules

You may freely consume the APIs exposed by Hjem to write your own modules, primarily to fill the gap between a comprehensive (and clunky) module system like Home Manager and something as quick and streamlined as Hjem.

To do so, you must write your own Hjem modules. Those modules will be evaluated by Hjem to make more options, such as programs.foo or services.bar as you might be used to from other module systems, once they are imported.

The NixOS Manual covers very comprehensively how to write your own modules. For the sake of brevity, we will only cover the basics required to write a valid Hjem module, and how you may integrate it with Hjem as a separate module system.

The anatomy of a Hjem module is simple. You want to define options, and you want to set config based on those options. Here is an example from Hjem Rum's FZF module:

{
  lib,
  pkgs,
  config,
  ...
}: let
  inherit (lib.meta) getExe;
  inherit (lib.modules) mkAfter mkIf;
  inherit (lib.options) mkEnableOption mkPackageOption;

  cfg = config.rum.programs.fzf;
in {
  options.rum.programs.fzf = {
    enable = mkEnableOption "fzf";

    package = mkPackageOption pkgs "fzf" {nullable = true;};

    integrations = {
      fish.enable = mkEnableOption "fzf integration with fish";
      zsh.enable = mkEnableOption "fzf integration with zsh";
    };
  };

  config = mkIf cfg.enable {
    packages = mkIf (cfg.package != null) [cfg.package];

    rum.programs.fish.config = mkIf cfg.integrations.fish.enable (
      mkAfter "${getExe cfg.package} --fish | source"
    );
    rum.programs.zsh.initConfig = mkIf cfg.integrations.zsh.enable (
      mkAfter "source <(${getExe cfg.package} --zsh)"
    );
  };
}

options.<namespace> is the critical component here, as it will make options such as rum.programs.fzf available for user configurations. The other component, config, sets values in Hjem's own packages field and Hjem Rum's rum.programs.fish and rum.programs.zsh fields which, in turn, set files such as .zshrc and .zshenv.

Let's assume you have defined a similar module in mymodules/fzf.nix in your configuration. You may consume it in your NixOS configuration by first importing Hjem as a NixOS module, and adding it to Hjem's hjem.extraModules in order to be evaluated.

{
  # flake.nix
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
    hjem.url = "github:feel-co/hjem";
  };

  # One example of importing the module into your system configuration
  outputs = {nixpkgs, ...} @ inputs: {
    nixosConfigurations = {
      default = nixpkgs.lib.nixosSystem {
        specialArgs = {inherit inputs;};
        modules = [
          # 1. First import the Hjem module
          inputs.hjem.nixosModules.default

          # 2. Now set `hjem.extraModules`
          ({pkgs, ...}: {
            hjem.extraModules = [
              # 3. We previously made a module in ./modules/fzf.nix. Let's import it
              # so that our new options are available.
              ./modules/fzf.nix # <- has to be a valid module!
            ];

            # 4. Now let's set some of our options, we previously made them available
            # under 'rum.programs.fzf', so let's go with that. The name is arbitrary, and
            # you can set it anything you want; for example 'mymodule.programs.fzf-yay' is
            # perfectly valid too!
            rum.programs.fzf = {
              enable = true;
              package = pkgs.fzf; # or something like pkgs.fzf.override { ... }
            };
          })
        ];
      };
    };
  };
}

Once you create and import your module, you are done. It all boils down to defining a valid module, and consuming it in hjem.extraModules. You may also choose to export your defined modules as hjemModules in your flake.nix if you want to allow others to use them too!

{
  inputs = { /* ... */ };
  outputs = {self, ...}: {
    hjemModules = {
      my-fzf-module = ./modules/fzf.nix; # The name is once again arbitrary.
      default = self.hjemModules.my-fzf-module; # You can set a default.
    };
  };
}