Last active
May 26, 2026 17:07
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { 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 | |
| ''; | |
| } | |
| ); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { 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"; | |
| } | |
| ]; | |
| } |
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.
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 👍
Based off of @elbelga's config, but works for newer versions (because nix-darwin now runs as root), however, I use home.activation rather than su ${cfg.username} from @kahlstrm as I didn't really like that an extra option local.dock.username was necessary.
# Original source: https://gist.github.com/antifuchs/10138c4d838a63c0a05e725ccd7bccdd
# and @elbelga's modifications
{ config, pkgs, lib, ... }:
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
{
home.activation.dockPersistent = lib.hm.dag.entryAfter ["writeBoundary"] ''
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}
/usr/bin/killall Dock
else
echo >&2 "Dock setup complete."
fi
'';
}
);
}Example usage:
local.dock = {
enable = true;
entries = [
{ path = "/System/Applications/Apps.app"; }
{ type = "flex-spacer"; }
{ path = "/Applications/Firefox.app"; }
{ path = "/System/Applications/Utilities/Terminal.app"; }
{ type = "spacer"; }
{ path = "~/Applications/Home Manager Apps/Obsidian.app"; }
{ path = "/Applications/Emacs.app"; }
{ type = "small-spacer"; }
{ path = "/Applications/Discord.app"; }
{ path = "/Applications/Thunderbird.app"; }
{
path = "~/Downloads/";
section = "others";
options = "--sort dateadded --view fan --display stack";
}
];
};Notes:
- I had to change
killall Dockto/usr/bin/killall Dockotherwise nix would complain command not found. - The Downloads section mimics the layout that macOS does by default on my machine.
- Don't worry, I don't actually keep all of those spacers in my dock; it's just for demonstration purposes.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
postUserActivationwas removed, so here's an updated version for 25.05 usingpostActivationand usage is now: