targets/darwin: persistent-apps and persistent-others

Add typed Dock entries while preserving native tile dictionaries.
Verify generated plist values, path shorthands, and spacer defaults.

Taken from:
https://github.com/nix-darwin/nix-darwin/blob/master/modules/system/defaults/dock.nix

Assisted-by: qwen3-coder-30b-a3b-instruct, z-ai/glm-5.3-flash

Co-authored-by: Ricardo Pinto <hessian_loom_0u@icloud.com>
Co-authored-by: Dylan Frankland <dfrankland@users.noreply.github.com>
Co-authored-by: Austin Horstman <khaneliman12@gmail.com>
This commit is contained in:
eveeifyeve
2026-06-21 06:47:22 +10:00
committed by Austin Horstman
parent 3f62c52cba
commit 38b2bf3ede
7 changed files with 378 additions and 2 deletions

View File

@@ -0,0 +1,14 @@
{ pkgs, ... }:
{
time = "2026-06-20T21:13:28+00:00";
condition = pkgs.stdenv.isDarwin;
message = ''
The options `targets.darwin.defaults."com.apple.dock".persistent-apps`
and `targets.darwin.defaults."com.apple.dock".persistent-others` now
support shorthand entries for applications, files, folders, and spacers.
These options configure the macOS Dock declaratively, similarly to
nix-darwin. Existing native Dock tile dictionaries remain supported.
Restart the Dock or log out and back in for changes to take effect.
'';
}

View File

