Skip to content

Instantly share code, notes, and snippets.

@antifuchs
Last active July 3, 2025 08:52
Show Gist options
  • Save antifuchs/10138c4d838a63c0a05e725ccd7bccdd to your computer and use it in GitHub Desktop.
Save antifuchs/10138c4d838a63c0a05e725ccd7bccdd to your computer and use it in GitHub Desktop.
A nix module that arranges the macOS dock the way you want it. Note: It won't allow you to manually re-arrange the items on it; the dock gets reset everytime you log in.
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.local.dock;
stdenv = pkgs.stdenv;
in
{
options = {
local.dock.enable = mkOption {
description = "Enable dock";
default = stdenv.isDarwin;
example = false;
};
local.dock.entries = mkOption
{
description = "Entries on the Dock";
type = with types; listOf (submodule {
options = {
path = lib.mkOption { type = str; };
section = lib.mkOption {
type = str;
default = "apps";
};
options = lib.mkOption {
type = str;
default = "";
};
};
});
readOnly = true;
};
};
config =
mkIf (cfg.enable)
(
let
dockutil = (import ./dockutil.nix);
du = "env PYTHONIOENCODING=utf-8 ${dockutil}/bin/dockutil";
normalize = path: if hasSuffix ".app" path then path + "/" else path;
entryURI = path: "file://" + (builtins.replaceStrings
# TODO: This is entirely too naive and works only with the bundles that I have seen on my system so far:
[" " "!" "\"" "#" "$" "%" "&" "'" "(" ")"]
["%20" "%21" "%22" "%23" "%24" "%25" "%26" "%27" "%28" "%29"]
(normalize path)
);
wantURIs = concatMapStrings
(entry: "${entryURI entry.path}\n")
cfg.entries;
createEntries = concatMapStrings
(entry: "${du} --no-restart --add '${entry.path}' --section ${entry.section} ${entry.options}\n")
cfg.entries;
in
{
system.activationScripts.postUserActivation.text = ''
echo >&2 "Setting up persistent dock items..."
haveURIs="$(${du} --list | ${pkgs.coreutils}/bin/cut -f2)"
if ! diff -wu <(echo -n "$haveURIs") <(echo -n '${wantURIs}') >&2 ; then
echo >&2 "Resetting Dock."
${du} --no-restart --remove all
${createEntries}
killall Dock
else
echo >&2 "Dock is how we want it."
fi
'';
}
);
}
with (import <nixpkgs> { });
derivation {
name = "dockutil-2.0.5";
builder = "${bash}/bin/bash";
args = [
"-xeuc"
''
${unzip}/bin/unzip $src
${coreutils}/bin/mkdir -p $out/bin
${coreutils}/bin/mv dockutil-2.0.5/scripts/dockutil $out/bin/dockutil
''
];
src = fetchurl {
url = "https://github.com/kcrawford/dockutil/archive/2.0.5.zip";
sha256 = "0b18awdaimf3gc4dhxx6lpivvx4li7j8kci648ssz39fwmbknlam";
};
system = builtins.currentSystem;
}
{ config, pkgs, ... }:
{
local.dock.entries = [
{ path = "${pkgs.emacs}/Applications/Emacs.app/"; }
{ path = "/Applications/Mailplane.app"; }
{ path = "/Applications/IRCCloud.app/"; }
{ path = "/Applications/Google Chrome.app/"; }
{ path = "/Applications/iPulse.app/"; }
{ path = "/Applications/Dash.app/"; }
{ path = "/System/Applications/Messages.app/"; }
{ path = "/Applications/iTerm.app/"; }
{ path = "/System/Applications/Music.app/"; }
{ path = "/System/Applications/Home.app/"; }
# Folders:
{
path = "/Users/asf/Downloads/";
section = "others";
options = "--sort dateadded --view grid --display folder";
}
{
path = "/Users/asf/Mess/Mess/";
section = "others";
options = "--sort name --view grid --display folder";
}
];
}
@elbelga
Copy link

elbelga commented Oct 10, 2024

I have modified dock.nix to add spacers and folders

{ config, pkgs, lib, ... }:

# Original source: https://gist.github.com/antifuchs/10138c4d838a63c0a05e725ccd7bccdd

with lib;
let
  cfg = config.local.dock;
  inherit (pkgs) stdenv dockutil;
