From 38b2bf3ede19e787e16709f3e770e3b06d87ffca Mon Sep 17 00:00:00 2001 From: eveeifyeve Date: Sun, 21 Jun 2026 06:47:22 +1000 Subject: [PATCH] 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 Co-authored-by: Dylan Frankland Co-authored-by: Austin Horstman --- .../misc/news/2026/06/2026-06-21_07-13-28.nix | 14 ++ .../darwin/user-defaults/opts-allhosts.nix | 202 ++++++++++++++++++ tests/modules/targets-darwin/default.nix | 9 +- .../default.nix} | 0 .../user-defaults/dock-null-empty-reverse.nix | 23 ++ .../user-defaults/dock-null-empty.nix | 23 ++ .../targets-darwin/user-defaults/dock.nix | 109 ++++++++++ 7 files changed, 378 insertions(+), 2 deletions(-) create mode 100644 modules/misc/news/2026/06/2026-06-21_07-13-28.nix rename tests/modules/targets-darwin/{user-defaults.nix => user-defaults/default.nix} (100%) create mode 100644 tests/modules/targets-darwin/user-defaults/dock-null-empty-reverse.nix create mode 100644 tests/modules/targets-darwin/user-defaults/dock-null-empty.nix create mode 100644 tests/modules/targets-darwin/user-defaults/dock.nix diff --git a/modules/misc/news/2026/06/2026-06-21_07-13-28.nix b/modules/misc/news/2026/06/2026-06-21_07-13-28.nix new file mode 100644 index 0000000000..a50d7b2fbf --- /dev/null +++ b/modules/misc/news/2026/06/2026-06-21_07-13-28.nix @@ -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. + ''; +} diff --git a/modules/targets/darwin/user-defaults/opts-allhosts.nix b/modules/targets/darwin/user-defaults/opts-allhosts.nix index 6ca29a0b26..5652c20a50 100644 --- a/modules/targets/darwin/user-defaults/opts-allhosts.nix +++ b/modules/targets/darwin/user-defaults/opts-allhosts.nix @@ -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" = { diff --git a/tests/modules/targets-darwin/default.nix b/tests/modules/targets-darwin/default.nix index eb6252278d..760977cc4e 100644 --- a/tests/modules/targets-darwin/default.nix +++ b/tests/modules/targets-darwin/default.nix @@ -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; } diff --git a/tests/modules/targets-darwin/user-defaults.nix b/tests/modules/targets-darwin/user-defaults/default.nix similarity index 100% rename from tests/modules/targets-darwin/user-defaults.nix rename to tests/modules/targets-darwin/user-defaults/default.nix diff --git a/tests/modules/targets-darwin/user-defaults/dock-null-empty-reverse.nix b/tests/modules/targets-darwin/user-defaults/dock-null-empty-reverse.nix new file mode 100644 index 0000000000..e74db97a40 --- /dev/null +++ b/tests/modules/targets-darwin/user-defaults/dock-null-empty-reverse.nix @@ -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 + ''; + }; +} diff --git a/tests/modules/targets-darwin/user-defaults/dock-null-empty.nix b/tests/modules/targets-darwin/user-defaults/dock-null-empty.nix new file mode 100644 index 0000000000..267ae90426 --- /dev/null +++ b/tests/modules/targets-darwin/user-defaults/dock-null-empty.nix @@ -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 + ''; + }; +} diff --git a/tests/modules/targets-darwin/user-defaults/dock.nix b/tests/modules/targets-darwin/user-defaults/dock.nix new file mode 100644 index 0000000000..bc501b1213 --- /dev/null +++ b/tests/modules/targets-darwin/user-defaults/dock.nix @@ -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 + ''; + }; +}