@@ -22,6 +22,14 @@ let
description = "Whether to enable ${name}.";
};
# Keep native dictionaries distinct from shorthand tags during type merging.
rawDockTileType = types.addCheck (types.attrsOf types.anything) (
tile:
tile ? tile-data
|| tile ? tile-type
|| !(tile ? app || tile ? file || tile ? folder || tile ? spacer)
);
safari = config."com.apple.Safari";
in
{
@@ -135,6 +143,200 @@ in
example = 64;
description = "Sets the size of the dock.";
};
persistent-apps = mkNullableOption {
description = ''
Persistent applications, spacers, files, and folders in the Dock.
Strings and paths are shorthand for applications. Native Dock tile
dictionaries are passed through unchanged and can be mixed with
shorthand entries.
Set to `null` to leave the setting unchanged or `[ ]` to clear it.
Restart the Dock or log out and back in for changes to take effect.
'';
type =
let
taggedType = types.attrTag {
app = lib.mkOption {
description = "An application to be added to the dock.";
type = types.str;
};
file = lib.mkOption {
description = "A file to be added to the dock.";
type = types.str;
};
folder = lib.mkOption {
description = "A folder to be added to the dock.";
type = types.str;
};
spacer = lib.mkOption {
description = "A spacer to be added to the dock. Can be small or regular size.";
type = types.submodule {
options.small = lib.mkOption {
type = types.bool;
default = false;
description = "Whether the spacer is small.";
};
};
};
};
simpleType = types.either types.str types.path;
toTagged = path: { app = toString path; };
in
types.listOf (types.coercedTo simpleType toTagged (types.either rawDockTileType taggedType));
apply =
let
toTile =
item:
if rawDockTileType.check item then
item
else if item ? app then
{
tile-data.file-data = {
_CFURLString = item.app;
_CFURLStringType = 0;
};
}
else if item ? spacer then
{
tile-data = { };
tile-type = if item.spacer.small then "small-spacer-tile" else "spacer-tile";
}
else if item ? folder then
{
tile-data.file-data = {
_CFURLString = "file://" + item.folder;
_CFURLStringType = 15;
};
tile-type = "directory-tile";
}
else if item ? file then
{
tile-data.file-data = {
_CFURLString = "file://" + item.file;
_CFURLStringType = 15;
};
tile-type = "file-tile";
}
else
item;
in
value: if value == null then null else map toTile value;
};
persistent-others = mkNullableOption {
description = ''
Persistent files and folders in the Dock.
Strings and paths are shorthand for folders. Native Dock tile
dictionaries are passed through unchanged and can be mixed with
shorthand entries.
Set to `null` to leave the setting unchanged or `[ ]` to clear it.
Restart the Dock or log out and back in for changes to take effect.
'';
type =
let
folderType = types.submodule {
options.path = lib.mkOption {
description = "Path to a folder to be added to the dock.";
type = types.str;
};
options.arrangement = lib.mkOption {
description = "Sort order for files in folder when clicked.";
type = types.enum [
"name"
"date-added"
"date-modified"
"date-created"
"kind"
];
default = "name";
};
options.displayas = lib.mkOption {
description = "How to display the folder before clicked. stack: Stack of file previews. folder: A folder icon";
type = types.enum [
"stack"
"folder"
];
default = "stack";
};
options.showas = lib.mkOption {
description = "Effect to show files when clicked. fan: fan-out effect, grid: box, list: list";
type = types.enum [
"automatic"
"fan"
"grid"
"list"
];
default = "automatic";
};
};
taggedType = types.attrTag {
file = lib.mkOption {
description = "A file to be added to the dock.";
type = types.str;
};
folder = lib.mkOption {
description = "A folder to be added to the dock.";
type = types.coercedTo types.str (str: { path = str; }) folderType;
};
};
simpleType = types.either types.str types.path;
toTagged = path: { folder = toString path; };
in
types.listOf (types.coercedTo simpleType toTagged (types.either rawDockTileType taggedType));
apply =
let
arrangementMap = {
name = 1;
date-added = 2;
date-modified = 3;
date-created = 4;
kind = 5;
};
displayasMap = {
stack = 0;
folder = 1;
};
showasMap = {
automatic = 0;
fan = 1;
grid = 2;
list = 3;
};
parseFolder =
folder:
builtins.mapAttrs (
name: val:
if name == "arrangement" then
arrangementMap.${val}
else if name == "displayas" then
displayasMap.${val}
else if name == "showas" then
showasMap.${val}
else
val
) folder;
toTile =
item:
if rawDockTileType.check item then
item
else
{
tile-data = {
file-data = {
_CFURLString = "file://" + (if item ? folder then item.folder.path else item.file);
_CFURLStringType = 15;
};
}
// (
if item ? folder then { inherit (parseFolder item.folder) arrangement displayas showas; } else { }
);
tile-type = if item ? folder then "directory-tile" else "file-tile";
};
in
value: if value == null then null else map toTile value;
};
};
"com.apple.finder" = {

View File

@@ -1,7 +1,12 @@
{
{ lib, pkgs, ... }:
lib.optionalAttrs pkgs.stdenv.hostPlatform.isDarwin {
# Disabled for now due to conflicting behavior with nix-darwin. See
# https://github.com/nix-community/home-manager/issues/1341#issuecomment-687286866
#targets-darwin = ./darwin.nix;
terminfo = ./terminfo.nix;
user-defaults = ./user-defaults.nix;
user-defaults = ./user-defaults;
user-defaults-dock = ./user-defaults/dock.nix;
user-defaults-dock-null-empty = ./user-defaults/dock-null-empty.nix;
user-defaults-dock-null-empty-reverse = ./user-defaults/dock-null-empty-reverse.nix;
}

View File

@@ -0,0 +1,23 @@
{ pkgs, ... }:
{
config = {
targets.darwin.defaults."com.apple.dock" = {
persistent-apps = null;
persistent-others = [ ];
};
nmt.script = ''
plist=$(grep -oE '/nix/store/[a-z0-9]+-com[.]apple[.]dock[.]plist' "$TESTED/activate")
test -n "$plist"
${pkgs.python3}/bin/python - "$plist" <<'PY'
import plistlib
import sys
with open(sys.argv[1], "rb") as stream:
plist = plistlib.load(stream)
assert plist == {"persistent-others": []}
PY
'';
};
}

View File

@@ -0,0 +1,23 @@
{ pkgs, ... }:
{
config = {
targets.darwin.defaults."com.apple.dock" = {
persistent-apps = [ ];
persistent-others = null;
};
nmt.script = ''
plist=$(grep -oE '/nix/store/[a-z0-9]+-com[.]apple[.]dock[.]plist' "$TESTED/activate")
test -n "$plist"
${pkgs.python3}/bin/python - "$plist" <<'PY'
import plistlib
import sys
with open(sys.argv[1], "rb") as stream:
plist = plistlib.load(stream)
assert plist == {"persistent-apps": []}
PY
'';
};
}

View File

@@ -0,0 +1,109 @@
{ pkgs, ... }:
{
config = {
targets.darwin.defaults."com.apple.dock" = {
persistent-apps = [
"/Applications/Calendar.app"
/.
{ app = "/Applications/Mail.app"; }
{ file = "/Users/test/notes.txt"; }
{ folder = "/Users/test/Documents"; }
{ spacer = { }; }
{ spacer.small = false; }
{ spacer.small = true; }
{
app = "metadata-that-must-not-be-reinterpreted";
tile-data.arbitrary-native-key = "preserved";
tile-type = "arbitrary-native-tile";
}
{ }
];
persistent-others = [
"/Users/test/Downloads"
{ folder.path = "/Users/test/Desktop"; }
{ folder = "/Users/test/Documents"; }
/.
{
folder = {
path = "/Users/test/Projects";
arrangement = "date-modified";
displayas = "folder";
showas = "list";
};
}
{ file = "/Users/test/archive.zip"; }
{
arbitrary-native-key = "preserved";
folder = "metadata-that-must-not-be-reinterpreted";
tile-data.file-data = {
_CFURLString = "file:///Users/test/native";
_CFURLStringType = 15;
};
tile-type = "directory-tile";
}
];
};
nmt.script = ''
plist=$(grep -oE '/nix/store/[a-z0-9]+-com[.]apple[.]dock[.]plist' "$TESTED/activate")
test -n "$plist"
${pkgs.python3}/bin/python - "$plist" <<'PY'
import plistlib
import sys
with open(sys.argv[1], "rb") as stream:
plist = plistlib.load(stream)
assert plist["persistent-apps"] == [
{"tile-data": {"file-data": {
"_CFURLString": "/Applications/Calendar.app", "_CFURLStringType": 0,
}}},
{"tile-data": {"file-data": {
"_CFURLString": "/", "_CFURLStringType": 0,
}}},
{"tile-data": {"file-data": {
"_CFURLString": "/Applications/Mail.app", "_CFURLStringType": 0,
}}},
{"tile-data": {"file-data": {
"_CFURLString": "file:///Users/test/notes.txt", "_CFURLStringType": 15,
}}, "tile-type": "file-tile"},
{"tile-data": {"file-data": {
"_CFURLString": "file:///Users/test/Documents", "_CFURLStringType": 15,
}}, "tile-type": "directory-tile"},
{"tile-data": {}, "tile-type": "spacer-tile"},
{"tile-data": {}, "tile-type": "spacer-tile"},
{"tile-data": {}, "tile-type": "small-spacer-tile"},
{"app": "metadata-that-must-not-be-reinterpreted",
"tile-data": {"arbitrary-native-key": "preserved"},
"tile-type": "arbitrary-native-tile"},
{},
]
assert plist["persistent-others"] == [
{"tile-data": {"file-data": {
"_CFURLString": "file:///Users/test/Downloads", "_CFURLStringType": 15,
}, "arrangement": 1, "displayas": 0, "showas": 0}, "tile-type": "directory-tile"},
{"tile-data": {"file-data": {
"_CFURLString": "file:///Users/test/Desktop", "_CFURLStringType": 15,
}, "arrangement": 1, "displayas": 0, "showas": 0}, "tile-type": "directory-tile"},
{"tile-data": {"file-data": {
"_CFURLString": "file:///Users/test/Documents", "_CFURLStringType": 15,
}, "arrangement": 1, "displayas": 0, "showas": 0}, "tile-type": "directory-tile"},
{"tile-data": {"file-data": {
"_CFURLString": "file:///", "_CFURLStringType": 15,
}, "arrangement": 1, "displayas": 0, "showas": 0}, "tile-type": "directory-tile"},
{"tile-data": {"file-data": {
"_CFURLString": "file:///Users/test/Projects", "_CFURLStringType": 15,
}, "arrangement": 3, "displayas": 1, "showas": 3}, "tile-type": "directory-tile"},
{"tile-data": {"file-data": {
"_CFURLString": "file:///Users/test/archive.zip", "_CFURLStringType": 15,
}}, "tile-type": "file-tile"},
{"arbitrary-native-key": "preserved",
"folder": "metadata-that-must-not-be-reinterpreted",
"tile-data": {"file-data": {
"_CFURLString": "file:///Users/test/native", "_CFURLStringType": 15,
}}, "tile-type": "directory-tile"},
]
PY
'';
};
}