Write scripts in home-manager

I am trying to write a helper script in my home-manager file to control the volume for polybar. What I am trying to do is something like:

home.files.".local/bin/volume.sh" = {
  executable = true;
  text = ''
    #!/bin/bash
    wpctl ... | awk
  '';
};

This does not work from polybar, but does from running on the terminal.

Making the adjustments works a bit:

... text = ''
    #!${pkgs.bash}/bin/bash

    wpctl ... | ${pkgs.gawk}/bin/awk ...
  '';

But I still get wpctl not found

My system says wpctl is located : /nix/store/8jks1bd8g5a7fkc2r519m6nmlxar6jx3-system-path/bin/wpctl but I don’t know what to use for ${pkgs.*} to get this path?

But I feel like I am doing this entirely wrong since my terminal running bash has no issue running the script.

What is the correct way to write these scripts like this?

I also ran this in the script without success:

#!/usr/bin/env bash

And this doesn’t work in polybar, but does in my terminal, though it is noticably slower

#!/usr/bin/env nix-shell
#! nix-shell -i bash -p bash

You would probably be better off using home.packages with pkgs.writeShellApplication.

{ pkgs, ... }:
{
  home.packages = [
    (pkgs.writeShellApplication {
      name = "volume.sh"; # this will be the name of the binary
      runtimeInputs = [ pkgs.wireplumber pkgs.gawk ]; # Dependencies go here
      # Note that no shebang is necessary, writeShellApplication will prepend
      # it.
      # 
      # Also note that there is no need to reference the package, the
      # runtimeInputs will take care of adding `bin` directory to this script's
      # path
      text = ''
        wpctl ... | awk ...
      '';
    })
  ];
}
4 Likes

Ok, I did that and it still works in the terminal, however polybar is still not happy about the command. The output on the bar is now /bin/sh: line 1: volume.sh: command not found My polybar is basically like so:

{ theme, fonts, ...}:

{
  services.polybar = {
    enable = true;
    config = {
      ...
      "module/volume" = {
        type = "custom/ipc";
        hook-0 = "volume.sh";
        initial = 1;
      };
    };

Which is imported into my home.nix as so:

{ config, pkgs, ... }:

let
  theme = import ./themes/theme.nix;
  fonts = {
    mono = "FiraCode Nerd Font Mono";
  };
in {
  imports = [
    (import ./services/polybar.nix { theme = theme; fonts = fonts; })
  ];
}

If you only want to use this volume control script in your polybar, you can use something like:

...
"module/volume" = {
        type = "custom/ipc";
        hook-0 = lib.getExe (pkgs.writeShellApplication {
      name = "volume.sh"; # this will be the name of the binary
      runtimeInputs = [ pkgs.wireplumber pkgs.gawk ]; # Dependencies go here
      # Note that no shebang is necessary, writeShellApplication will prepend
      # it.
      # 
      # Also note that there is no need to reference the package, the
      # runtimeInputs will take care of adding `bin` directory to this script's
      # path
      text = ''
        wpctl ... | awk ...
      '';
    }); 
        initial = 1;
      };

I actually want it to a separate script I can call, because there are other uses for calling the script separately. But, I do want it to be used in polybar primarily

You could combine the two in one file if you want and do something like this:

{ pkgs, ... }:
let
  script = pkgs.writeShellApplication {
      name = "volume.sh"; # this will be the name of the binary
      runtimeInputs = [ pkgs.wireplumber pkgs.gawk ]; # Dependencies go here
      # Note that no shebang is necessary, writeShellApplication will prepend
      # it.
      # 
      # Also note that there is no need to reference the package, the
      # runtimeInputs will take care of adding `bin` directory to this script's
      # path
      text = ''
        wpctl ... | awk ...
      '';
    }
in
{
  home.packages = [ script ];
  services.polybar = {
    enable = true;
    config = {
      ...
      "module/volume" = {
        type = "custom/ipc";
        hook-0 = lib.getExe script;
        initial = 1;
      };
    };
  };
}
1 Like

So this is what I am doing now, which works. But I would like this to be more maintainable in the long run.

So I have

{ config, pkgs, ... }:

let scripts = {
  volume = pkgs.writeShellApplication { ... };
};
in {...}

But I would like to import the scripts from its own dedicated file, something like

{ config, pkgs, ... }:

let
  scripts = {
    volume = import ./scripts/volume.nix;
  };
in { ... }

If you want to define your script as a separate Nix file, you should probably structure it like a package and use callPackage to import it.

volume.nix:

{
  writeShellApplication,
  wireplumber,
  gawk,
  # any other dependencies from pkgs go here
  # all this will be injected by callPackage
}:
writeShellApplication {
  name = "volume.sh";
  runtimeInputs = [ wireplumber gawk ];
  # ...
}

home.nix:

{ config, pkgs, ... }:

let scripts = {
  volume = pkgs.callPackage scripts/volume.nix { };
};
in {...}
2 Likes

callPackage is also explained in more detail here: Package parameters and overrides with callPackage — nix.dev documentation

1 Like