mirror of
https://github.com/nix-community/home-manager.git
synced 2026-10-02 19:49:18 +00:00
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:
committed by
Austin Horstman
parent
3f62c52cba
commit
38b2bf3ede
14
modules/misc/news/2026/06/2026-06-21_07-13-28.nix
Normal file
14
modules/misc/news/2026/06/2026-06-21_07-13-28.nix
Normal 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.
|
||||
'';
|
||||
}
|
||||
@@ -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" = {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
'';
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
'';
|
||||
};
|
||||
}
|
||||
109
tests/modules/targets-darwin/user-defaults/dock.nix
Normal file
109
tests/modules/targets-darwin/user-defaults/dock.nix
Normal 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
|
||||
'';
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user