in
{
  options = {
    local.dock.enable = mkOption {
      description = "Enable dock";
      default = stdenv.isDarwin;
      example = false;
    };

    local.dock.entries = mkOption
      {
        description = "Entries on the Dock";
        type = with types; listOf (submodule {
          options = {
            type = lib.mkOption { 
              type = str;
              default = "app";
            };
            path = lib.mkOption { 
              type = str;
              default = "";
            };
            view = lib.mkOption {
              type = str;
              default = "auto";
            };
            display = lib.mkOption {
              type = str;
              default = "folder";
            };
            section = lib.mkOption {
              type = str;
              default = "apps";
            };
            options = lib.mkOption {
              type = str;
              default = "";
            };
          };
        });
        readOnly = true;
      };
  };

  config =
    mkIf cfg.enable
      (
        let
          normalize = path: if hasSuffix ".app" path then path + "/" else path;
          entryURI = path: "file://" + (builtins.replaceStrings
            [" "   "!"   "\""  "#"   "$"   "%"   "&"   "'"   "("   ")"]
            ["%20" "%21" "%22" "%23" "%24" "%25" "%26" "%27" "%28" "%29"]
            (normalize path)
          );
          wantURIs = concatMapStrings
            (entry: "${entryURI entry.path}\n")
            cfg.entries;
          createEntries = concatMapStrings
            (entry: 
              if hasSuffix "spacer" entry.type then 
                "${dockutil}/bin/dockutil --no-restart --add '' --type ${entry.type} --section ${entry.section}\n"
              else if entry.type == "folder" then
                "${dockutil}/bin/dockutil --no-restart --add '${entry.path}' --view ${entry.view} --display ${entry.display}\n"
              else
                "${dockutil}/bin/dockutil --no-restart --add '${entry.path}' --section ${entry.section} ${entry.options}\n"
            ) cfg.entries;
        in
        {
          system.activationScripts.postUserActivation.text = ''
            echo >&2 "Setting up the Dock..."
            haveURIs="$(${dockutil}/bin/dockutil --list | ${pkgs.coreutils}/bin/cut -f2)"
            if ! diff -wu <(echo -n "$haveURIs") <(echo -n '${wantURIs}') >&2 ; then
              echo >&2 "Resetting Dock."
              ${dockutil}/bin/dockutil --no-restart --remove all
              ${createEntries}
              killall Dock
            else
              echo >&2 "Dock setup complete."
            fi
          '';
        }
      );
}

@kahlstrm
Copy link

kahlstrm commented May 17, 2025

postUserActivation was removed, so here's an updated version for 25.05 using postActivation

{
  config,
  pkgs,
  lib,
  ...
}:

# Original source: https://gist.github.com/antifuchs/10138c4d838a63c0a05e725ccd7bccdd

with lib;
let
  cfg = config.local.dock;
  inherit (pkgs) stdenv dockutil;
in
{
  options = {
    local.dock.enable = mkOption {
      description = "Enable dock";
      default = stdenv.isDarwin;
      example = false;
    };

    local.dock.entries = mkOption {
      description = "Entries on the Dock";
      type =
        with types;
        listOf (submodule {
          options = {
            path = lib.mkOption { type = str; };
            section = lib.mkOption {
              type = str;
              default = "apps";
            };
            options = lib.mkOption {
              type = str;
              default = "";
            };
          };
        });
      readOnly = true;
    };
    local.dock.username = mkOption {
      description = "Username to apply the dock settings to";
      default = config.system.primaryUser;
      type = types.str;
    };
  };

  config = mkIf cfg.enable (
    let
      normalize = path: if hasSuffix ".app" path then path + "/" else path;
      entryURI =
        path:
        "file://"
        + (builtins.replaceStrings
          [
            " "
            "!"
            "\""
            "#"
            "$"
            "%"
            "&"
            "'"
            "("
            ")"
          ]
          [
            "%20"
            "%21"
            "%22"
            "%23"
            "%24"
            "%25"
            "%26"
            "%27"
            "%28"
            "%29"
          ]
          (normalize path)
        );
      wantURIs = concatMapStrings (entry: "${entryURI entry.path}\n") cfg.entries;
      createEntries = concatMapStrings (
        entry:
        "${dockutil}/bin/dockutil --no-restart --add '${entry.path}' --section ${entry.section} ${entry.options}\n"
      ) cfg.entries;
    in
    {
      system.activationScripts.postActivation.text = ''
          echo >&2 "Setting up the Dock for ${cfg.username}..."
          su ${cfg.username} -s /bin/sh <<'USERBLOCK'
        haveURIs="$(${dockutil}/bin/dockutil --list | ${pkgs.coreutils}/bin/cut -f2)"
        if ! diff -wu <(echo -n "$haveURIs") <(echo -n '${wantURIs}') >&2 ; then
          echo >&2 "Resetting Dock."
          ${dockutil}/bin/dockutil --no-restart --remove all
          ${createEntries}
          killall Dock
        else
          echo >&2 "Dock setup complete."
        fi
        USERBLOCK
      '';
    }
  );
}

and usage is now:

  # Fully declarative dock using the latest from Nix Store
  local.dock.enable = true;
  local.dock.entries = [
    # installed from brew/App Store
    { path = "/Applications/Ghostty.app/"; }
    { path = "/Applications/Brave Browser.app/"; }
    { path = "/Applications/Slack.app/"; }
    { path = "/System/Applications/System Settings.app"; }
    {
      path = "/Users/alice/Downloads/";
      section = "others";
    }
  ];
  # defaults to config.system.primaryUser
  local.dock.username = "alice";

@antifuchs
Copy link
Author

    local.dock.username = mkOption {
      description = "Username to apply the dock settings to";
      type = types.str;
    };
  };

That's a good catch - you can use default = config.system.primaryUser; there, so you don't have to set that manually.

@kahlstrm
Copy link

    local.dock.username = mkOption {
      description = "Username to apply the dock settings to";
      type = types.str;
    };
  };

That's a good catch - you can use default = config.system.primaryUser; there, so you don't have to set that manually.

Good point! Updated my Gist 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment